Commit 8cec0939 authored by shigemi miura's avatar shigemi miura

・クラス分割

・チャットのイメージデータの先読み ・イメージデータ送信時はテンポラリを表示
parent d2befa8b
......@@ -79,6 +79,11 @@
D52D213A2AEBAC0500324D58 /* HttpRequestType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52D21392AEBAC0500324D58 /* HttpRequestType.swift */; };
D52D213F2AEBB7D700324D58 /* RegisteredEca.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52D213E2AEBB7D700324D58 /* RegisteredEca.swift */; };
D52D21412AEDE18F00324D58 /* EcaCoordinatesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52D21402AEDE18F00324D58 /* EcaCoordinatesTable.swift */; };
D52E916E2E860FBA00118E73 /* CustomTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52E916D2E860FA500118E73 /* CustomTabBar.swift */; };
D52E91742E86495C00118E73 /* NotificationBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52E91732E86494D00118E73 /* NotificationBadge.swift */; };
D52E91762E864E2100118E73 /* CustomTabBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52E91752E864E1500118E73 /* CustomTabBarViewModel.swift */; };
D52E917A2E86991E00118E73 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52E91792E86991000118E73 /* ChatViewModel.swift */; };
D52E91802E87CEA300118E73 /* ChatInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52E917F2E87CEA100118E73 /* ChatInputViewModel.swift */; };
D536F6712B678D8900A5BCF9 /* ReqUploadImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D536F6702B678D8900A5BCF9 /* ReqUploadImage.swift */; };
D539F3302C1809790088E609 /* NgaTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = D539F32F2C1809790088E609 /* NgaTask.swift */; };
D53B97262B341867000B3D29 /* AboutAppView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53B97252B341867000B3D29 /* AboutAppView.swift */; };
......@@ -94,6 +99,7 @@
D55186132E7E9158004CD8BD /* ImageQualityPreset.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */; };
D5598B782C435A5C00611AE0 /* ChatUrlImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5598B772C435A5C00611AE0 /* ChatUrlImageView.swift */; };
D5598B7A2C435C4500611AE0 /* ChatUrlRawImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5598B792C435C4500611AE0 /* ChatUrlRawImageView.swift */; };
D57405062E827C45001C74DF /* ChatUrlVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57405052E827C35001C74DF /* ChatUrlVideoView.swift */; };
D57905FE2C1C069000AF797C /* SetNgaArea.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57905FD2C1C069000AF797C /* SetNgaArea.swift */; };
D57906002C1C06F600AF797C /* SessionNgaList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57905FF2C1C06F600AF797C /* SessionNgaList.swift */; };
D57906022C1C0CFB00AF797C /* ReqNgaList.swift in Sources */ = {isa = PBXBuildFile; fileRef = D57906012C1C0CFB00AF797C /* ReqNgaList.swift */; };
......@@ -117,6 +123,14 @@
D5AE351A2AEBA66A00059889 /* ReqLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AE35182AEBA66A00059889 /* ReqLogin.swift */; };
D5AE351B2AEBA66A00059889 /* ResLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AE35192AEBA66A00059889 /* ResLogin.swift */; };
D5AE351D2AEBA6FC00059889 /* SessionLogin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AE351C2AEBA6FC00059889 /* SessionLogin.swift */; };
D5AF8A4E2E8915DF00BECA22 /* EcaMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A4D2E8915D700BECA22 /* EcaMenuView.swift */; };
D5AF8A502E89169600BECA22 /* NgaMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A4F2E89169000BECA22 /* NgaMenuView.swift */; };
D5AF8A522E89252400BECA22 /* MenuMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A512E89251E00BECA22 /* MenuMainView.swift */; };
D5AF8A542E892D0500BECA22 /* FuelSwitchingMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A532E892CF200BECA22 /* FuelSwitchingMainView.swift */; };
D5AF8A562E892DF200BECA22 /* EcaListMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A552E892DEB00BECA22 /* EcaListMainView.swift */; };
D5AF8A582E892EB900BECA22 /* EcaSettingMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A572E892EB200BECA22 /* EcaSettingMainView.swift */; };
D5AF8A5A2E892F4F00BECA22 /* NgaNotificationMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A592E892F4600BECA22 /* NgaNotificationMainView.swift */; };
D5AF8A5C2E892FC200BECA22 /* NgaSettingMainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AF8A5B2E892FB800BECA22 /* NgaSettingMainView.swift */; };
D5B493A02B48E930008B3620 /* terms.html in Resources */ = {isa = PBXBuildFile; fileRef = D5B4939E2B48E930008B3620 /* terms.html */; };
D5B493A12B48E930008B3620 /* privacy.html in Resources */ = {isa = PBXBuildFile; fileRef = D5B4939F2B48E930008B3620 /* privacy.html */; };
D5B493A42B48EE50008B3620 /* Manual in Resources */ = {isa = PBXBuildFile; fileRef = D5B493A32B48EE50008B3620 /* Manual */; };
......@@ -254,6 +268,11 @@
D52D21392AEBAC0500324D58 /* HttpRequestType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HttpRequestType.swift; path = Sailassist/Http/HttpRequestType.swift; sourceTree = SOURCE_ROOT; };
D52D213E2AEBB7D700324D58 /* RegisteredEca.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = RegisteredEca.swift; path = Sailassist/ECA/RegisteredEca.swift; sourceTree = SOURCE_ROOT; };
D52D21402AEDE18F00324D58 /* EcaCoordinatesTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EcaCoordinatesTable.swift; path = Sailassist/ECA/EcaCoordinatesTable.swift; sourceTree = SOURCE_ROOT; };
D52E916D2E860FA500118E73 /* CustomTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabBar.swift; sourceTree = "<group>"; };
D52E91732E86494D00118E73 /* NotificationBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBadge.swift; sourceTree = "<group>"; };
D52E91752E864E1500118E73 /* CustomTabBarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabBarViewModel.swift; sourceTree = "<group>"; };
D52E91792E86991000118E73 /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = "<group>"; };
D52E917F2E87CEA100118E73 /* ChatInputViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputViewModel.swift; sourceTree = "<group>"; };
D536F6702B678D8900A5BCF9 /* ReqUploadImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReqUploadImage.swift; path = Sailassist/Json/ReqUploadImage.swift; sourceTree = SOURCE_ROOT; };
D539F32F2C1809790088E609 /* NgaTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NgaTask.swift; sourceTree = "<group>"; };
D53ACA752B0AECF8008DCB18 /* Sailassist.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Sailassist.entitlements; path = Sailassist/Sailassist.entitlements; sourceTree = "<group>"; };
......@@ -270,6 +289,7 @@
D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageQualityPreset.swift; sourceTree = "<group>"; };
D5598B772C435A5C00611AE0 /* ChatUrlImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ChatUrlImageView.swift; path = Sailassist/Chat/View/ChatUrlImageView.swift; sourceTree = SOURCE_ROOT; };
D5598B792C435C4500611AE0 /* ChatUrlRawImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ChatUrlRawImageView.swift; path = Sailassist/Chat/View/ChatUrlRawImageView.swift; sourceTree = SOURCE_ROOT; };
D57405052E827C35001C74DF /* ChatUrlVideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUrlVideoView.swift; sourceTree = "<group>"; };
D57905FD2C1C069000AF797C /* SetNgaArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNgaArea.swift; sourceTree = "<group>"; };
D57905FF2C1C06F600AF797C /* SessionNgaList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SessionNgaList.swift; path = Sailassist/ServerSession/SessionNgaList.swift; sourceTree = SOURCE_ROOT; };
D57906012C1C0CFB00AF797C /* ReqNgaList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReqNgaList.swift; path = Sailassist/Json/ReqNgaList.swift; sourceTree = SOURCE_ROOT; };
......@@ -293,6 +313,14 @@
D5AE35182AEBA66A00059889 /* ReqLogin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ReqLogin.swift; path = Sailassist/Json/ReqLogin.swift; sourceTree = SOURCE_ROOT; };
D5AE35192AEBA66A00059889 /* ResLogin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ResLogin.swift; path = Sailassist/Json/ResLogin.swift; sourceTree = SOURCE_ROOT; };
D5AE351C2AEBA6FC00059889 /* SessionLogin.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionLogin.swift; path = Sailassist/ServerSession/SessionLogin.swift; sourceTree = SOURCE_ROOT; };
D5AF8A4D2E8915D700BECA22 /* EcaMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcaMenuView.swift; sourceTree = "<group>"; };
D5AF8A4F2E89169000BECA22 /* NgaMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NgaMenuView.swift; sourceTree = "<group>"; };
D5AF8A512E89251E00BECA22 /* MenuMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuMainView.swift; sourceTree = "<group>"; };
D5AF8A532E892CF200BECA22 /* FuelSwitchingMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuelSwitchingMainView.swift; sourceTree = "<group>"; };
D5AF8A552E892DEB00BECA22 /* EcaListMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcaListMainView.swift; sourceTree = "<group>"; };
D5AF8A572E892EB200BECA22 /* EcaSettingMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EcaSettingMainView.swift; sourceTree = "<group>"; };
D5AF8A592E892F4600BECA22 /* NgaNotificationMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NgaNotificationMainView.swift; sourceTree = "<group>"; };
D5AF8A5B2E892FB800BECA22 /* NgaSettingMainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NgaSettingMainView.swift; sourceTree = "<group>"; };
D5B4939E2B48E930008B3620 /* terms.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = terms.html; path = Sailassist/html/terms.html; sourceTree = SOURCE_ROOT; };
D5B4939F2B48E930008B3620 /* privacy.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = privacy.html; path = Sailassist/html/privacy.html; sourceTree = SOURCE_ROOT; };
D5B493A32B48EE50008B3620 /* Manual */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Manual; path = Sailassist/html/Manual; sourceTree = SOURCE_ROOT; };
......@@ -386,8 +414,6 @@
02CE4DC62ADF97D8002E79BC /* Extensions */,
020B986A2ADD3E4A0029DE4C /* Lib */,
020B985C2ADCFEF90029DE4C /* Preferences */,
020B98132AD8C3140029DE4C /* SailassistApp.swift */,
020B98152AD8C3150029DE4C /* ContentView.swift */,
020B98612ADD14AE0029DE4C /* Menu */,
020B98602ADD149E0029DE4C /* Alert */,
020B985F2ADD14970029DE4C /* Chat */,
......@@ -395,12 +421,14 @@
020B983F2AD8C3690029DE4C /* Map */,
020B983E2AD8C3500029DE4C /* Tab */,
020B983D2AD8C34A0029DE4C /* Login */,
020B98172AD8C3190029DE4C /* Assets.xcassets */,
020B98192AD8C3190029DE4C /* Preview Content */,
020B98132AD8C3140029DE4C /* SailassistApp.swift */,
020B98152AD8C3150029DE4C /* ContentView.swift */,
D5CB6F9A2B021C4F00EC2010 /* LocationViewModel.swift */,
D5B803272B3B988E003B32AD /* AppVersionModel.swift */,
D5CB6F9E2B0220A700EC2010 /* ErrorView.swift */,
027EF9F12B0D804C0079B825 /* WebView.swift */,
020B98172AD8C3190029DE4C /* Assets.xcassets */,
);
path = SailAssist;
sourceTree = "<group>";
......@@ -445,9 +473,12 @@
020B983E2AD8C3500029DE4C /* Tab */ = {
isa = PBXGroup;
children = (
D5AAB69C2E7A5E8F0027CD90 /* AlertModifiers.swift */,
D5AF8A4C2E88CE8100BECA22 /* View */,
02CE4DC92ADF9D2E002E79BC /* ViewModel */,
020B98582AD92A4C0029DE4C /* MainTabView.swift */,
D52E91732E86494D00118E73 /* NotificationBadge.swift */,
D52E91752E864E1500118E73 /* CustomTabBarViewModel.swift */,
D52E916D2E860FA500118E73 /* CustomTabBar.swift */,
D5AAB69C2E7A5E8F0027CD90 /* AlertModifiers.swift */,
);
path = Tab;
sourceTree = "<group>";
......@@ -456,8 +487,8 @@
isa = PBXGroup;
children = (
0227890F2AE6028000A87787 /* Task */,
02CE4DD92ADFBA72002E79BC /* MapRepresentable.swift */,
02CD068E2AE6535F005F8D8F /* MapSource.swift */,
02CE4DD92ADFBA72002E79BC /* MapRepresentable.swift */,
D5258C9A2B03401E00365276 /* MonitoringRoute.swift */,
D546B0122C197BB50058CD98 /* MapInformation.swift */,
);
......@@ -506,10 +537,10 @@
020B985F2ADD14970029DE4C /* Chat */ = {
isa = PBXGroup;
children = (
D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */,
020B98622ADD14E40029DE4C /* ChatView.swift */,
02A1DE2D2AFB497B005BCF55 /* View */,
D5AF8A4B2E88CDF600BECA22 /* ViewModel */,
D5258CA42B036F0700365276 /* GetMessage.swift */,
D55186122E7E914C004CD8BD /* ImageQualityPreset.swift */,
D524DAF72B5A6D3600A399DD /* Imagepicker.swift */,
);
path = Chat;
......@@ -556,8 +587,8 @@
0227890F2AE6028000A87787 /* Task */ = {
isa = PBXGroup;
children = (
022789112AE6028800A87787 /* ViewModel */,
022789102AE6028400A87787 /* View */,
022789112AE6028800A87787 /* ViewModel */,
);
path = Task;
sourceTree = "<group>";
......@@ -565,12 +596,20 @@
022789102AE6028400A87787 /* View */ = {
isa = PBXGroup;
children = (
D5C7823B2BFB4CEF001EBB3A /* FuelSwitchingView.swift */,
D5C782392BFB4C2A001EBB3A /* NgaNotificationView.swift */,
D5AF8A512E89251E00BECA22 /* MenuMainView.swift */,
D5C7822E2BF88755001EBB3A /* MenuTaskView.swift */,
02CE4D862ADF62A7002E79BC /* EcaListView.swift */,
02CE4D882ADF62E1002E79BC /* EcaSettingView.swift */,
D5AF8A4F2E89169000BECA22 /* NgaMenuView.swift */,
D5AF8A5B2E892FB800BECA22 /* NgaSettingMainView.swift */,
D5AF8A592E892F4600BECA22 /* NgaNotificationMainView.swift */,
D5C782482BFC2DA2001EBB3A /* NgaSettingView.swift */,
D5C782392BFB4C2A001EBB3A /* NgaNotificationView.swift */,
D5AF8A4D2E8915D700BECA22 /* EcaMenuView.swift */,
D5AF8A572E892EB200BECA22 /* EcaSettingMainView.swift */,
02CE4D882ADF62E1002E79BC /* EcaSettingView.swift */,
D5AF8A552E892DEB00BECA22 /* EcaListMainView.swift */,
02CE4D862ADF62A7002E79BC /* EcaListView.swift */,
D5C7823B2BFB4CEF001EBB3A /* FuelSwitchingView.swift */,
D5AF8A532E892CF200BECA22 /* FuelSwitchingMainView.swift */,
);
path = View;
sourceTree = "<group>";
......@@ -624,9 +663,11 @@
02A1DE2D2AFB497B005BCF55 /* View */ = {
isa = PBXGroup;
children = (
020B98622ADD14E40029DE4C /* ChatView.swift */,
02A1DE2E2AFB4AA0005BCF55 /* ChatInputView.swift */,
D57405052E827C35001C74DF /* ChatUrlVideoView.swift */,
D5A4C2522E7AB46B00642D7D /* SpeechRecognizer.swift */,
02C3E5D02AFCC16800AF7837 /* ChatTitleView.swift */,
02A1DE2E2AFB4AA0005BCF55 /* ChatInputView.swift */,
02A1DE302AFB61D8005BCF55 /* MyChatContentView.swift */,
02C3E5CD2AFCA04700AF7837 /* OtherChatContentView.swift */,
02A1DE322AFB654A005BCF55 /* CustomCornerRadius.swift */,
......@@ -768,6 +809,23 @@
path = Json;
sourceTree = "<group>";
};
D5AF8A4B2E88CDF600BECA22 /* ViewModel */ = {
isa = PBXGroup;
children = (
D52E91792E86991000118E73 /* ChatViewModel.swift */,
D52E917F2E87CEA100118E73 /* ChatInputViewModel.swift */,
);
path = ViewModel;
sourceTree = "<group>";
};
D5AF8A4C2E88CE8100BECA22 /* View */ = {
isa = PBXGroup;
children = (
020B98582AD92A4C0029DE4C /* MainTabView.swift */,
);
path = View;
sourceTree = "<group>";
};
D5C782452BFB55F0001EBB3A /* NGA */ = {
isa = PBXGroup;
children = (
......@@ -1006,10 +1064,12 @@
D52D21412AEDE18F00324D58 /* EcaCoordinatesTable.swift in Sources */,
02A1DE2F2AFB4AA0005BCF55 /* ChatInputView.swift in Sources */,
D57905FE2C1C069000AF797C /* SetNgaArea.swift in Sources */,
D5AF8A562E892DF200BECA22 /* EcaListMainView.swift in Sources */,
020B98532AD919180029DE4C /* LoginTypeSelectView.swift in Sources */,
D52D213F2AEBB7D700324D58 /* RegisteredEca.swift in Sources */,
02CD06952AE895F5005F8D8F /* APIError.swift in Sources */,
D5B803282B3B988E003B32AD /* AppVersionModel.swift in Sources */,
D52E91762E864E2100118E73 /* CustomTabBarViewModel.swift in Sources */,
D592D5232B0F14FE00B91A1C /* SessionUploadImage.swift in Sources */,
D5FCEF552B478985009A81D0 /* ResChatMessage.swift in Sources */,
D5598B782C435A5C00611AE0 /* ChatUrlImageView.swift in Sources */,
......@@ -1026,6 +1086,7 @@
02C3E6092AFDF30000AF7837 /* ChatMemberView.swift in Sources */,
020B98412AD8C3810029DE4C /* LoginView.swift in Sources */,
D5AE351D2AEBA6FC00059889 /* SessionLogin.swift in Sources */,
D5AF8A522E89252400BECA22 /* MenuMainView.swift in Sources */,
D5258C9F2B03527400365276 /* ResGetMessages.swift in Sources */,
D55135242B15C3BF007B66B1 /* DeleteEcaArea.swift in Sources */,
D524DAFA2B5A6F5F00A399DD /* CameraView.swift in Sources */,
......@@ -1045,12 +1106,15 @@
020B98632ADD14E50029DE4C /* ChatView.swift in Sources */,
D55186132E7E9158004CD8BD /* ImageQualityPreset.swift in Sources */,
02CE4DDA2ADFBA72002E79BC /* MapRepresentable.swift in Sources */,
D5AF8A542E892D0500BECA22 /* FuelSwitchingMainView.swift in Sources */,
020B985E2ADCFF130029DE4C /* PreferencesKey.swift in Sources */,
0227890C2AE22E0B00A87787 /* SharingData.swift in Sources */,
D57906002C1C06F600AF797C /* SessionNgaList.swift in Sources */,
020B984D2AD9178F0029DE4C /* ScannerViewModel.swift in Sources */,
020B98142AD8C3140029DE4C /* SailassistApp.swift in Sources */,
D5AF8A502E89169600BECA22 /* NgaMenuView.swift in Sources */,
D546B0132C197BB50058CD98 /* MapInformation.swift in Sources */,
D57405062E827C45001C74DF /* ChatUrlVideoView.swift in Sources */,
D5CB6F9F2B0220A700EC2010 /* ErrorView.swift in Sources */,
D5CB6F992B02088C00EC2010 /* ResShipStatus.swift in Sources */,
023DB8282B1429E400B351CF /* NotificationContentView.swift in Sources */,
......@@ -1059,9 +1123,12 @@
020B984B2AD915810029DE4C /* QRReadView.swift in Sources */,
02A1DE332AFB654A005BCF55 /* CustomCornerRadius.swift in Sources */,
D545FC762B09C81300F206D0 /* PushNotificationTypes.swift in Sources */,
D5AF8A4E2E8915DF00BECA22 /* EcaMenuView.swift in Sources */,
D5E008742B2ADD3F00C4070A /* MenuManualECDISView.swift in Sources */,
D52E91742E86495C00118E73 /* NotificationBadge.swift in Sources */,
D545FC742B09C74300F206D0 /* AlertManager.swift in Sources */,
020B98692ADD221E0029DE4C /* Preferences.swift in Sources */,
D52E916E2E860FBA00118E73 /* CustomTabBar.swift in Sources */,
D54A5FD72B8F10B500F3A9D6 /* StatusEnum.swift in Sources */,
D536F6712B678D8900A5BCF9 /* ReqUploadImage.swift in Sources */,
02CD06932AE88970005F8D8F /* ServerSession.swift in Sources */,
......@@ -1085,13 +1152,16 @@
D5258CA12B03593500365276 /* SessionMonitoringRoute.swift in Sources */,
D5E03A672B04484D00D65FCE /* SessionTaskList.swift in Sources */,
020B986E2ADD3E8D0029DE4C /* DateTextLib.swift in Sources */,
D52E917A2E86991E00118E73 /* ChatViewModel.swift in Sources */,
D5258C9D2B03507A00365276 /* ResMonitoringRoute.swift in Sources */,
D5C782462BFB55F0001EBB3A /* DeleteNgaArea.swift in Sources */,
D5C782492BFC2DA2001EBB3A /* NgaSettingView.swift in Sources */,
020B98672ADD15050029DE4C /* MenuView.swift in Sources */,
D52E91802E87CEA300118E73 /* ChatInputViewModel.swift in Sources */,
020B98572AD924F50029DE4C /* CameraPreview.swift in Sources */,
D5CB6FA12B023F7F00EC2010 /* EcaTask.swift in Sources */,
D59908C82B1ABD43000E13DD /* SessionPushHistory.swift in Sources */,
D5AF8A582E892EB900BECA22 /* EcaSettingMainView.swift in Sources */,
02CD068F2AE6535F005F8D8F /* MapSource.swift in Sources */,
02C3E5CE2AFCA04700AF7837 /* OtherChatContentView.swift in Sources */,
020B98442AD8E4910029DE4C /* FontStyle.swift in Sources */,
......@@ -1116,7 +1186,9 @@
02C3E5D12AFCC16800AF7837 /* ChatTitleView.swift in Sources */,
024EDE1D2B0C42F70013BAC8 /* MenuGpsSelectView.swift in Sources */,
020B98472AD8FEE30029DE4C /* ColorSet.swift in Sources */,
D5AF8A5C2E892FC200BECA22 /* NgaSettingMainView.swift in Sources */,
D58EF2452B901D3900FB784C /* SessionInformation.swift in Sources */,
D5AF8A5A2E892F4F00BECA22 /* NgaNotificationMainView.swift in Sources */,
D58EF2492B90562B00FB784C /* GetInformation.swift in Sources */,
D524DAF82B5A6D3600A399DD /* Imagepicker.swift in Sources */,
D51AA4072B099FCC00EBBDD4 /* AlertDB.swift in Sources */,
......
......@@ -38,9 +38,9 @@ struct NotificationView: View {
.onAppear {
let pushCount = pushHist.viewCnt
print(debug: "\(pushCount)")
DispatchQueue.main.async {
pushHist.viewCnt = 0
}
// DispatchQueue.main.async {
// pushHist.viewCnt = 0
// }
}
}
......
......@@ -9,6 +9,7 @@ import Foundation
class GetMessage {
var sessionGetMessage = SessionGetMessage()
var chatViewModel: ChatViewModel?
func start() {
print(debug: "called")
......@@ -27,6 +28,11 @@ class GetMessage {
if let msg = res.messages {
//既読マーク確認
SharingData.message.messages = msg
Task { @MainActor in
self.chatViewModel?.loadMessages()
}
self.checkUnreadMessages()
}
SharingData.message.users = []
......
......@@ -12,20 +12,21 @@ enum ImageQualityPreset {
}
}
var targetSize: CGSize {
var scale: CGFloat {
switch self {
case .low: return CGSize(width: 640, height: 480)
case .middle: return CGSize(width: 1280, height: 960)
case .high: return CGSize(width: 1920, height: 1440)
case .low: return 0.3
case .middle: return 0.6
case .high: return 1.0
}
}
}
extension UIImage {
func resized(to targetSize: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: targetSize)
func resized(to scale: CGFloat) -> UIImage? {
let newSize = CGSize(width: self.size.width * scale, height: self.size.height * scale)
let renderer = UIGraphicsImageRenderer(size: newSize)
return renderer.image { _ in
self.draw(in: CGRect(origin: .zero, size: targetSize))
self.draw(in: CGRect(origin: .zero, size: newSize))
}
}
}
......@@ -41,19 +41,46 @@ struct Imagepicker : UIViewControllerRepresentable {
}
//MARK: - Use Photo
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
if let image = info[.originalImage] as? UIImage {
let resizedImage = image.resized(to: parent.preset.targetSize)
if let data = resizedImage.jpegData(compressionQuality: parent.preset.compressionQuality) {
parent.image = data
//MARK: - 画像データのリサイズ
DispatchQueue.global(qos: .userInitiated).async {
// リサイズ処理
let scale: CGFloat = min(0.1, max(self.parent.preset.scale, 1.0))
if let resizedImage = image.resized(to: scale),
let data = resizedImage.jpegData(compressionQuality: self.parent.preset.compressionQuality) {
DispatchQueue.main.async {
self.parent.image = data
self.parent.show.toggle()
}
} else {
DispatchQueue.main.async {
print("Failed to resize or compress image")
self.parent.show.toggle()
}
}
}
} else if let videoURL = info[.mediaURL] as? URL {
// 動画ファイルの処理(例: Dataに変換して保存)
if let videoData = try? Data(contentsOf: videoURL) {
parent.image = videoData
//MARK: - 動画データのリサイズ
DispatchQueue.global(qos: .userInitiated).async {
if let videoData = try? Data(contentsOf: videoURL) {
DispatchQueue.main.async {
self.parent.image = videoData
self.parent.show.toggle()
}
} else {
DispatchQueue.main.async {
print("Failed to process video data")
self.parent.show.toggle()
}
}
}
} else {
DispatchQueue.main.async {
print("No valid media selected")
self.parent.show.toggle()
}
}
parent.show.toggle()
}
}
}
import SwiftUI
import Combine
class KeyboardResponder: ObservableObject {
@Published var currentHeight: CGFloat = 0
@Published var isKeyboardVisible: Bool = false
private var cancellableSet: Set<AnyCancellable> = []
init() {
let willShow = NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)
.compactMap { notification -> CGFloat? in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.isKeyboardVisible = true
}
return (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect)?.height ?? 0
}
let willHide = NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ -> CGFloat in
DispatchQueue.main.async {
self.isKeyboardVisible = false
}
return 0
}
Publishers.Merge(willShow, willHide)
.receive(on: RunLoop.main)
.assign(to: \KeyboardResponder.currentHeight, on: self)
.store(in: &cancellableSet)
}
deinit {
cancellableSet.forEach { $0.cancel() }
}
}
import SwiftUI
import Speech
enum MediaInputType {
case none
case camera
case photoLibrary
case fileImport
}
struct ChatInputView: View {
@EnvironmentObject private var sceneDelegate: SceneDelegate
@ObservedObject var sessionUploadImage = SessionUploadImage()
@StateObject private var speechRecognizer = SpeechRecognizer()
@State private var inputText = ""
@State private var isChatAlert = false
@StateObject private var viewModel: ChatInputViewModel
@State private var imageData: Data = .init(capacity: 0)
@State private var mediaInputType: MediaInputType = .none
@State private var isImagePickerPresented = false
@State private var isFileImporterPresented = false
@State private var source: UIImagePickerController.SourceType = .photoLibrary
@State private var isSignalrRestart = false
@State private var isUploadingDialogPresented = false
@State private var failedUploadImage: ReqUploadImage? = nil
@State private var isRetryDialogPresented = false
@State private var isRecording = false
@State private var textViewHeight: CGFloat = 40
@FocusState private var isKeyboardFocused: Bool
@Binding var isFocus: Bool
@FocusState var isKeyboardFocused: Bool
@Binding private var isFocus: Bool
@Binding var isUploadingDialogPresented: Bool
init(isFocus: Binding<Bool>, isUploadingDialogPresented: Binding<Bool>) {
self._isFocus = isFocus
self._isUploadingDialogPresented = isUploadingDialogPresented
self._viewModel = StateObject(wrappedValue: ChatInputViewModel())
}
var body: some View {
VStack(spacing: 0) {
......@@ -40,9 +29,8 @@ struct ChatInputView: View {
// MARK: - カメラボタン
Button {
sceneDelegate.tabWindow?.isHidden = true
mediaInputType = .camera
source = .camera
isImagePickerPresented = true
viewModel.handleMediaInput(type: .camera)
} label: {
Image(systemName: "camera")
.resizable()
......@@ -54,17 +42,15 @@ struct ChatInputView: View {
Menu {
Button {
sceneDelegate.tabWindow?.isHidden = true
mediaInputType = .photoLibrary
source = .photoLibrary
isImagePickerPresented = true
viewModel.handleMediaInput(type: .photoLibrary)
} label: {
Label("Photo Library", systemImage: "photo.on.rectangle")
}
Button {
sceneDelegate.tabWindow?.isHidden = true
mediaInputType = .fileImport
isFileImporterPresented = true
viewModel.handleMediaInput(type: .photoLibrary)
} label: {
Label("Choose File", systemImage: "folder")
}
......@@ -76,39 +62,39 @@ struct ChatInputView: View {
}
//MARK: - 音声入力ボタン
// Button {
// isRecording.toggle()
// if isRecording {
// speechRecognizer.transcribedText = ""
// speechRecognizer.startRecording()
// } else {
// speechRecognizer.stopRecording()
// inputText = speechRecognizer.transcribedText
// }
// } label: {
// Image(systemName: isRecording ? "mic.fill" : "mic")
// .resizable()
// .frame(width: 20, height: 20)
// .padding(5)
// }
Button {
viewModel.isRecording.toggle()
if viewModel.isRecording {
speechRecognizer.transcribedText = ""
speechRecognizer.startRecording()
} else {
speechRecognizer.stopRecording()
viewModel.inputText = speechRecognizer.transcribedText
}
} label: {
Image(systemName: viewModel.isRecording ? "mic.fill" : "mic")
.resizable()
.frame(width: 20, height: 20)
.padding(5)
}
//MARK: - テキスト入力
TextEditor(text: $inputText)
TextEditor(text: $viewModel.inputText)
.focused($isKeyboardFocused)
.font(FontStyle.DefaultText.font)
.scrollContentBackground(Visibility.hidden)
.foregroundColor(ColorSet.BodyChat.color)
.background(ColorSet.ChatBaloon.color)
.cornerRadius(5)
.frame(height: textViewHeight)
.frame(height: viewModel.textViewHeight)
.onAppear {
recalculateHeight()
viewModel.recalculateHeight()
}
.onChange(of: inputText) { _ in
recalculateHeight()
.onChange(of: viewModel.inputText) { _ in
viewModel.recalculateHeight()
}
.overlay(alignment: .topLeading) {
if inputText.isEmpty {
if viewModel.inputText.isEmpty {
Text("Enter your message")
.font(FontStyle.DefaultText.font)
.foregroundColor(ColorSet.BodyNotice.color)
......@@ -119,50 +105,44 @@ struct ChatInputView: View {
.padding(EdgeInsets(top: 2, leading: 10, bottom: 3, trailing: 0))
.onChange(of: isKeyboardFocused) { isFocused in
sceneDelegate.tabWindow?.isHidden = isFocused
viewModel.isKeyboardFocused = isFocused
isFocus = isFocused
}
.onChange(of: viewModel.isKeyboardFocused) { isFocused in
isKeyboardFocused = isFocused
}
//MARK: - 送信ボタン
Button {
sendChatMessage()
viewModel.sendChatMessage()
} label: {
Image("send")
.resizable()
.frame(width: 20, height: 20)
.padding(5)
.opacity(inputText.isEmpty ? 0.5 : 1.0)
.opacity(viewModel.inputText.isEmpty ? 0.5 : 1.0)
}
.padding(.vertical, 6)
.padding(.trailing, 11)
.disabled(inputText.isEmpty)
.disabled(viewModel.inputText.isEmpty)
}
.background(ColorSet.BackgroundSecondary.color)
// MARK: - 画像ピッカー
.fullScreenCover(isPresented: $isImagePickerPresented, onDismiss: {
.fullScreenCover(isPresented: $viewModel.isImagePickerPresented, onDismiss: {
sceneDelegate.tabWindow?.isHidden = false
isKeyboardFocused = false
viewModel.isKeyboardFocused = false
if let image = UIImage(data: imageData),
let jpegData = image.jpegData(compressionQuality: 1.0) {
let uploadImage = ReqUploadImage(
shipId: Preferences.shipId,
messageId: UUID().uuidString,
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
files: jpegData
)
sendChatImage(uploadImage)
if let image = UIImage(data: imageData) {
viewModel.sendImageToTemporary(image)
}
}) {
CameraView(imageData: $imageData, source: $source, isActionSheet: .constant(false), isImagePicker: $isImagePickerPresented)
CameraView(imageData: $imageData, source: $source, isActionSheet: .constant(false), isImagePicker: $viewModel.isImagePickerPresented)
}
//MARK: - File Importer
.fileImporter(isPresented: $isFileImporterPresented, allowedContentTypes: [.png, .jpeg, .pdf]) { result in
.fileImporter(isPresented: $viewModel.isFileImporterPresented, allowedContentTypes: [.png, .jpeg, .pdf]) { result in
switch result {
case .success(let url):
guard let data = try? Data(contentsOf: url),
......@@ -178,7 +158,7 @@ struct ChatInputView: View {
files: jpegData
)
sendChatImage(uploadImage)
viewModel.sendChatImage(uploadImage)
case .failure:
print("File import failed")
}
......@@ -187,125 +167,49 @@ struct ChatInputView: View {
.onChange(of: isFocus) { focus in
if !focus {
isKeyboardFocused = false
viewModel.isKeyboardFocused = false
}
}
.onReceive(speechRecognizer.$transcribedText) { newText in
if isRecording {
inputText = newText
if viewModel.isRecording {
viewModel.inputText = newText
}
}
}
.onAppear {
isKeyboardFocused = viewModel.isKeyboardFocused
}
.frame(maxHeight: 55)
.alert("Error", isPresented: $isChatAlert) {
.alert("Error", isPresented: $viewModel.isChatAlert) {
Button("OK") {
if isSignalrRestart {
if viewModel.isSignalrRestart {
SignalR().startConnection()
isSignalrRestart = false
viewModel.isSignalrRestart = false
}
}
} message: {
Text("The message could not be sent.")
}
.alert("Uploading...", isPresented: $isUploadingDialogPresented) {
ProgressView(value: sessionUploadImage.progress)
.progressViewStyle(LinearProgressViewStyle())
.padding()
Button("Cancel") {
sessionUploadImage.cancelUpload()
isUploadingDialogPresented = false
}
}
.alert("Upload failed", isPresented: $isRetryDialogPresented) {
.alert("Upload failed", isPresented: $viewModel.isRetryDialogPresented) {
Button("retry") {
if let imageToRetry = failedUploadImage {
sendChatImage(imageToRetry)
if let imageToRetry = viewModel.failedUploadImage {
viewModel.sendChatImage(imageToRetry)
}
}
Button("Cancel", role: .cancel) {
failedUploadImage = nil
isRetryDialogPresented = false
viewModel.failedUploadImage = nil
viewModel.isRetryDialogPresented = false
SharingData.message.messages.removeAll { $0.messageId.caseInsensitiveCompare(viewModel.tempId) == .orderedSame }
}
} message: {
Text("Image upload failed. Would you like to try again?")
}
}
// MARK: - Send Chat Image
func sendChatImage(_ uploadImage: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await sessionUploadImage.requestUploadImage(uploadImage)
print("Upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
sessionUploadImage.progress = 0.0 // 完了後リセット
isUploadingDialogPresented = false
} catch {
print("Upload failed: \(error)")
sessionUploadImage.progress = 0.0 // エラー時もリセット
isUploadingDialogPresented = false
failedUploadImage = uploadImage
isRetryDialogPresented = true
}
}
}
// MARK: - Send Chat Message
func sendChatMessage() {
guard !SharingData.message.sendInf else { return }
isRecording = false
SignalR().chatMessage(message: inputText, completion: responseChatMessage)
SharingData.message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
restartChatMessage()
}
}
// MARK: - Chat Response
func responseChatMessage(error: Error?) {
SharingData.message.sendInf = false
if let error = error {
print("Chat error: \(error)")
isChatAlert = true
} else {
isKeyboardFocused = false
inputText = ""
}
}
// MARK: - Restart Chat if No Response
func restartChatMessage() {
if SharingData.message.sendInf {
SignalR().stopConnection()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
isSignalrRestart = true
}
}
}
//MARK: - テキスト高さ
private func recalculateHeight() {
let textView = UITextView()
textView.text = inputText
textView.font = UIFont.preferredFont(forTextStyle: .body)
let fixedWidth = UIScreen.main.bounds.width - 40 // 適宜調整
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
textViewHeight = max(newSize.height, 40) // 最低高さを40に設定
}
}
#Preview {
ChatInputView(isFocus: .constant(false))
ChatInputView(isFocus: .constant(false), isUploadingDialogPresented: .constant(false))
.environmentObject(SceneDelegate())
}
......@@ -8,55 +8,42 @@
import SwiftUI
struct ChatUrlImageView: View {
var imageUrl = ""
init(message: ChatMessage) {
if let url = message.message {
self.imageUrl = url
}
}
let imageUrl: String
var onLoad: (() -> Void)? = nil
var body: some View {
AsyncImage(url: URL(string: imageUrl)) { phase in
if let image = phase.image {
image
.resizable(resizingMode: .stretch)
.aspectRatio(contentMode: .fit)
.frame(width: 250)
} else if phase.error != nil {
Color.gray.opacity(0.2)
.overlay(Image(systemName: "rectangle.slash"))
} else {
Color.gray.opacity(0.2)
ProgressView()
if let url = URL(string: imageUrl) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let image):
image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 250)
.onAppear {
onLoad?()
}
case .failure(_):
VStack {
Image(systemName: "exclamationmark.triangle")
Text("Image loading failed")
.font(.caption)
}
.frame(width: 250, height: 250)
.background(Color.gray.opacity(0.2))
case .empty:
ProgressView()
.frame(width: 250, height: 250)
.background(Color.gray.opacity(0.2))
@unknown default:
EmptyView()
}
}
} else {
Color.gray.opacity(0.2)
.frame(width: 250, height: 250)
.overlay(Text("Invalid URL"))
}
.cornerRadius(16)
.frame(height: 250)
}
}
#Preview {
ChatUrlImageView(message: ChatMessage(
shipId: 10000003,
messageId: "92c2dfb5-f5ed-4943-98a3-9848d7f9a962",
type: 0,
time: "2023-10-06T01:51:01.872Z",
location: 1,
from: "はだだ",
fromId: "487420489",
mode: 0,
message: "999",
stampId: 0,
viewer: [
Viewer(
time: "2023-10-06T01:51:12.973Z",
location: 1,
id: ""),
Viewer(
time: "2023-10-06T01:51:12.973Z",
location: 2,
id: "")
]
))
}
import SwiftUI
import AVKit
struct ChatUrlVideoView: View {
@State private var player: AVPlayer?
@State private var isPlaying = false
let imageUrl: String
var onLoad: (() -> Void)? = nil
var body: some View {
ZStack {
if let url = URL(string: imageUrl) {
VideoPlayer(player: player)
.frame(height: 250)
.cornerRadius(16)
.onAppear {
player = AVPlayer(url: url)
onLoad?()
}
.onTapGesture {
if isPlaying {
player?.pause()
} else {
player?.play()
}
isPlaying.toggle()
}
} else {
Color.gray.opacity(0.2)
.frame(height: 250)
.overlay(
VStack {
Image(systemName: "video.slash")
Text("Video URL is invalid")
.font(.caption)
}
)
}
}
}
}
......@@ -13,6 +13,10 @@ struct ChatView: View {
@State var isShowMember: Bool = false
@State var isNotification = Preferences.ChatNotification
@State var isFocus: Bool = true
@State private var loadedMediaCount: Int = 0
@State private var totalMediaCount: Int = 0
@State private var isMediaLoading: Bool = false
@State private var isUploadingDialogPresented: Bool = false
var body: some View {
ZStack {
......@@ -30,12 +34,12 @@ struct ChatView: View {
ForEach(message.messages, id: \.messageId) { msg in
if msg.message != nil {
if msg.from == Preferences.UserName {
//自分のメッセージ
MyChatContentView(message: msg)
//MARK: - 自分のメッセージ
MyChatContentView(message: msg, onMediaLoaded: {handleMediaLoaded(proxy: proxy)})
.padding(.bottom, 24)
}else{
//他人のメッセージ
OtherChatContentView(message: msg)
//MARK: - 他人のメッセージ
OtherChatContentView(message: msg, onMediaLoaded: {handleMediaLoaded(proxy: proxy)})
.padding(.bottom, 24)
}
} else {
......@@ -48,27 +52,32 @@ struct ChatView: View {
.onAppear {
guard !message.messages.isEmpty,
let id = message.messages.last?.messageId else { return }
proxy.scrollTo(id, anchor: .bottom)
DispatchQueue.main.async {
message.viewCnt = 0
totalMediaCount = message.messages.reduce(0) { count, msg in
count + (msg.type >= 2 ? 1 : 0)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
}
}
.onChange(of: message.messages.count) { newValue in
print(debug: "ChatView: onChange { newValue: \(newValue)")
withAnimation {
if let id = message.messages.last?.messageId {
proxy.scrollTo(id, anchor: .bottom)
//Warningモードは既読は返さない
if message.messages.last?.message != nil {
if message.messages.last?.from != Preferences.UserName {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let getMessage = GetMessage()
getMessage.readNotification()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
//MARK: - Warningモードは既読は返さない
if message.messages.last?.message != nil {
if message.messages.last?.from != Preferences.UserName {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let getMessage = GetMessage()
getMessage.readNotification()
}
}
}
}
message.viewCnt = 0
}
}
}
......@@ -77,7 +86,6 @@ struct ChatView: View {
guard !value.isEmpty else {return}
if let id = message.messages.last?.messageId {
proxy.scrollTo(id, anchor: .bottom)
message.viewCnt = 0
}
}
}
......@@ -116,19 +124,80 @@ struct ChatView: View {
.onTapGesture {
isFocus = false
}
ChatInputView(isFocus: $isFocus)
.onAppear {
//MARK: - 画面が表示された時に実行 既読処理
if message.viewCnt > 0 {
message.viewCnt = 0
}
}
ChatInputView(isFocus: $isFocus, isUploadingDialogPresented: $isUploadingDialogPresented)
}
if !isMediaLoading {
LoadingView()
}
if isUploadingDialogPresented {
// UpLoadingView()
}
}
.background(ColorSet.BackgroundPrimary.color)
}
//MARK: - 動画・画像読み込み完了後に最新チャットに移動
private func handleMediaLoaded(proxy: ScrollViewProxy) {
guard !message.messages.isEmpty,
let id = message.messages.last?.messageId else { return }
loadedMediaCount += 1
if loadedMediaCount >= totalMediaCount {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
proxy.scrollTo(id, anchor: .bottom)
isMediaLoading = true
}
}
}
}
// MARK: - 全てのチャットのローディング画面
struct LoadingView: View {
var body: some View {
ZStack {
Color.black.opacity(1.0)
.edgesIgnoringSafeArea(.all)
ProgressView("Loading...")
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.foregroundColor(.white)
.padding()
.background(Color.gray.opacity(0.7))
.cornerRadius(10)
}
}
}
// MARK: - 動画・画像のアップロード画面
struct UpLoadingView: View {
var body: some View {
ZStack {
Color.black.opacity(0.8)
.edgesIgnoringSafeArea(.all)
ProgressView("UpLoading...")
.progressViewStyle(CircularProgressViewStyle(tint: .white))
.foregroundColor(.white)
.padding()
.background(Color.gray.opacity(0.7))
.cornerRadius(10)
}
}
}
struct AlertChatMessage: View {
@Environment(\ .colorScheme) var colorScheme
@Environment(\.colorScheme) var colorScheme
var message : ChatMessage
var body: some View {
if message.mode == ChatMode.warningProgress.rawValue {
switch message.mode {
case ChatMode.warningProgress.rawValue:
HStack() {
Rectangle()
.fill(ColorSet.ChatDate.color)
......@@ -140,7 +209,7 @@ struct AlertChatMessage: View {
.fill(ColorSet.ChatDate.color)
.frame(width: 20, height: 1)
}
} else {
case ChatMode.normal.rawValue:
HStack() {
Rectangle()
.fill(ColorSet.ChatDate.color)
......@@ -152,6 +221,8 @@ struct AlertChatMessage: View {
.fill(ColorSet.ChatDate.color)
.frame(width: 20, height: 1)
}
default:
EmptyView()
}
}
}
......
......@@ -46,7 +46,6 @@ enum Corners{
case br
}
fileprivate struct CustomCornerRadiusModifier: ViewModifier {
let cornerRadius: CGFloat
let corners: [Corners]
......
......@@ -9,14 +9,18 @@ import SwiftUI
struct MyChatContentView: View {
var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil
var body: some View {
HStack {
Spacer()
VStack(alignment: .trailing, spacing: 6) {
Group {
if let msg = message.message {
if msg.contains("https://") {
ChatUrlImageView(message: message)
if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded)
} else {
Text(msg)
.font(FontStyle.DefaultText.font)
......@@ -36,7 +40,7 @@ struct MyChatContentView: View {
}
HStack(spacing: 5){
//既読マーク
//MARK: - 既読マーク
Text(DateTextLib.ISO86012FormatText(message.time, format: "yyyy/MM/dd HH:mm", errFormat: ""))
.padding(.trailing, 8)
......
......@@ -9,6 +9,8 @@ import SwiftUI
struct OtherChatContentView: View {
var message : ChatMessage
var onMediaLoaded: (() -> Void)? = nil
var body: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
......@@ -20,8 +22,10 @@ struct OtherChatContentView: View {
VStack(alignment: .leading, spacing: 10) {
Group {
if let msg = message.message {
if msg.contains("https://") {
ChatUrlImageView(message: message)
if msg.contains(".jpg") || msg.contains(".png") {
ChatUrlImageView(imageUrl: msg, onLoad: onMediaLoaded)
} else if msg.contains(".mp4") || msg.contains(".mov") {
ChatUrlVideoView(imageUrl: msg, onLoad: onMediaLoaded)
} else {
Text(msg)
.font(FontStyle.DefaultText.font)
......
import SwiftUI
import Combine
import Speech
enum MediaInputType {
case none
case camera
case photoLibrary
case fileImport
}
class ChatInputViewModel: ObservableObject {
@Published var failedUploadImage: ReqUploadImage? = nil
@Published var isRetryDialogPresented = false
@Published var isSignalrRestart = false
@Published var isRecording = false
@Published var isChatAlert = false
@Published var inputText = ""
@Published var textViewHeight: CGFloat = 40
@Published var tempId: String = ""
@Published var mediaInputType: MediaInputType = .none
@Published var isImagePickerPresented = false
@Published var isFileImporterPresented = false
@Published var isKeyboardFocused: Bool = false
@Published var isUploadingDialogPresented: Bool = false
@Published var isFocus: Double = 0.0
private let sessionUploadImage = SessionUploadImage()
//MARK: - メディア入力
func handleMediaInput(type: MediaInputType) {
self.mediaInputType = type
if type == .camera || type == .photoLibrary {
self.isImagePickerPresented = true
} else if type == .fileImport {
self.isFileImporterPresented = true
}
}
// MARK: - Send Chat Image
func sendChatImage(_ uploadImage: ReqUploadImage) {
Task {
do {
isUploadingDialogPresented = true
let response = try await sessionUploadImage.requestUploadImage(uploadImage)
print(debug: "Upload success: \(response)")
let serverSession = ServerSession()
_ = serverSession.fromJSON(resultData: response, resltType: ResLogin.self)
sessionUploadImage.progress = 0.0 // 完了後リセット
isUploadingDialogPresented = false
} catch {
print(debug: "Upload failed: \(error)")
sessionUploadImage.progress = 0.0 // エラー時もリセット
isUploadingDialogPresented = false
failedUploadImage = uploadImage
isRetryDialogPresented = true
}
}
}
// MARK: - Send Chat Message
func sendChatMessage() {
guard !SharingData.message.sendInf else { return }
isRecording = false
SignalR().chatMessage(message: inputText, completion: responseChatMessage)
SharingData.message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.restartChatMessage()
}
}
// MARK: - Chat Response
private func responseChatMessage(error: Error?) {
SharingData.message.sendInf = false
if let error = error {
print("Chat error: \(error)")
isChatAlert = true
} else {
isKeyboardFocused = false
inputText = ""
}
}
// MARK: - Restart Chat if No Response
private func restartChatMessage() {
if SharingData.message.sendInf {
SignalR().stopConnection()
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.isSignalrRestart = true
}
}
}
//MARK: - テキスト高さ
func recalculateHeight() {
let textView = UITextView()
textView.text = inputText
textView.font = UIFont.preferredFont(forTextStyle: .body)
let fixedWidth = UIScreen.main.bounds.width - 40 // 適宜調整
let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
textViewHeight = max(newSize.height, 40) // 最低高さを40に設定
}
// MARK: - ローカル画像を一時保存してURLを取得
func saveImageToTemporaryDirectory(_ image: UIImage) -> String? {
let fileName = UUID().uuidString + ".jpg"
let tempDir = FileManager.default.temporaryDirectory
let fileURL = tempDir.appendingPathComponent(fileName)
guard let data = image.jpegData(compressionQuality: 0.8) else {
print("Image JPEG conversion failed")
return nil
}
do {
try data.write(to: fileURL)
return fileURL.absoluteString
} catch {
print("Image JPEG conversion failed: \(error.localizedDescription)")
return nil
}
}
//MARK: - 送信前に送信画像をテンポラリに入れる
func sendImageToTemporary(_ image: UIImage) {
if let localURL = saveImageToTemporaryDirectory(image),
let jpegData = image.jpegData(compressionQuality: 1.0) {
tempId = UUID().uuidString
let viewer = Viewer(
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
id: "",
name: ""
)
let tempMessage = ChatMessage(
shipId: Preferences.shipId,
messageId: tempId,
type: 2,
time: DateTextLib.Date2ISO8601Text(Date()),
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
mode: SharingData.message.mode ? 1 : 0,
message: localURL,
stampId: 0,
viewer: [viewer]
)
SharingData.message.messages.append(tempMessage)
let uploadImage = ReqUploadImage(
shipId: Preferences.shipId,
messageId: tempId,
location: 2,
from: Preferences.UserName,
fromId: String(SharingData.my.id),
files: jpegData
)
sendChatImage(uploadImage)
}
}
}
import SwiftUI
import AVFoundation
@MainActor
class ChatViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
@Published var isMediaPrefetched: Bool = false
func loadMessages() {
messages = SharingData.message.messages
prefetchMedia()
}
private func prefetchMedia() {
let imageURLs = messages.filter { $0.type == 2 && $0.message != nil }.compactMap { $0.message }.compactMap { URL(string: $0) }
let videoURLs = messages.filter { $0.type == 3 && $0.message != nil }.compactMap { $0.message }.compactMap { URL(string: $0) }
Task {
await withTaskGroup(of: Void.self) { group in
for url in imageURLs {
group.addTask {
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
_ = try? await URLSession.shared.data(for: request)
}
}
for url in videoURLs {
group.addTask {
let asset = AVURLAsset(url: url)
do {
_ = try await asset.load(.isPlayable)
} catch {
print(debug: "Video loading failure: \(url)")
}
}
}
}
isMediaPrefetched = true
}
}
}
......@@ -14,6 +14,7 @@ class LoginViewModel: ObservableObject{
struct ContentView: View {
@StateObject private var loginViewModel = LoginViewModel()
@EnvironmentObject private var sceneDelegate: SceneDelegate
@StateObject private var chatViewModel = ChatViewModel()
let selectedTabModel = SelectedTabModel()
......@@ -38,6 +39,7 @@ struct ContentView: View {
}, content: {
LoginView(isLogin: $loginViewModel.isLogin)
.environmentObject(selectedTabModel)
.environmentObject(chatViewModel)
})
}
}
......
......@@ -908,9 +908,9 @@ class LocationCalculation{
distance = rtn.xte * -1
}
if let dist = distance {
// if let dist = distance {
// print(debug: "checkPolyline \(dist)")
}
// }
return distance
}
......
......@@ -37,6 +37,7 @@ struct LoginView: View {
@ObservedObject var scannerViewModel = ScannerViewModel()
@EnvironmentObject var locationViewModel: LocationViewModel
@EnvironmentObject var selectedTabModel: SelectedTabModel
@EnvironmentObject var chatViewModel: ChatViewModel
@Binding var isLogin: Bool
@State var isQrRead: Bool = false
@State var viewMode: LoginViewMode = .SelectType
......@@ -153,9 +154,7 @@ struct LoginView: View {
}
}
/**
* QRコードでのログイン
*/
//MARK: - QRコードでのログイン
func LoginCheckQR() -> () {
isProgressView = true
loginViewParam.shipId = Preferences.Id
......@@ -167,9 +166,7 @@ struct LoginView: View {
isProgressView = false
}
/**
* Autoログイン
*/
//MARK: - Autoログイン
func LoginCheck() -> () {
let lastUnixTime = Preferences.lastLoginDate_Int64 ?? 0
let lastDate = DateTextLib.UnixTime2Date(lastUnixTime)
......@@ -211,6 +208,7 @@ struct LoginView: View {
isLogin = true
let message = GetMessage()
message.chatViewModel = chatViewModel
message.start()
timer = Timer.scheduledTimer(withTimeInterval: TimerInterval, repeats: true) { _ in
......@@ -269,4 +267,5 @@ fileprivate struct Triangle: Shape{
#Preview {
LoginView(isLogin: .constant(false))
.environmentObject(SelectedTabModel())
.environmentObject(ChatViewModel())
}
......@@ -63,7 +63,7 @@ struct MapRepresentable: UIViewControllerRepresentable {
//MARK: - 自船を画面中央に表示
if location.focusOwnShip {
mapVC.updateCamera(location: location.location, zoomlevel: 10.0)
mapVC.updateCamera(location: location.location, zoomlevel: 5.0)
}
if let mylocation = location.location {
......@@ -86,7 +86,9 @@ struct MapRepresentable: UIViewControllerRepresentable {
if SharingData.nga.editType == EditNgaType.registered {
mapVC.updateEditArea(remove: true)
SharingData.nga.editType = EditNgaType.nonEdit
DispatchQueue.main.async {
SharingData.nga.editType = EditNgaType.nonEdit
}
}
if SharingData.nga.editType == EditNgaType.deletePoint {
......
import SwiftUI
struct EcaListMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
EcaListView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
EcaListMainView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct EcaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ecaData = SharingData.eca
@Environment(\ .colorScheme) var colorScheme
@State var isDeleteAlert: Bool = false
let deleteEcaArea = DeleteEcaArea()
var eca: RegisteredEca
var body: some View {
Menu {
Text(eca.name)
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
taskViewModel.viewMode = .EcaSetting
} label: {
Text("Edit Notice Setting")
}
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
isDeleteAlert = true
} label: {
Text("Delete ECA Task")
}
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(eca.isRunning == true ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(eca.isRunning)
.alert("Delete", isPresented: $isDeleteAlert) {
Button("Yes") {
if let ecaArea = taskViewModel.edittingEcaArea {
var newData = ecaArea
newData.isEnable = false
newData.status = EcaState.cancel
ecaData.editEcaArea(key: ecaArea.areaId, value: newData, type: EcaOperation.Delete)
deleteEcaArea.start(ecaId: ecaArea.areaId)
}
taskViewModel.edittingEcaArea = nil
}
Button("No"){}
} message: {
Text("Have you finished " + taskViewModel.ecaName + " fuel switching?")
}
}
}
import SwiftUI
struct EcaSettingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
if let edittingEcaArea = taskViewModel.edittingEcaArea {
EcaSettingView(taskViewModel: taskViewModel, edittingEca: edittingEcaArea)
}
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
EcaSettingMainView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct FuelSwitchingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.FuelSwitching.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
//ECAリスト
ScrollViewReader { proxy in
ScrollView(.vertical){
FuelSwitchingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
FuelSwitchingMainView(taskViewModel: TaskViewModel())
}
......@@ -4,7 +4,6 @@
//
// Created by Mamoru Sugita on 2023/10/18.
//
import SwiftUI
struct FuelSwitchingView: View {
......@@ -96,59 +95,6 @@ struct FuelSwitchingView: View {
}
}
struct EcaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ecaData = SharingData.eca
@Environment(\ .colorScheme) var colorScheme
@State var isDeleteAlert: Bool = false
let deleteEcaArea = DeleteEcaArea()
var eca: RegisteredEca
var body: some View {
Menu {
Text(eca.name)
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
taskViewModel.viewMode = .EcaSetting
} label: {
Text("Edit Notice Setting")
}
Button{
taskViewModel.edittingEcaArea = eca
taskViewModel.ecaName = eca.name
isDeleteAlert = true
} label: {
Text("Delete ECA Task")
}
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(eca.isRunning == true ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(eca.isRunning)
.alert("Delete", isPresented: $isDeleteAlert) {
Button("Yes") {
if let ecaArea = taskViewModel.edittingEcaArea {
var newData = ecaArea
newData.isEnable = false
newData.status = EcaState.cancel
ecaData.editEcaArea(key: ecaArea.areaId, value: newData, type: EcaOperation.Delete)
deleteEcaArea.start(ecaId: ecaArea.areaId)
}
taskViewModel.edittingEcaArea = nil
}
Button("No"){}
} message: {
Text("Have you finished " + taskViewModel.ecaName + " fuel switching?")
}
}
}
#Preview {
FuelSwitchingView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct MenuMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
VStack(spacing: 0) {
headerView
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
VStack(spacing: 16) {
Color.clear.frame(height: 1).id("top")
MenuButton(
title: TaskViewMode.FuelSwitching.title,
isEnabled: SharingData.my.isFuelSwitchTask,
action: { taskViewModel.viewMode = .FuelSwitching }
)
MenuButton(
title: TaskViewMode.NgaNotification.title,
isEnabled: SharingData.my.isNga,
action: { taskViewModel.viewMode = .NgaNotification }
)
}
.padding(.top, 16)
}
.onChange(of: taskViewModel.viewMode) { _ in
withAnimation {
proxy.scrollTo("top", anchor: .top)
}
}
}
Spacer().frame(height: 55)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
}
private var headerView: some View {
HStack {
Spacer()
Text(TaskViewMode.MenuList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
}
}
}
struct MenuButton: View {
let title: String
let isEnabled: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
Text(title)
.font(FontStyle.DefaultText.font)
.padding()
.frame(maxWidth: .infinity, minHeight: 42)
}
.foregroundColor(ColorSet.ButtonText.color)
.background(isEnabled ? ColorSet.PrimaryActiveIcon.color : ColorSet.SecondaryDisable.color)
.cornerRadius(30)
.disabled(!isEnabled)
.frame(width: 153)
}
}
#Preview {
MenuMainView(taskViewModel: TaskViewModel())
}
......@@ -4,7 +4,6 @@
//
// Created by Mamoru Sugita on 2023/10/17.
//
import SwiftUI
enum TaskViewMode {
......@@ -49,17 +48,17 @@ struct MenuTaskView: View {
switch taskViewModel.viewMode {
case .MenuList:
MenuMain(taskViewModel: taskViewModel)
MenuMainView(taskViewModel: taskViewModel)
case .FuelSwitching:
FuelSwitchingMain(taskViewModel: taskViewModel)
FuelSwitchingMainView(taskViewModel: taskViewModel)
case .EcaList:
EcaListMain(taskViewModel: taskViewModel)
EcaListMainView(taskViewModel: taskViewModel)
case .EcaSetting:
EcaSettingMain(taskViewModel: taskViewModel)
EcaSettingMainView(taskViewModel: taskViewModel)
case .NgaNotification:
NgaNotificationMain(taskViewModel: taskViewModel)
NgaNotificationMainView(taskViewModel: taskViewModel)
case .NgaSetting:
NgaSettingMain(taskViewModel: taskViewModel)
NgaSettingMainView(taskViewModel: taskViewModel)
}
}
.onAppear{
......@@ -130,348 +129,6 @@ struct MenuTaskView: View {
}
}
/**
* Menu画面
*/
struct MenuMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Spacer()
Text(TaskViewMode.MenuList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Text(TaskViewMode.FuelSwitching.title)
.font(FontStyle.DefaultText.font)
.padding()
})
.frame(width: 153, height: 42)
.foregroundColor(ColorSet.ButtonText.color)
.background(!SharingData.my.isFuelSwitchTask ? ColorSet.SecondaryDisable.color : ColorSet.PrimaryActiveIcon.color)
.cornerRadius(30)
.disabled(!SharingData.my.isFuelSwitchTask)
Button(action: {
taskViewModel.viewMode = .NgaNotification
}, label: {
Text(TaskViewMode.NgaNotification.title)
.font(FontStyle.DefaultText.font)
.padding()
})
.frame(width: 153, height: 42)
.foregroundColor(ColorSet.ButtonText.color)
.background(!SharingData.my.isNga ? ColorSet.SecondaryDisable.color : ColorSet.PrimaryActiveIcon.color)
.cornerRadius(30)
.disabled(!SharingData.my.isNga)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
/**
* Fuel Switching画面
*/
struct FuelSwitchingMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.FuelSwitching.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
//ECAリスト
ScrollViewReader { proxy in
ScrollView(.vertical){
FuelSwitchingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
/**
* ECA List画面
*/
struct EcaListMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaList.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
EcaListView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
/**
* ECA Setting画面
*/
struct EcaSettingMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .FuelSwitching
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.EcaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
if let edittingEcaArea = taskViewModel.edittingEcaArea {
EcaSettingView(taskViewModel: taskViewModel, edittingEca: edittingEcaArea)
}
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
/**
* NGA Notification画面
*/
struct NgaNotificationMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaNotification.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaNotificationView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
/**
* NGA設定画面
*/
struct NgaSettingMain: View {
@ObservedObject var taskViewModel = TaskViewModel()
@ObservedObject var nga = SharingData.nga
@ObservedObject var map = SharingData.map
var body: some View {
//タイトルエリア
HStack{
Button(action: {
if taskViewModel.viewMode == .NgaSetting && nga.editType == EditNgaType.movePoint {
nga.editNga = nga.moveNga
nga.editType = EditNgaType.addPoint
map.isMapFree = true
} else {
taskViewModel.viewMode = .NgaNotification
nga.editType = EditNgaType.registered
}
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaSettingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
MenuTaskView()
}
#Preview {
MenuMain(taskViewModel: TaskViewModel())
}
#Preview {
FuelSwitchingMain(taskViewModel: TaskViewModel())
}
#Preview {
EcaListMain(taskViewModel: TaskViewModel())
}
#Preview {
EcaSettingMain(taskViewModel: TaskViewModel())
}
#Preview {
NgaNotificationMain(taskViewModel: TaskViewModel())
}
#Preview {
NgaSettingMain(taskViewModel: TaskViewModel())
}
import SwiftUI
struct NgaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ngaData = SharingData.nga
@Environment(\ .colorScheme) var colorScheme
@State var isDelete: Bool = false
var nga: RegisteredNga
var body: some View {
Menu {
Text(nga.name)
Button{
ngaData.editNga = nga
taskViewModel.viewMode = .NgaSetting
ngaData.editType = EditNgaType.nonEdit
} label: {
Text("Edit NGA Setting")
}
//削除
Button{
ngaData.editNga = nga
isDelete = true
} label: {
Text("Delete NGA Task")
}.disabled(nga.isLock)
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(nga.isRunning ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(nga.isRunning)
.alert("Delete", isPresented: $isDelete) {
Button("Yes") {
if let newData = ngaData.editNga {
ngaData.editNgaArea(value: newData, type: NgaOperation.Delete)
}
}
Button("No"){}
} message: {
if let editNga = ngaData.editNga {
Text("Delete " + editNga.name + " ?")
}
}
}
}
import SwiftUI
struct NgaNotificationMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
var body: some View {
//タイトルエリア
HStack{
Button(action: {
taskViewModel.viewMode = .MenuList
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaNotification.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaNotificationView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
NgaNotificationMainView(taskViewModel: TaskViewModel())
}
......@@ -4,7 +4,6 @@
//
// Created by Mamoru Sugita on 2023/10/18.
//
import SwiftUI
struct NgaNotificationView: View {
......@@ -87,54 +86,6 @@ struct NgaNotificationView: View {
}
}
struct NgaMenuView: View {
@ObservedObject var taskViewModel: TaskViewModel
@ObservedObject var ngaData = SharingData.nga
@Environment(\ .colorScheme) var colorScheme
@State var isDelete: Bool = false
var nga: RegisteredNga
var body: some View {
Menu {
Text(nga.name)
Button{
ngaData.editNga = nga
taskViewModel.viewMode = .NgaSetting
ngaData.editType = EditNgaType.nonEdit
} label: {
Text("Edit NGA Setting")
}
//削除
Button{
ngaData.editNga = nga
isDelete = true
} label: {
Text("Delete NGA Task")
}.disabled(nga.isLock)
} label: {
Image(systemName: "ellipsis")
.frame(width: 22, height: 22)
.opacity(nga.isRunning ? 0.2 : 1.0)
.foregroundColor(colorScheme == .light ? .black : .white)
}
.disabled(nga.isRunning)
.alert("Delete", isPresented: $isDelete) {
Button("Yes") {
if let newData = ngaData.editNga {
ngaData.editNgaArea(value: newData, type: NgaOperation.Delete)
}
}
Button("No"){}
} message: {
if let editNga = ngaData.editNga {
Text("Delete " + editNga.name + " ?")
}
}
}
}
#Preview {
NgaNotificationView(taskViewModel: TaskViewModel())
}
import SwiftUI
struct NgaSettingMainView: View {
@ObservedObject var taskViewModel = TaskViewModel()
@ObservedObject var nga = SharingData.nga
@ObservedObject var map = SharingData.map
var body: some View {
//タイトルエリア
HStack{
Button(action: {
if taskViewModel.viewMode == .NgaSetting && nga.editType == EditNgaType.movePoint {
nga.editNga = nga.moveNga
nga.editType = EditNgaType.addPoint
map.isMapFree = true
} else {
taskViewModel.viewMode = .NgaNotification
nga.editType = EditNgaType.registered
}
}, label: {
Image("ink_02")
})
.frame(width: 48, height: 48)
Spacer()
Text(TaskViewMode.NgaSetting.title)
.font(FontStyle.TitleL.font)
.frame(height: 20)
.padding(.vertical, 14)
Spacer()
Button(action: {
}, label: {
})
.frame(width: 48, height: 48)
}
.padding(EdgeInsets(top: 10, leading: 8, bottom: 13, trailing: 17))
Divider()
.background(ColorSet.LineColor03.color)
ScrollViewReader { proxy in
ScrollView(.vertical){
NgaSettingView(taskViewModel: taskViewModel)
}
.onChange(of: taskViewModel.viewMode){ newViewMode in
proxy.scrollTo(0, anchor: .top)
}
}
Spacer()
.frame(height: 55)
}
}
#Preview {
NgaSettingMainView(taskViewModel: TaskViewModel())
}
......@@ -84,6 +84,7 @@ var connection: HubConnection?
class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, MSInstallationLifecycleDelegate {
@ObservedObject var msg = SharingData.message
private var hubConnectionDelegate: HubConnectionDelegate?
private var isReconnecting = false // 再接続中かどうかを管理するフラグ
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print(debug: "called")
......@@ -105,7 +106,7 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
application.registerForRemoteNotifications()
}
//ユニークな値の生成
//MARK: - ユニークな値の生成
if Preferences.DeviceId == "" {
Preferences.DeviceId = UUID().uuidString
}
......@@ -117,32 +118,37 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
.withAutoReconnect()
// .withLogging(minLogLevel: .error)
.withLogging(minLogLevel: .debug)
.withHubConnectionOptions(configureHubConnectionOptions: {options in options.keepAliveInterval = 30})
.withHubConnectionOptions(configureHubConnectionOptions: {options in options.keepAliveInterval = 20})
.build()
if let r_connection = connection {
//Chat
r_connection.on(method: "ChatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
setupSignalRHandlers(connection: r_connection)
r_connection.start()
}
//Photo / Image
r_connection.on(method: "chatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
return true
}
r_connection.on(method: "AckMessage", callback: { (message: ResAckMessage) in
self.handleAckMessage(message: message)
})
private func setupSignalRHandlers(connection: HubConnection) {
// Chatメッセージ処理
connection.on(method: "ChatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
r_connection.on(method: "ChatMode", callback: { (message: ResChatMode) in
self.handleChatMode(message: message)
})
// Photo / Image
connection.on(method: "chatMessage", callback: { (message: ResChatMessage) in
self.handleChatMessage(message: message)
})
r_connection.start()
}
// Ackメッセージ処理
connection.on(method: "AckMessage", callback: { (message: ResAckMessage) in
self.handleAckMessage(message: message)
})
return true
// Chatモード処理
connection.on(method: "ChatMode", callback: { (message: ResChatMode) in
self.handleChatMode(message: message)
})
}
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
......@@ -162,7 +168,7 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
completionHandler(.noData)
}
// Push通知を受信した時(サイレントプッシュ)
//MARK: - Push通知を受信した時(サイレントプッシュ)
func notificationHub(_ notificationHub: MSNotificationHub, didReceivePushNotification notification: MSNotificationHubMessage) {
print(debug: "called")
// let title = notification.title ?? ""
......@@ -188,9 +194,36 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
}
private func handleChatMessage(message: ResChatMessage) {
print(debug: "called")
let ownMsg = ChatMessage(shipId: message.shipId, messageId: message.messageId, type: message.type, time: message.time, location: message.location, from: message.from, fromId: message.fromId, mode: message.mode, message: message.message, stampId: message.stampId, viewer: [])
self.msg.messages.append(ownMsg)
DispatchQueue.main.async {
print(debug: "called")
let newMsg = ChatMessage(
shipId: message.shipId,
messageId: message.messageId,
type: message.type,
time: message.time,
location: message.location,
from: message.from,
fromId: message.fromId,
mode: message.mode,
message: message.message,
stampId: message.stampId,
viewer: []
)
if let index = self.msg.messages.lastIndex(where: { $0.messageId.lowercased() == newMsg.messageId.lowercased() }) {
var updatedMsg = newMsg
let existingMsg = self.msg.messages[index]
if existingMsg.message!.hasPrefix("file://") {
updatedMsg.message = existingMsg.message
}
self.msg.messages[index] = updatedMsg
} else {
self.msg.messages.append(newMsg)
}
}
}
private func handleAckMessage(message: ResAckMessage) {
......@@ -219,7 +252,20 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
self.msg.messages.append(ownMsg)
}
//アプリ終了時
//MARK: - Signal-R再接続
func reconnect() {
guard !isReconnecting else { return } // 再接続中なら何もしない
isReconnecting = true
print(debug: "Attempting to reconnect...")
DispatchQueue.global().asyncAfter(deadline: .now() + 5.0) { [weak self] in
guard let self = self else { return }
connection?.start()
self.isReconnecting = false
}
}
//MARK: - アプリ終了時
func applicationWillTerminate(_ aNotification: UIApplication) {
if let r_connection = connection {
r_connection.stop()
......@@ -232,12 +278,14 @@ class AppDelegate: NSObject, UIApplicationDelegate ,MSNotificationHubDelegate, M
func connectionDidFailToOpen(error: Error) {
print(debug: "connectionDidFailToOpen:\(error)")
reconnect()
}
func connectionDidClose(error: Error?) {
if let err = error {
print(debug: "connectionDidClose:\(err)")
}
reconnect()
}
func connectionWillReconnect(error: Error) {
......@@ -253,17 +301,15 @@ class SignalR: NSObject {
@ObservedObject var msg = SharingData.message
func chatMessage(message: String, completion: @escaping (_ error: Error?) -> Void) {
print(debug: "called")
var request = ReqMessage(shipId: Preferences.shipId, messageId: UUID().uuidString.lowercased())
request.type = 0 //0:テキスト, 1:スタンプ
request.time = DateTextLib.Date2ISO8601Text(Date())
request.location = 2 //1:Shore , 2:Ship
request.from = Preferences.UserName //投稿者名
request.fromId = String(SharingData.my.id) //ユーザーID
if SharingData.message.mode {
request.mode = 1
} else {
request.mode = 0
}
request.mode = SharingData.message.mode ? 1 : 0
request.message = message
request.stampId = 0 //スタンプ番号 0:Fire~
......@@ -299,11 +345,15 @@ class SignalR: NSObject {
let test = connection.debugDescription
print(debug: "Test: Chat Message \(test)")
connection!.stop()
guard let connection = connection else { return }
connection.stop()
}
func startConnection() {
connection!.start()
guard let connection = connection else { return }
if connection.connectionId == nil {
connection.start()
}
}
}
......@@ -355,6 +405,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
let arrCategory = arrAPS["category"] as? String ?? ""
switch arrCategory {
case "chat":
print(debug: "chat")
guard let arrAlert = arrAPS["alert"] as? [String: Any] else {
completionHandler([[.banner, .badge, .sound]])
return
......
......@@ -20,29 +20,25 @@ class SessionUploadImage : ObservableObject {
func cancelUpload() {
uploadTask?.cancel()
uploadTask = nil
isUploading = false
progress = 0.0
}
/**
* メッセージ
*/
//MARK: - 画像のアップロード
func requestUploadImage(_ uploadImage: ReqUploadImage) async throws -> Data {
print(debug: "calld")
print(debug: "called")
guard !Calling else {
throw APIError.busy
}
Calling = true
defer { Calling = false }
// リクエストURLの組み立て
guard let req_url = URL(string: HttpRequestType.UploadImage.rawValue) else {
throw APIError.invalidURL
}
let imageFileName = "itemp.jpg"
let boundary = "----------\(UUID().uuidString)"
var httpBody = Data()
func appendFormField(name: String, value: String) {
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
......@@ -57,7 +53,7 @@ class SessionUploadImage : ObservableObject {
appendFormField(name: "FromId", value: uploadImage.fromId)
httpBody.append("--\(boundary)\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"files\"; filename=\"\(imageFileName)\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Disposition: form-data; name=\"files\"; filename=\"itemp.jpg\"\r\n".data(using: .utf8)!)
httpBody.append("Content-Type: image/jpeg\r\n\r\n".data(using: .utf8)!)
httpBody.append(uploadImage.files)
httpBody.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
......@@ -65,22 +61,35 @@ class SessionUploadImage : ObservableObject {
var request = URLRequest(url: req_url)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
let session = URLSession(configuration: .default, delegate: UploadProgressDelegate(parent: self), delegateQueue: nil)
isUploading = true
uploadTask = session.uploadTask(with: request, from: httpBody)
uploadTask?.resume()
let (data, response) = try await session.upload(for: request, from: httpBody)
isUploading = false
progress = 0.0
return try await withCheckedThrowingContinuation { continuation in
self.uploadTask = session.uploadTask(with: request, from: httpBody) { data, response, error in
DispatchQueue.main.async {
self.isUploading = false
self.progress = 0.0
}
if let error = error {
continuation.resume(throwing: error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode),
let data = data else {
continuation.resume(throwing: APIError.serverError)
return
}
continuation.resume(returning: data)
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
throw APIError.serverError
self.uploadTask?.resume()
}
return data
}
func postFormAsync(boundary: String, url: URL, body: Data) async throws -> Data {
......@@ -105,8 +114,11 @@ class SessionUploadImage : ObservableObject {
self.parent = parent
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64,
totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
func urlSession(_ session: URLSession, task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64) {
guard totalBytesExpectedToSend > 0 else { return }
DispatchQueue.main.async {
self.parent?.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
}
......
import SwiftUI
struct CustomTabBar: View {
@EnvironmentObject private var selectedTabModel: SelectedTabModel
@StateObject private var viewModel = CustomTabBarViewModel()
@Environment(\.openURL) var openURL
@ObservedObject var my = SharingData.my
@ObservedObject var message = SharingData.message
@ObservedObject var location = SharingData.location
@ObservedObject var pushHistory = SharingData.pushHistory
var body: some View {
VStack(spacing: 0){
Divider()
HStack(spacing: 0){
ForEach(Tab.allCases, id: \.rawValue) { tab in
VStack{
ZStack(alignment: .bottomTrailing) {
if !SharingData.my.isCommunication && tab == Tab.chat {
Image("tab_chat_Invalid")
.onTapGesture {
viewModel.handleTabSelection(
selectedTabModel: selectedTabModel,
tab: tab,
message: message,
history: pushHistory,
location: location,
)
}
} else {
Image(selectedTabModel.activeTab == tab ? tab.rawValue + "_selected" : tab.rawValue)
.onTapGesture {
viewModel.handleTabSelection(
selectedTabModel: selectedTabModel,
tab: tab,
message: message,
history: pushHistory,
location: location,
)
}
}
if tab == Tab.chat {
NotificationBadge(viewModel: viewModel.chatBadgeViewModel, font: FontStyle.VersionText.font)
}
if tab == Tab.alert {
NotificationBadge(viewModel: viewModel.alertBadgeViewModel, font: FontStyle.VersionText.font)
}
}
Text(tab.title)
.font(.caption)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
.frame(height: 50)
}
.background(ColorSet.BottomNav.color)
.modifier(ChangeModeAlertModifier(
isPresented: $selectedTabModel.isShowChangeEmrMode,
currentMode: message.mode,
onConfirm: {
var chatMode = message.mode
chatMode.toggle()
let signalRService = SignalR()
signalRService.chatMode(mode: chatMode, completion: responseChatMode)
message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
restartChatMode()
}
}))
.modifier(LocationAlertModifier(isPresented: $selectedTabModel.isLocationAlert))
.modifier(UpdateAlertModifier(isPresented: $my.isUpDate, appStoreURL: URL(string: HttpRequestType.AppStore.rawValue)!))
.modifier(WarningModeAlertModifier(isPresented: $viewModel.isModeAlert, isSignalrRestert: $viewModel.isSignalrRestart,
onConfirm: {
let signalRService = SignalR()
signalRService.startConnection()
viewModel.isSignalrRestart = false
}))
//MARK: - チャットTab上の既読マーク
.onChange(of: message.viewCnt) { newValue in
viewModel.chatBadgeViewModel.updateViewCnt(to: newValue)
}
//MARK: - アラートTab上の既読マーク
.onChange(of: pushHistory.viewCnt) { newValue in
viewModel.alertBadgeViewModel.updateViewCnt(to: newValue)
}
}
//MARK: - Warninngモードレスポンス
func responseChatMode(error: Error?) {
print(debug: "responseChatMode")
message.sendInf = false
if let e = error {
print(debug: "Error chat:\(e)")
viewModel.isModeAlert = true
} else {
message.changeMode()
}
}
//MARK: - モード変更レスポンスが無かった場合
func restartChatMode() {
if message.sendInf {
let signalRService = SignalR()
signalRService.stopConnection()
Thread.sleep(forTimeInterval: 1.0)
viewModel.isSignalrRestart = true
}
}
}
import SwiftUI
import Foundation
import UIKit
import Combine
class CustomTabBarViewModel: ObservableObject {
@Published var chatBadgeViewModel = NotificationBadgeViewModel()
@Published var alertBadgeViewModel = NotificationBadgeViewModel()
@Published var isMenuTaskViewVisible = false
@Published var isSignalrRestart = false
@Published var isModeAlert = false
private let messageService = GetMessage()
func handleTabSelection(
selectedTabModel: SelectedTabModel,
tab: Tab,
message: SharingData.Message,
history: SharingData.PushHistory,
location: SharingData.Location
) {
selectedTabModel.activeTab = tab
switch tab {
case .map:
selectedTabModel.isPoppver.toggle()
location.focusOwnShip = true
if UIDevice.current.userInterfaceIdiom == .pad {
isMenuTaskViewVisible.toggle()
updateMapMenuVisibility()
}
case .chat:
messageService.start()
messageService.readNotification()
messageService.checkUnreadMessages()
message.viewCnt = 0
chatBadgeViewModel.resetBadge()
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
case .alert:
history.viewCnt = 0
alertBadgeViewModel.resetBadge()
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
default:
resetPopoverAndMenu(selectedTabModel: selectedTabModel)
}
}
private func resetPopoverAndMenu(selectedTabModel: SelectedTabModel) {
selectedTabModel.isPoppver = false
if UIDevice.current.userInterfaceIdiom == .pad {
isMenuTaskViewVisible = false
updateMapMenuVisibility()
}
}
//MARK: - マップメニュー表示の切り替え
private func updateMapMenuVisibility() {
let screenWidth = UIScreen.main.bounds.width
let screenHeight = UIScreen.main.bounds.height
let menuWidth = min(400, screenWidth * 0.3)
let menuHeight = screenHeight * 0.8
if isMenuTaskViewVisible {
MenuWindowManager.shared.showMenuView(
MenuTaskView()
.frame(width: menuWidth, height: menuHeight)
.background(ColorSet.BackgroundSecondary.color)
.cornerRadius(15),
frame: CGRect(x: 5, y: screenHeight * 0.12, width: menuWidth, height: menuHeight)
)
} else {
MenuWindowManager.shared.hideMenuView()
}
}
}
class MenuWindowManager {
static let shared = MenuWindowManager()
private var window: UIWindow?
func showMenuView<Content: View>(_ content: Content, frame: CGRect) {
guard window == nil else { return }
let hostingController = UIHostingController(rootView: content)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
var rFrame = frame
rFrame.size.height -= 20
let newWindow = UIWindow(windowScene: windowScene)
newWindow.rootViewController = hostingController
newWindow.windowLevel = .alert + 1
newWindow.backgroundColor = UIColor.clear
newWindow.makeKeyAndVisible()
newWindow.alpha = 0.8
newWindow.frame = rFrame
newWindow.layer.cornerRadius = 15
newWindow.layer.masksToBounds = true
self.window = newWindow
}
}
func hideMenuView() {
guard let window = window else { return }
window.isHidden = true
window.rootViewController = nil
self.window = nil
}
}
//
// MainTabView.swift
// forShip
//
// Created by Mamoru Sugita on 2023/10/13.
//
import SwiftUI
enum Tab: String, CaseIterable{
case map = "tab_map"
case chat = "tab_chat"
case alert = "tab_notification"
case menu = "tab_menu"
var title: String{
switch self {
case .map: return "Map"
case .chat: return "Chat"
case .alert: return "Alert"
case .menu: return "Menu"
}
}
}
struct MainTabView: View {
@EnvironmentObject var selectedTabModel: SelectedTabModel
@EnvironmentObject private var sceneDelegate: SceneDelegate
@ObservedObject var location = SharingData.location
@State var isSignout = false
var isTabShow: Bool {
sceneDelegate.tabWindow != nil
}
var isTaskSel: Bool {
selectedTabModel.activeTab == .map
}
var body: some View {
Group {
if UIDevice.current.userInterfaceIdiom == .phone {
phoneView
} else {
padView
}
}
.onAppear {
SharingData.location.focusOwnShip = true
configureTabBarAppearance()
}
}
private var phoneView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
mapTab
if SharingData.my.isCommunication {
ChatView().tag(Tab.chat)
}
NotificationView().tag(Tab.alert)
MenuView(isSignout: $isSignout).tag(Tab.menu)
}
.sheet(isPresented: .constant(isTaskSel && isTabShow)) {
MenuTaskView()
.zIndex(0)
.presentationDragIndicator(.hidden)
.presentationDetents([.height(150), .medium, .fraction(0.99)])
.presentationCornerRadius(15)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
.presentationBackground(ColorSet.BackgroundSecondary.color)
.interactiveDismissDisabled()
}
}
}
private var padView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
mapTab.padding(.bottom, 50)
if SharingData.my.isCommunication {
ChatView().padding(.bottom, 50).tag(Tab.chat)
}
NotificationView().padding(.bottom, 50).tag(Tab.alert)
MenuView(isSignout: $isSignout).padding(.bottom, 50).tag(Tab.menu)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
@ViewBuilder
private var mapTab: some View {
ZStack {
MapRepresentable()
MapInformation()
}
.ignoresSafeArea(edges: .top)
.tag(Tab.map)
}
private func configureTabBarAppearance() {
let appearance = UITabBarAppearance()
appearance.backgroundColor = .clear
UITabBar.appearance().scrollEdgeAppearance = appearance
UITabBar.appearance().standardAppearance = appearance
}
}
class MenuWindowManager {
static let shared = MenuWindowManager()
private var window: UIWindow?
func showMenuView<Content: View>(_ content: Content, frame: CGRect) {
guard window == nil else { return }
let hostingController = UIHostingController(rootView: content)
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
var rFrame = frame
rFrame.size.height -= 20
let newWindow = UIWindow(windowScene: windowScene)
newWindow.rootViewController = hostingController
newWindow.windowLevel = .alert + 1
newWindow.backgroundColor = UIColor.clear
newWindow.makeKeyAndVisible()
newWindow.alpha = 0.8
newWindow.frame = rFrame
newWindow.layer.cornerRadius = 15
newWindow.layer.masksToBounds = true
self.window = newWindow
}
}
func hideMenuView() {
guard let window = window else { return }
window.isHidden = true
window.rootViewController = nil
window.removeFromSuperview()
self.window = nil
}
}
struct NotificationBadge: View {
let count: Int
let font: Font
var body: some View {
Group {
if count > 0 {
Ellipse()
.fill(Color.red)
.frame(width: 12, height: 12)
.overlay(
Text(count < 10 ? "\(count)" : "-")
.font(font)
.foregroundColor(.white)
)
}
}
}
}
struct CustomTabBar: View {
@EnvironmentObject private var selectedTabModel: SelectedTabModel
@State private var isModeAlert: Bool = false
@State var isLocationAlert = false
@Environment(\.openURL) var openURL
@ObservedObject var my = SharingData.my
@ObservedObject var message = SharingData.message
@ObservedObject var location = SharingData.location
@ObservedObject var pushHistory = SharingData.pushHistory
@State var isSignalrRestert = false
@State var isMenuTaskViewVisible = false
var body: some View {
VStack(spacing: 0){
Divider()
HStack(spacing: 0){
ForEach(Tab.allCases, id: \.rawValue) { tab in
VStack{
ZStack(alignment: .bottomTrailing) {
if !SharingData.my.isCommunication && tab == Tab.chat {
Image("tab_chat_Invalid")
.font(.title2)
.onTapGesture {
handleTabSelection(tab)
print("Image for \(tab.rawValue) tapped")
}
} else {
Image(selectedTabModel.activeTab == tab ? tab.rawValue + "_selected" : tab.rawValue)
.font(.title2)
.onTapGesture {
handleTabSelection(tab)
print("Image for \(tab.rawValue) tapped")
}
}
//MARK: - チャットTab上の既読マーク
if tab == Tab.chat {
NotificationBadge(count: message.viewCnt, font: FontStyle.VersionText.font)
.id(message.viewCnt)
}
if tab == Tab.alert {
NotificationBadge(count: pushHistory.viewCnt, font: FontStyle.VersionText.font)
.id(pushHistory.viewCnt)
}
}
Text(tab.title)
.font(.caption)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
}
}
.frame(height: 50)
}
.background(ColorSet.BottomNav.color)
.modifier(ChangeModeAlertModifier(
isPresented: $selectedTabModel.isShowChangeEmrMode,
currentMode: message.mode,
onConfirm: {
var chatMode = message.mode
chatMode.toggle()
let signalRService = SignalR()
signalRService.chatMode(mode: chatMode, completion: responseChatMode)
message.sendInf = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
restartChatMode()
}
}))
.modifier(LocationAlertModifier(isPresented: $selectedTabModel.isLocationAlert))
.modifier(UpdateAlertModifier(isPresented: $my.isUpDate, appStoreURL: URL(string: HttpRequestType.AppStore.rawValue)!))
.modifier(WarningModeAlertModifier(isPresented: $isModeAlert, isSignalrRestert: $isSignalrRestert,
onConfirm: {
let signalRService = SignalR()
signalRService.startConnection()
isSignalrRestert = false
}))
}
/**
* タブが選択されたときの処理
*/
private func handleTabSelection(_ tab: Tab) {
selectedTabModel.activeTab = tab
if tab == .map {
selectedTabModel.isPoppver.toggle()
// iPhoneは船に移動、iPadはMenu表示
if UIDevice.current.userInterfaceIdiom == .phone {
location.focusOwnShip = true
} else {
if isMenuTaskViewVisible {
MenuWindowManager.shared.hideMenuView()
} else {
mapFunctionsView()
}
isMenuTaskViewVisible.toggle()
}
} else if tab == .chat {
let message = GetMessage()
message.start()
message.readNotification()
message.checkUnreadMessages()
selectedTabModel.isPoppver = false
if UIDevice.current.userInterfaceIdiom == .pad {
MenuWindowManager.shared.hideMenuView()
isMenuTaskViewVisible = false
}
} else {
selectedTabModel.isPoppver = false
if UIDevice.current.userInterfaceIdiom == .pad {
MenuWindowManager.shared.hideMenuView()
isMenuTaskViewVisible = false
}
}
}
private func mapFunctionsView() {
let screenBounds = UIScreen.main.bounds
let screenHeight = screenBounds.height
let menuTaskViewWidth: CGFloat = 400
let menuTaskViewOffset: CGFloat = 80
let menuTaskViewX: CGFloat = 10
// メニューの縦サイズを画面の高さに応じて計算
let menuTaskViewHeight = max(200, screenHeight * 0.8)
// メニューのY座標を計算(画面の下部に配置)
let menuTaskViewY = screenHeight - menuTaskViewHeight - menuTaskViewOffset
MenuWindowManager.shared.showMenuView(
MenuTaskView()
.frame(width: menuTaskViewWidth, height: menuTaskViewHeight)
.background(ColorSet.BackgroundSecondary.color)
.cornerRadius(15),
frame: CGRect(x: menuTaskViewX, y: menuTaskViewY, width: menuTaskViewWidth, height: menuTaskViewHeight)
)
}
/**
* Warninngモードレスポンス
*/
func responseChatMode(error: Error?) {
print(debug: "responseChatMode")
message.sendInf = false
if let e = error {
print(debug: "Error chat:\(e)")
isModeAlert = true
} else {
message.changeMode()
}
}
/**
* モード変更レスポンスが無かった場合
*/
func restartChatMode() {
if message.sendInf {
let signalRService = SignalR()
signalRService.stopConnection()
Thread.sleep(forTimeInterval: 1.0)
self.isSignalrRestert = true
}
}
}
#Preview {
MainTabView()
.environmentObject(SelectedTabModel())
.environmentObject(SceneDelegate())
}
import SwiftUI
class NotificationBadgeViewModel: ObservableObject {
@Published private(set) var viewCnt: Int = 0
@Published private(set) var hasUnread: Bool = false
init(viewCnt: Int = 0) {
self.viewCnt = viewCnt
}
//MARK: - 未読カウントを更新
func updateViewCnt(to newValue: Int) {
viewCnt = newValue
hasUnread = newValue > 0
}
//MARK: - バッジをリセット(既読状態にする)
func resetBadge() {
viewCnt = 0
hasUnread = false
}
//MARK: - 現在の未読数を取得
func getUnreadCount() -> Int {
return viewCnt
}
}
struct NotificationBadge: View {
@ObservedObject var viewModel: NotificationBadgeViewModel
let font: Font
var body: some View {
Group {
if viewModel.getUnreadCount() > 0 {
Ellipse()
.fill(Color.red)
.frame(width: 12, height: 12)
.overlay(
Text(viewModel.getUnreadCount() < 10 ? "\(viewModel.getUnreadCount())" : "-")
.font(font)
.foregroundColor(.white)
)
}
}
}
}
//
// MainTabView.swift
// forShip
//
// Created by Mamoru Sugita on 2023/10/13.
//
import SwiftUI
enum Tab: String, CaseIterable{
case map = "tab_map"
case chat = "tab_chat"
case alert = "tab_notification"
case menu = "tab_menu"
var title: String{
switch self {
case .map: return "Map"
case .chat: return "Chat"
case .alert: return "Alert"
case .menu: return "Menu"
}
}
}
struct MainTabView: View {
@EnvironmentObject var selectedTabModel: SelectedTabModel
@EnvironmentObject private var sceneDelegate: SceneDelegate
@ObservedObject var location = SharingData.location
@State private var offset = CGSize.zero
@State var isSignout = false
var isTabWindowActive: Bool {
sceneDelegate.tabWindow != nil
}
var isTaskSel: Bool {
selectedTabModel.activeTab == .map
}
var body: some View {
Group {
if UIDevice.current.userInterfaceIdiom == .phone {
phoneView
} else {
padView
}
}
.onAppear {
SharingData.location.focusOwnShip = true
configureTabBarAppearance()
}
}
private var phoneView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
mapTab
if SharingData.my.isCommunication {
ChatView().tag(Tab.chat)
}
NotificationView().tag(Tab.alert)
MenuView(isSignout: $isSignout).tag(Tab.menu)
}
.sheet(isPresented: .constant(isTaskSel && isTabWindowActive)) {
MenuTaskView()
.zIndex(0)
.presentationDragIndicator(.hidden)
.presentationDetents([.height(150), .medium, .fraction(0.99)])
.presentationCornerRadius(15)
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
.presentationBackground(ColorSet.BackgroundSecondary.color)
.interactiveDismissDisabled()
}
}
}
private var padView: some View {
ZStack(alignment: .bottom) {
TabView(selection: $selectedTabModel.activeTab) {
GeometryReader { geometry in
mapTab.padding(.bottom, geometry.safeAreaInsets.bottom + 10)
}
if SharingData.my.isCommunication {
ChatView().padding(.bottom, 50).tag(Tab.chat)
}
NotificationView().padding(.bottom, 50).tag(Tab.alert)
MenuView(isSignout: $isSignout).padding(.bottom, 50).tag(Tab.menu)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
}
@ViewBuilder
private var mapTab: some View {
ZStack {
MapRepresentable()
MapInformation().zIndex(1)
}
.ignoresSafeArea(edges: .top)
.tag(Tab.map)
}
private func configureTabBarAppearance() {
let appearance = UITabBarAppearance()
appearance.backgroundColor = .clear
UITabBar.appearance().scrollEdgeAppearance = appearance
UITabBar.appearance().standardAppearance = appearance
}
}
#Preview {
MainTabView()
.environmentObject(SelectedTabModel())
.environmentObject(SceneDelegate())
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment