- 接口联调
|
|
@ -19,6 +19,9 @@
|
||||||
305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CD2FCA8C7000227D26 /* SignPlugin.swift */; };
|
305A768E2FCA8C7000227D26 /* SignPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CD2FCA8C7000227D26 /* SignPlugin.swift */; };
|
||||||
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CE2FCA8C7000227D26 /* SystemAPI.swift */; };
|
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CE2FCA8C7000227D26 /* SystemAPI.swift */; };
|
||||||
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CF2FCA8C7000227D26 /* UserAPI.swift */; };
|
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74CF2FCA8C7000227D26 /* UserAPI.swift */; };
|
||||||
|
55B230033032000100785004 /* PigeonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230023032000100785003 /* PigeonAPI.swift */; };
|
||||||
|
55B230013032000100785002 /* UploadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230003032000100785001 /* UploadService.swift */; };
|
||||||
|
55B230053032000100785006 /* PigeonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230043032000100785005 /* PigeonService.swift */; };
|
||||||
305A76912FCA8C7000227D26 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D22FCA8C7000227D26 /* Constant.swift */; };
|
305A76912FCA8C7000227D26 /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D22FCA8C7000227D26 /* Constant.swift */; };
|
||||||
305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D32FCA8C7000227D26 /* LogUtils.swift */; };
|
305A76922FCA8C7000227D26 /* LogUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D32FCA8C7000227D26 /* LogUtils.swift */; };
|
||||||
305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D52FCA8C7000227D26 /* AddImageCell.swift */; };
|
305A76932FCA8C7000227D26 /* AddImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A74D52FCA8C7000227D26 /* AddImageCell.swift */; };
|
||||||
|
|
@ -87,6 +90,7 @@
|
||||||
305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75292FCA8C7000227D26 /* ThemeManager.swift */; };
|
305A76D22FCA8C7000227D26 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75292FCA8C7000227D26 /* ThemeManager.swift */; };
|
||||||
305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752B2FCA8C7000227D26 /* URLManager.swift */; };
|
305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752B2FCA8C7000227D26 /* URLManager.swift */; };
|
||||||
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752E2FCA8C7000227D26 /* GroupModel.swift */; };
|
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752E2FCA8C7000227D26 /* GroupModel.swift */; };
|
||||||
|
55B230073032000100785008 /* PigeonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230063032000100785007 /* PigeonModel.swift */; };
|
||||||
305A76D52FCA8C7000227D26 /* SystemResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752F2FCA8C7000227D26 /* SystemResponse.swift */; };
|
305A76D52FCA8C7000227D26 /* SystemResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A752F2FCA8C7000227D26 /* SystemResponse.swift */; };
|
||||||
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75312FCA8C7000227D26 /* ImagePlugin.swift */; };
|
305A76D62FCA8C7000227D26 /* ImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75312FCA8C7000227D26 /* ImagePlugin.swift */; };
|
||||||
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75322FCA8C7000227D26 /* NotEmpty.swift */; };
|
305A76D72FCA8C7000227D26 /* NotEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A75322FCA8C7000227D26 /* NotEmpty.swift */; };
|
||||||
|
|
@ -374,6 +378,10 @@
|
||||||
C15D41A1770F5B4A02E5FC59 /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
|
C15D41A1770F5B4A02E5FC59 /* AppRestrictShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */; };
|
||||||
C3C4ACC1AE3CAF5BA21328B7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226CA98495C2884EBB1D1372 /* Foundation.framework */; };
|
C3C4ACC1AE3CAF5BA21328B7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 226CA98495C2884EBB1D1372 /* Foundation.framework */; };
|
||||||
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC46E80B848E4012D192E4CE /* LockDistractView.swift */; };
|
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC46E80B848E4012D192E4CE /* LockDistractView.swift */; };
|
||||||
|
55B230093032000100785010 /* LockDistractPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230083032000100785009 /* LockDistractPermissionPopView.swift */; };
|
||||||
|
55B232013034000100785031 /* PigeonUploadPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232003034000100785030 /* PigeonUploadPopView.swift */; };
|
||||||
|
55B232033034000100785033 /* PigeonWatermarkCameraVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */; };
|
||||||
|
55B232053034000100785035 /* PigeonWatermarkCameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B232043034000100785034 /* PigeonWatermarkCameraView.swift */; };
|
||||||
CC53DE2CA88E529129547A52 /* ShieldConfigurationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
CC53DE2CA88E529129547A52 /* ShieldConfigurationExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
D3C101B198090A9F4D93E44F /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
|
D3C101B198090A9F4D93E44F /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
|
||||||
D60B79067C80CD6AF50D63FE /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
|
D60B79067C80CD6AF50D63FE /* FamilyControls.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CD6B965A124276661A84C4C /* FamilyControls.framework */; };
|
||||||
|
|
@ -436,6 +444,9 @@
|
||||||
305A74CD2FCA8C7000227D26 /* SignPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignPlugin.swift; sourceTree = "<group>"; };
|
305A74CD2FCA8C7000227D26 /* SignPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignPlugin.swift; sourceTree = "<group>"; };
|
||||||
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAPI.swift; sourceTree = "<group>"; };
|
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemAPI.swift; sourceTree = "<group>"; };
|
||||||
305A74CF2FCA8C7000227D26 /* UserAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPI.swift; sourceTree = "<group>"; };
|
305A74CF2FCA8C7000227D26 /* UserAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAPI.swift; sourceTree = "<group>"; };
|
||||||
|
55B230023032000100785003 /* PigeonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonAPI.swift; sourceTree = "<group>"; };
|
||||||
|
55B230003032000100785001 /* UploadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadService.swift; sourceTree = "<group>"; };
|
||||||
|
55B230043032000100785005 /* PigeonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonService.swift; sourceTree = "<group>"; };
|
||||||
305A74D12FCA8C7000227D26 /* adhoc.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = adhoc.xcconfig; sourceTree = "<group>"; };
|
305A74D12FCA8C7000227D26 /* adhoc.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = adhoc.xcconfig; sourceTree = "<group>"; };
|
||||||
305A74D22FCA8C7000227D26 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = "<group>"; };
|
305A74D22FCA8C7000227D26 /* Constant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = "<group>"; };
|
||||||
305A74D32FCA8C7000227D26 /* LogUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogUtils.swift; sourceTree = "<group>"; };
|
305A74D32FCA8C7000227D26 /* LogUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogUtils.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -507,6 +518,7 @@
|
||||||
305A75292FCA8C7000227D26 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
305A75292FCA8C7000227D26 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = "<group>"; };
|
||||||
305A752B2FCA8C7000227D26 /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = "<group>"; };
|
305A752B2FCA8C7000227D26 /* URLManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLManager.swift; sourceTree = "<group>"; };
|
||||||
305A752E2FCA8C7000227D26 /* GroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupModel.swift; sourceTree = "<group>"; };
|
305A752E2FCA8C7000227D26 /* GroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupModel.swift; sourceTree = "<group>"; };
|
||||||
|
55B230063032000100785007 /* PigeonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonModel.swift; sourceTree = "<group>"; };
|
||||||
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
|
305A752F2FCA8C7000227D26 /* SystemResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemResponse.swift; sourceTree = "<group>"; };
|
||||||
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
|
305A75312FCA8C7000227D26 /* ImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePlugin.swift; sourceTree = "<group>"; };
|
||||||
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
|
305A75322FCA8C7000227D26 /* NotEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotEmpty.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -797,6 +809,10 @@
|
||||||
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
98E91E51811235EAD0985421 /* DeviceActivityMonitorExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DeviceActivityMonitorExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
A1B2C3D52FDABC8C009215C1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
|
AC46E80B848E4012D192E4CE /* LockDistractView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LockDistractView.swift; sourceTree = "<group>"; };
|
||||||
|
55B230083032000100785009 /* LockDistractPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockDistractPermissionPopView.swift; sourceTree = "<group>"; };
|
||||||
|
55B232003034000100785030 /* PigeonUploadPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonUploadPopView.swift; sourceTree = "<group>"; };
|
||||||
|
55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraVC.swift; sourceTree = "<group>"; };
|
||||||
|
55B232043034000100785034 /* PigeonWatermarkCameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonWatermarkCameraView.swift; sourceTree = "<group>"; };
|
||||||
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
|
BE3D39F325FC7624C1D914D6 /* ITunesSearchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ITunesSearchService.swift; path = AppRestrict/ITunesSearchService.swift; sourceTree = "<group>"; };
|
||||||
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
|
BFFBDE92B94E7004F6B642AD /* AppRestrictShieldSettingsVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShieldSettingsVC.swift; path = AppRestrict/AppRestrictShieldSettingsVC.swift; sourceTree = "<group>"; };
|
||||||
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
|
D0112B7865829D5B6A4A0BD0 /* AppRestrictVC.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictVC.swift; path = AppRestrict/AppRestrictVC.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -914,6 +930,7 @@
|
||||||
305A74CD2FCA8C7000227D26 /* SignPlugin.swift */,
|
305A74CD2FCA8C7000227D26 /* SignPlugin.swift */,
|
||||||
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */,
|
305A74CE2FCA8C7000227D26 /* SystemAPI.swift */,
|
||||||
305A74CF2FCA8C7000227D26 /* UserAPI.swift */,
|
305A74CF2FCA8C7000227D26 /* UserAPI.swift */,
|
||||||
|
55B230023032000100785003 /* PigeonAPI.swift */,
|
||||||
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */,
|
30BAB8502FCD331C00C33B5C /* GroupAPI.swift */,
|
||||||
30D891F42FE22E0600E958FD /* OrderAPI.swift */,
|
30D891F42FE22E0600E958FD /* OrderAPI.swift */,
|
||||||
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */,
|
30D74AB52FEA34FF0050EB2C /* ItineraryAPI.swift */,
|
||||||
|
|
@ -1205,6 +1222,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
305A752E2FCA8C7000227D26 /* GroupModel.swift */,
|
305A752E2FCA8C7000227D26 /* GroupModel.swift */,
|
||||||
|
55B230063032000100785007 /* PigeonModel.swift */,
|
||||||
305A752F2FCA8C7000227D26 /* SystemResponse.swift */,
|
305A752F2FCA8C7000227D26 /* SystemResponse.swift */,
|
||||||
30EFF3DF2FDA9CE300EB35D4 /* EmergencyContactModel.swift */,
|
30EFF3DF2FDA9CE300EB35D4 /* EmergencyContactModel.swift */,
|
||||||
);
|
);
|
||||||
|
|
@ -1391,6 +1409,8 @@
|
||||||
children = (
|
children = (
|
||||||
305A763B2FCA8C7000227D26 /* SystemService.swift */,
|
305A763B2FCA8C7000227D26 /* SystemService.swift */,
|
||||||
305A763C2FCA8C7000227D26 /* UserService.swift */,
|
305A763C2FCA8C7000227D26 /* UserService.swift */,
|
||||||
|
55B230003032000100785001 /* UploadService.swift */,
|
||||||
|
55B230043032000100785005 /* PigeonService.swift */,
|
||||||
30BAB8522FCD337C00C33B5C /* GroupService.swift */,
|
30BAB8522FCD337C00C33B5C /* GroupService.swift */,
|
||||||
30D891F62FE22E6E00E958FD /* OrderService.swift */,
|
30D891F62FE22E6E00E958FD /* OrderService.swift */,
|
||||||
30D74AB72FEA36A50050EB2C /* ItineraryService.swift */,
|
30D74AB72FEA36A50050EB2C /* ItineraryService.swift */,
|
||||||
|
|
@ -2058,6 +2078,9 @@
|
||||||
55B219C3302A000100784772 /* PigeonMessageView.swift */,
|
55B219C3302A000100784772 /* PigeonMessageView.swift */,
|
||||||
55B219C5302A000100784775 /* PigeonMessageHistoryVC.swift */,
|
55B219C5302A000100784775 /* PigeonMessageHistoryVC.swift */,
|
||||||
55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */,
|
55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */,
|
||||||
|
55B232003034000100785030 /* PigeonUploadPopView.swift */,
|
||||||
|
55B232023034000100785032 /* PigeonWatermarkCameraVC.swift */,
|
||||||
|
55B232043034000100785034 /* PigeonWatermarkCameraView.swift */,
|
||||||
);
|
);
|
||||||
path = PigeonMessage;
|
path = PigeonMessage;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -2105,6 +2128,7 @@
|
||||||
children = (
|
children = (
|
||||||
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */,
|
F9CFFB14BA2ECC259151BCDA /* LockDistractVC.swift */,
|
||||||
AC46E80B848E4012D192E4CE /* LockDistractView.swift */,
|
AC46E80B848E4012D192E4CE /* LockDistractView.swift */,
|
||||||
|
55B230083032000100785009 /* LockDistractPermissionPopView.swift */,
|
||||||
);
|
);
|
||||||
path = LockDistract;
|
path = LockDistract;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -2356,6 +2380,9 @@
|
||||||
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */,
|
305A768F2FCA8C7000227D26 /* SystemAPI.swift in Sources */,
|
||||||
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */,
|
30C4C0222FDC0ED3009215C1 /* GroupInfoVC.swift in Sources */,
|
||||||
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */,
|
305A76902FCA8C7000227D26 /* UserAPI.swift in Sources */,
|
||||||
|
55B230033032000100785004 /* PigeonAPI.swift in Sources */,
|
||||||
|
55B230013032000100785002 /* UploadService.swift in Sources */,
|
||||||
|
55B230053032000100785006 /* PigeonService.swift in Sources */,
|
||||||
30EFF3DE2FDA982C00EB35D4 /* EmergencyContactAddVC.swift in Sources */,
|
30EFF3DE2FDA982C00EB35D4 /* EmergencyContactAddVC.swift in Sources */,
|
||||||
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */,
|
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */,
|
||||||
305A76912FCA8C7000227D26 /* Constant.swift in Sources */,
|
305A76912FCA8C7000227D26 /* Constant.swift in Sources */,
|
||||||
|
|
@ -2452,6 +2479,9 @@
|
||||||
55B219C4302A000100784773 /* PigeonMessageView.swift in Sources */,
|
55B219C4302A000100784773 /* PigeonMessageView.swift in Sources */,
|
||||||
55B219C6302A000100784776 /* PigeonMessageHistoryVC.swift in Sources */,
|
55B219C6302A000100784776 /* PigeonMessageHistoryVC.swift in Sources */,
|
||||||
55B219C8302A000100784778 /* PigeonMessageHistoryView.swift in Sources */,
|
55B219C8302A000100784778 /* PigeonMessageHistoryView.swift in Sources */,
|
||||||
|
55B232013034000100785031 /* PigeonUploadPopView.swift in Sources */,
|
||||||
|
55B232033034000100785033 /* PigeonWatermarkCameraVC.swift in Sources */,
|
||||||
|
55B232053034000100785035 /* PigeonWatermarkCameraView.swift in Sources */,
|
||||||
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */,
|
30B819F22FF3CE3C00FAB693 /* ItineraryTraceView.swift in Sources */,
|
||||||
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
305A76B72FCA8C7000227D26 /* UIView+Extension.swift in Sources */,
|
||||||
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
305A76B82FCA8C7000227D26 /* UIViewController+Extension.swift in Sources */,
|
||||||
|
|
@ -2531,6 +2561,7 @@
|
||||||
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */,
|
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */,
|
||||||
305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */,
|
305A76D32FCA8C7000227D26 /* URLManager.swift in Sources */,
|
||||||
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */,
|
305A76D42FCA8C7000227D26 /* GroupModel.swift in Sources */,
|
||||||
|
55B230073032000100785008 /* PigeonModel.swift in Sources */,
|
||||||
305A798C2FCAB99300227D26 /* HomeViewModel.swift in Sources */,
|
305A798C2FCAB99300227D26 /* HomeViewModel.swift in Sources */,
|
||||||
30EFF3D12FDA69EC00EB35D4 /* AvatarIconListVC.swift in Sources */,
|
30EFF3D12FDA69EC00EB35D4 /* AvatarIconListVC.swift in Sources */,
|
||||||
30B74B4B2FF390EF00F6744D /* DrivingAPI.swift in Sources */,
|
30B74B4B2FF390EF00F6744D /* DrivingAPI.swift in Sources */,
|
||||||
|
|
@ -2687,6 +2718,7 @@
|
||||||
EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */,
|
EA13C1E21EC94958AC9755A8 /* FamilyActivityPickerHost.swift in Sources */,
|
||||||
B52F13A261ED273A3A58183F /* SelectActivityVC.swift in Sources */,
|
B52F13A261ED273A3A58183F /* SelectActivityVC.swift in Sources */,
|
||||||
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */,
|
C6A99DEE361AB31477F88475 /* LockDistractView.swift in Sources */,
|
||||||
|
55B230093032000100785010 /* LockDistractPermissionPopView.swift in Sources */,
|
||||||
E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */,
|
E3D4129BDA33540BA4E1292E /* LockDistractVC.swift in Sources */,
|
||||||
E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */,
|
E866CAAE7D4E74EB7AD608B9 /* ITunesSearchService.swift in Sources */,
|
||||||
DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */,
|
DB67C2EB2521042829A3DBA5 /* PairGuideStepsView.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
//
|
||||||
|
// PigeonAPI.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import Moya
|
||||||
|
internal import Alamofire
|
||||||
|
|
||||||
|
enum PigeonAPI {
|
||||||
|
/// 发送飞鸽传书
|
||||||
|
/// - Parameters:
|
||||||
|
/// - groupKey: 圈子 key
|
||||||
|
/// - toUsers: 收件人加密 id
|
||||||
|
/// - msgType: 1 图片 2 语音
|
||||||
|
/// - msg: 上传返回的 file id
|
||||||
|
/// - textMsg: 留言文本
|
||||||
|
/// - expireTime: 过期秒数,0 表示服务端默认 7 天
|
||||||
|
case send(groupKey: String, toUsers: [String], msgType: Int, msg: String, textMsg: String, expireTime: Int)
|
||||||
|
/// 已发送的传书记录
|
||||||
|
case sent(groupKey: String)
|
||||||
|
/// 收件箱
|
||||||
|
case inbox
|
||||||
|
/// 阅后即焚
|
||||||
|
case burn(messageUUID: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PigeonAPI: MultiTargetProtocol {
|
||||||
|
var path: String {
|
||||||
|
switch self {
|
||||||
|
case .send:
|
||||||
|
return "mapi/pigeon/send"
|
||||||
|
case .sent:
|
||||||
|
return "mapi/pigeon/sent"
|
||||||
|
case .inbox:
|
||||||
|
return "mapi/pigeon/inbox/messages"
|
||||||
|
case .burn:
|
||||||
|
return "mapi/pigeon/burn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var method: Moya.Method {
|
||||||
|
switch self {
|
||||||
|
case .sent, .inbox:
|
||||||
|
return .get
|
||||||
|
case .send, .burn:
|
||||||
|
return .post
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var task: Moya.Task {
|
||||||
|
switch self {
|
||||||
|
case let .send(groupKey, toUsers, msgType, msg, textMsg, expireTime):
|
||||||
|
var params = Parameters()
|
||||||
|
params["group_key"] = groupKey
|
||||||
|
params["to_user"] = toUsers
|
||||||
|
params["msg_type"] = msgType
|
||||||
|
params["msg"] = msg
|
||||||
|
params["text_msg"] = textMsg
|
||||||
|
params["expire_time"] = expireTime
|
||||||
|
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||||
|
case let .sent(groupKey):
|
||||||
|
var params = Parameters()
|
||||||
|
if !groupKey.isEmpty {
|
||||||
|
params["group_key"] = groupKey
|
||||||
|
}
|
||||||
|
return .requestParameters(parameters: params, encoding: URLEncoding())
|
||||||
|
case .inbox:
|
||||||
|
return .requestParameters(parameters: Parameters(), encoding: URLEncoding())
|
||||||
|
case let .burn(messageUUID):
|
||||||
|
return .requestParameters(parameters: ["message_uuid": messageUUID], encoding: JSONEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/avatar_lock.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "avatar_lock@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "avatar_lock@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/avatar_lock.imageset/avatar_lock@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/avatar_lock.imageset/avatar_lock@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/header_hero.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "header_hero@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "header_hero@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/header_hero.imageset/header_hero@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/header_hero.imageset/header_hero@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "horn_bird@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "horn_bird@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/horn_bird.imageset/horn_bird@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/horn_bird.imageset/horn_bird@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/info_circle.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "info_circle@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "info_circle@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/info_circle.imageset/info_circle@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 389 B |
BIN
QuickLocation/Assets.xcassets/LockDistract/info_circle.imageset/info_circle@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 828 B |
8
QuickLocation/Assets.xcassets/LockDistract/lock_icon_1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "lock_icon_1@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "lock_icon_1@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_1.imageset/lock_icon_1@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_1.imageset/lock_icon_1@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 36 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/lock_icon_2.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "lock_icon_2@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "lock_icon_2@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_2.imageset/lock_icon_2@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_2.imageset/lock_icon_2@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 33 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/lock_icon_3.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "lock_icon_3@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "lock_icon_3@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_3.imageset/lock_icon_3@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_3.imageset/lock_icon_3@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 34 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/lock_icon_4.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "lock_icon_4@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "lock_icon_4@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_4.imageset/lock_icon_4@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_4.imageset/lock_icon_4@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 32 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/lock_icon_5.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "lock_icon_5@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "lock_icon_5@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_5.imageset/lock_icon_5@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/lock_icon_5.imageset/lock_icon_5@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 30 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/locked_empty.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "locked_empty@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "locked_empty@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/locked_empty.imageset/locked_empty@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
QuickLocation/Assets.xcassets/LockDistract/locked_empty.imageset/locked_empty@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
8
QuickLocation/Assets.xcassets/LockDistract/member_next.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "member_prev@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "member_prev@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/member_next.imageset/member_prev@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 489 B |
BIN
QuickLocation/Assets.xcassets/LockDistract/member_next.imageset/member_prev@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 820 B |
8
QuickLocation/Assets.xcassets/LockDistract/member_prev.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "member_next@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "member_next@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/LockDistract/member_prev.imageset/member_next@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 472 B |
BIN
QuickLocation/Assets.xcassets/LockDistract/member_prev.imageset/member_next@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 803 B |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "camera@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "camera@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
8
QuickLocation/Assets.xcassets/PigeonMessage/camera_switch.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "camera_switch@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "camera_switch@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PigeonMessage/camera_switch.imageset/camera_switch@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
QuickLocation/Assets.xcassets/PigeonMessage/camera_switch.imageset/camera_switch@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "flash@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "flash@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 551 B After Width: | Height: | Size: 829 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.9 KiB |
8
QuickLocation/Assets.xcassets/PigeonMessage/upload_header.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "upload_header@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "upload_header@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PigeonMessage/upload_header.imageset/upload_header@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
QuickLocation/Assets.xcassets/PigeonMessage/upload_header.imageset/upload_header@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 64 KiB |
|
|
@ -86,7 +86,7 @@ extension DefaultsKeys {
|
||||||
case -1: // UAT
|
case -1: // UAT
|
||||||
return "https://jsapi.zuom8.cn/"
|
return "https://jsapi.zuom8.cn/"
|
||||||
default: // SIT
|
default: // SIT
|
||||||
return "http://172.16.10.24:9243/"
|
return "http://172.16.10.23:9243/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,8 @@ struct GroupMemberModel: Mappable, Equatable {
|
||||||
}
|
}
|
||||||
/// 昵称
|
/// 昵称
|
||||||
var nick_name: String = ""
|
var nick_name: String = ""
|
||||||
|
/// 备注
|
||||||
|
var remark: String = ""
|
||||||
/// 会员等级 1普通 2白银 3永久 4钻石
|
/// 会员等级 1普通 2白银 3永久 4钻石
|
||||||
var level: Int = 1
|
var level: Int = 1
|
||||||
var vipIcon: UIImage {
|
var vipIcon: UIImage {
|
||||||
|
|
@ -377,6 +379,7 @@ struct GroupMemberModel: Mappable, Equatable {
|
||||||
head_pic <- map["head_pic"]
|
head_pic <- map["head_pic"]
|
||||||
level <- (map["level"], kStrTransformInt)
|
level <- (map["level"], kStrTransformInt)
|
||||||
nick_name <- map["nick_name"]
|
nick_name <- map["nick_name"]
|
||||||
|
remark <- map["remark"]
|
||||||
battery <- map["battery"]
|
battery <- map["battery"]
|
||||||
last_position <- map["last_position"]
|
last_position <- map["last_position"]
|
||||||
is_online <- map["is_online"]
|
is_online <- map["is_online"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
//
|
||||||
|
// PigeonModel.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import ObjectMapper
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct PigeonSentResponse: BaseModelProtocol {
|
||||||
|
var code: String?
|
||||||
|
var message: String?
|
||||||
|
var list: [PigeonSentMessage] = []
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
code <- (map["code"], kIntTransformStr)
|
||||||
|
message <- map["message"]
|
||||||
|
list <- map["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PigeonSentMessage: Mappable {
|
||||||
|
var message_uuid: String = ""
|
||||||
|
var group_key: String = ""
|
||||||
|
var from_user: PigeonSentUser?
|
||||||
|
var to_user: [PigeonSentUser] = []
|
||||||
|
var msg_type: Int = 0
|
||||||
|
var msg: String = ""
|
||||||
|
var text_msg: String = ""
|
||||||
|
var expire_time: Int64 = 0
|
||||||
|
var send_time: Int64 = 0
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
message_uuid <- map["message_uuid"]
|
||||||
|
group_key <- map["group_key"]
|
||||||
|
from_user <- map["from_user"]
|
||||||
|
to_user <- map["to_user"]
|
||||||
|
msg_type <- map["msg_type"]
|
||||||
|
msg <- map["msg"]
|
||||||
|
text_msg <- map["text_msg"]
|
||||||
|
expire_time <- map["expire_time"]
|
||||||
|
send_time <- map["send_time"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var mediaURL: URL? {
|
||||||
|
let value = msg.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard value.lowercased().hasPrefix("http"), let url = URL(string: value) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func toHistoryItem() -> PigeonHistoryItem {
|
||||||
|
PigeonHistoryItem(
|
||||||
|
id: message_uuid.isEmpty ? UUID().uuidString : message_uuid,
|
||||||
|
dateText: Self.dateText(from: send_time),
|
||||||
|
timeText: Self.timeText(from: send_time),
|
||||||
|
kind: msg_type == 2 ? .voice : .image,
|
||||||
|
caption: text_msg,
|
||||||
|
image: nil,
|
||||||
|
mediaURL: mediaURL,
|
||||||
|
senderAvatar: from_user?.avatarImage,
|
||||||
|
receiverAvatars: to_user.map(\.avatarImage),
|
||||||
|
duration: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func date(from timestamp: Int64) -> Date {
|
||||||
|
Date(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func dateText(from timestamp: Int64) -> String {
|
||||||
|
let date = date(from: timestamp)
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let month = calendar.component(.month, from: date)
|
||||||
|
let day = calendar.component(.day, from: date)
|
||||||
|
let weekdays = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
||||||
|
let weekday = weekdays[calendar.component(.weekday, from: date) - 1]
|
||||||
|
return "\(month).\(day) \(weekday)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func timeText(from timestamp: Int64) -> String {
|
||||||
|
let date = date(from: timestamp)
|
||||||
|
let calendar = Calendar.current
|
||||||
|
let hour = calendar.component(.hour, from: date)
|
||||||
|
let minute = calendar.component(.minute, from: date)
|
||||||
|
let period = hour < 12 ? "上午" : "下午"
|
||||||
|
let hour12 = hour % 12 == 0 ? 12 : hour % 12
|
||||||
|
return String(format: "%@ %d:%02d", period, hour12, minute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PigeonSentUser: Mappable {
|
||||||
|
var user_id: String = ""
|
||||||
|
var nick_name: String = ""
|
||||||
|
var remark: String = ""
|
||||||
|
var head_pic: String = ""
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
user_id <- map["user_id"]
|
||||||
|
nick_name <- map["nick_name"]
|
||||||
|
remark <- map["remark"]
|
||||||
|
head_pic <- map["head_pic"]
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
let remarkName = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !remarkName.isEmpty { return remarkName }
|
||||||
|
return nick_name
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarImage: UIImage {
|
||||||
|
let iconName = head_pic.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !iconName.isEmpty,
|
||||||
|
let image = UIImage(named: "UserIcon/\(iconName)") else {
|
||||||
|
return UIImage(named: "UserIcon/1") ?? UIImage()
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import RxSwift
|
import RxSwift
|
||||||
import RxCocoa
|
import RxCocoa
|
||||||
import TagListView
|
|
||||||
|
|
||||||
final class GroupSettingView: UIView {
|
final class GroupSettingView: UIView {
|
||||||
|
|
||||||
|
|
@ -706,7 +705,14 @@ final class GroupSettingTagRow: UIView {
|
||||||
|
|
||||||
private let titleLab = UILabel()
|
private let titleLab = UILabel()
|
||||||
private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right"))
|
private let arrowImageView = UIImageView(image: UIImage(named: "GroupSetting/arrow_right"))
|
||||||
private let tagListView = TagListView()
|
private let tagStack: UIStackView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.axis = .horizontal
|
||||||
|
stack.alignment = .center
|
||||||
|
stack.spacing = 6
|
||||||
|
stack.isUserInteractionEnabled = false
|
||||||
|
return stack
|
||||||
|
}()
|
||||||
private let dividerView = UIView()
|
private let dividerView = UIView()
|
||||||
|
|
||||||
private var tagTrailingToArrow: NSLayoutConstraint?
|
private var tagTrailingToArrow: NSLayoutConstraint?
|
||||||
|
|
@ -722,13 +728,24 @@ final class GroupSettingTagRow: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
func configure(tags: [String], editable: Bool) {
|
func configure(tags: [String], editable: Bool) {
|
||||||
tagListView.removeAllTags()
|
tagStack.arrangedSubviews.forEach {
|
||||||
tagListView.addTags(tags)
|
tagStack.removeArrangedSubview($0)
|
||||||
tagListView.tagViews.forEach { $0.layer.cornerRadius = 7 }
|
$0.removeFromSuperview()
|
||||||
|
}
|
||||||
|
if tags.count > 2 {
|
||||||
|
tagStack.addArrangedSubview(GroupSettingTagChip(text: "···", compact: true))
|
||||||
|
tags.suffix(2).forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
||||||
|
} else {
|
||||||
|
tags.forEach { tagStack.addArrangedSubview(GroupSettingTagChip(text: $0, compact: false)) }
|
||||||
|
}
|
||||||
arrowImageView.isHidden = !editable
|
arrowImageView.isHidden = !editable
|
||||||
tagTrailingToArrow?.isActive = editable
|
tagTrailingToArrow?.isActive = false
|
||||||
tagTrailingToRow?.isActive = !editable
|
tagTrailingToRow?.isActive = false
|
||||||
tagListView.invalidateIntrinsicContentSize()
|
if editable {
|
||||||
|
tagTrailingToArrow?.isActive = true
|
||||||
|
} else {
|
||||||
|
tagTrailingToRow?.isActive = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
|
|
@ -741,36 +758,29 @@ final class GroupSettingTagRow: UIView {
|
||||||
arrowImageView.isHidden = true
|
arrowImageView.isHidden = true
|
||||||
dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0")
|
dividerView.backgroundColor = UIColor(hexStr: "#EDEEF0")
|
||||||
|
|
||||||
tagListView.textFont = .systemFont(ofSize: 14, weight: .regular)
|
tagStack.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
tagListView.textColor = UIColor(hexStr: "#16B3FF")
|
tagStack.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
||||||
tagListView.tagBackgroundColor = UIColor(hexStr: "#E4F6FF")
|
|
||||||
tagListView.paddingX = 12
|
|
||||||
tagListView.paddingY = 5
|
|
||||||
tagListView.alignment = .right
|
|
||||||
tagListView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
||||||
tagListView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
||||||
|
|
||||||
[titleLab, arrowImageView, tagListView, dividerView].forEach {
|
[titleLab, arrowImageView, tagStack, dividerView].forEach {
|
||||||
addSubview($0)
|
addSubview($0)
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
}
|
}
|
||||||
|
|
||||||
tagTrailingToArrow = tagListView.trailingAnchor.constraint(equalTo: arrowImageView.leadingAnchor, constant: -10)
|
tagTrailingToArrow = tagStack.trailingAnchor.constraint(equalTo: arrowImageView.leadingAnchor, constant: -10)
|
||||||
tagTrailingToRow = tagListView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
|
tagTrailingToRow = tagStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16)
|
||||||
tagTrailingToRow?.isActive = true
|
tagTrailingToRow?.isActive = true
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
titleLab.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
titleLab.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||||
titleLab.topAnchor.constraint(equalTo: topAnchor, constant: 18),
|
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||||
arrowImageView.centerYAnchor.constraint(equalTo: titleLab.centerYAnchor),
|
arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
arrowImageView.widthAnchor.constraint(equalToConstant: 16),
|
arrowImageView.widthAnchor.constraint(equalToConstant: 16),
|
||||||
arrowImageView.heightAnchor.constraint(equalToConstant: 16),
|
arrowImageView.heightAnchor.constraint(equalToConstant: 16),
|
||||||
tagListView.leadingAnchor.constraint(equalTo: titleLab.trailingAnchor, constant: 20),
|
tagStack.leadingAnchor.constraint(greaterThanOrEqualTo: titleLab.trailingAnchor, constant: 12),
|
||||||
tagListView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
|
tagStack.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
tagListView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12),
|
tagStack.heightAnchor.constraint(equalToConstant: 28),
|
||||||
tagListView.heightAnchor.constraint(greaterThanOrEqualToConstant: 28),
|
heightAnchor.constraint(equalToConstant: 62),
|
||||||
heightAnchor.constraint(greaterThanOrEqualToConstant: 62),
|
|
||||||
dividerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
dividerView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16),
|
||||||
dividerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
dividerView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16),
|
||||||
dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
dividerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
|
@ -779,6 +789,42 @@ final class GroupSettingTagRow: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final class GroupSettingTagChip: UIView {
|
||||||
|
|
||||||
|
private let label = UILabel()
|
||||||
|
private let insets: UIEdgeInsets
|
||||||
|
|
||||||
|
init(text: String, compact: Bool) {
|
||||||
|
insets = compact
|
||||||
|
? UIEdgeInsets(top: 5, left: 8, bottom: 5, right: 8)
|
||||||
|
: UIEdgeInsets(top: 5, left: 12, bottom: 5, right: 12)
|
||||||
|
super.init(frame: .zero)
|
||||||
|
isUserInteractionEnabled = false
|
||||||
|
backgroundColor = UIColor(hexStr: "#E4F6FF")
|
||||||
|
layer.cornerRadius = 8
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
label.text = text
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .regular)
|
||||||
|
label.textColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
label.textAlignment = .center
|
||||||
|
addSubview(label)
|
||||||
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: insets.left),
|
||||||
|
label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -insets.right),
|
||||||
|
label.topAnchor.constraint(equalTo: topAnchor, constant: insets.top),
|
||||||
|
label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom)
|
||||||
|
])
|
||||||
|
setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class GroupSettingActionRow: UIView {
|
final class GroupSettingActionRow: UIView {
|
||||||
|
|
||||||
init(title: String,
|
init(title: String,
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,12 @@ final class GroupViewController: BaseViewController {
|
||||||
self?.rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList
|
self?.rootView.cycleScrollView.imageURLStringsGroup = config.groupBannerList
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.refreshItineraryPage()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bindItineraryPage() {
|
private func bindItineraryPage() {
|
||||||
|
|
@ -237,8 +243,7 @@ final class GroupViewController: BaseViewController {
|
||||||
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
|
GroupListPopView.show(groupModel: groupModel) { [weak self] groupKey in
|
||||||
guard let self = self, let key = groupKey else { return }
|
guard let self = self, let key = groupKey else { return }
|
||||||
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
|
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
|
||||||
.subscribe(onNext: { [weak self] _ in
|
.subscribe(onNext: { _ in
|
||||||
self?.refreshItineraryPage()
|
|
||||||
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
||||||
})
|
})
|
||||||
.disposed(by: self.disposeBag)
|
.disposed(by: self.disposeBag)
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ final class MemberInfoVC: BaseViewController {
|
||||||
} else {
|
} else {
|
||||||
rootView.renderRows([
|
rootView.renderRows([
|
||||||
MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .text),
|
MemberInfoRow(title: "昵称", value: member?.nick_name ?? "", kind: .text),
|
||||||
MemberInfoRow(title: "备注", value: member?.nick_name ?? "", kind: .disclosure) {
|
MemberInfoRow(title: "备注", value: member?.remark ?? "", kind: .disclosure) {
|
||||||
DLToast.show(text: "敬请期待")
|
DLToast.show(text: "敬请期待")
|
||||||
},
|
},
|
||||||
MemberInfoRow(title: "关系", value: "未绑定", kind: .disclosure) {
|
MemberInfoRow(title: "关系", value: "未绑定", kind: .disclosure) {
|
||||||
|
|
|
||||||
|
|
@ -335,7 +335,7 @@ extension CommonGroupsPopVC: UITableViewDataSource {
|
||||||
private final class CommonGroupReadonlyCell: UITableViewCell {
|
private final class CommonGroupReadonlyCell: UITableViewCell {
|
||||||
private let cardView = UIView()
|
private let cardView = UIView()
|
||||||
private let iconView = UIImageView()
|
private let iconView = UIImageView()
|
||||||
private let nameLabel = MemberInfoTagLabel()
|
private let nameLabel = UILabel()
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
|
@ -359,13 +359,8 @@ private final class CommonGroupReadonlyCell: UITableViewCell {
|
||||||
iconView.contentMode = .scaleAspectFill
|
iconView.contentMode = .scaleAspectFill
|
||||||
iconView.layer.cornerRadius = 8
|
iconView.layer.cornerRadius = 8
|
||||||
iconView.clipsToBounds = true
|
iconView.clipsToBounds = true
|
||||||
|
nameLabel.textColor = UIColor(hexStr: "#293445")
|
||||||
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
nameLabel.textColor = UIColor(hexStr: "#D5923F")
|
|
||||||
nameLabel.backgroundColor = UIColor(hexStr: "#FFF9DD")
|
|
||||||
nameLabel.numberOfLines = 1
|
|
||||||
nameLabel.lineBreakMode = .byTruncatingTail
|
|
||||||
nameLabel.layer.cornerRadius = 7
|
|
||||||
nameLabel.clipsToBounds = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ final class CreateBubbleSetupView: UIView {
|
||||||
titleLab.textAlignment = .center
|
titleLab.textAlignment = .center
|
||||||
titleLab.layoutChain.topToBottomOfView(heroView, offset: 25).centerX()
|
titleLab.layoutChain.topToBottomOfView(heroView, offset: 25).centerX()
|
||||||
|
|
||||||
subtitleLab.text = "设置气泡时间"
|
subtitleLab.text = "设置气泡后会隐藏您的位置"
|
||||||
subtitleLab.font = .systemFont(ofSize: 18, weight: .bold)
|
subtitleLab.font = .systemFont(ofSize: 18, weight: .bold)
|
||||||
subtitleLab.textColor = UIColor(hexStr: "#9CA3AF")
|
subtitleLab.textColor = UIColor(hexStr: "#9CA3AF")
|
||||||
subtitleLab.layoutChain.topToBottomOfView(titleLab, offset: 19).centerX()
|
subtitleLab.layoutChain.topToBottomOfView(titleLab, offset: 19).centerX()
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ class GroupListPopView: UIView {
|
||||||
|
|
||||||
private var selectedGroupKey: String = ""
|
private var selectedGroupKey: String = ""
|
||||||
private var currentGroupOnlineCount: Int?
|
private var currentGroupOnlineCount: Int?
|
||||||
|
private var onlineCountGroupKey: String = ""
|
||||||
|
|
||||||
private var groupList: [GroupInfoModel] = [] {
|
private var groupList: [GroupInfoModel] = [] {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
@ -188,6 +189,7 @@ extension GroupListPopView {
|
||||||
/// 通过 window 从底部弹出圈子列表
|
/// 通过 window 从底部弹出圈子列表
|
||||||
static func show(groupModel: GroupModel,
|
static func show(groupModel: GroupModel,
|
||||||
currentMembers: [GroupMemberModel]? = nil,
|
currentMembers: [GroupMemberModel]? = nil,
|
||||||
|
selectedKey: String? = nil,
|
||||||
completion: @escaping ((String?) -> Void)) {
|
completion: @escaping ((String?) -> Void)) {
|
||||||
guard let superView = kKeyWindow else { return }
|
guard let superView = kKeyWindow else { return }
|
||||||
|
|
||||||
|
|
@ -199,6 +201,10 @@ extension GroupListPopView {
|
||||||
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
pop.frame = CGRect(x: 0, y: 0, width: kScreenWidth, height: kScreenHeight)
|
||||||
pop.currentGroupOnlineCount = currentMembers.map { $0.filter(\.is_online).count }
|
pop.currentGroupOnlineCount = currentMembers.map { $0.filter(\.is_online).count }
|
||||||
pop.groupModel = groupModel
|
pop.groupModel = groupModel
|
||||||
|
let highlightKey = (selectedKey?.isEmpty == false) ? selectedKey! : groupModel.default_group_key
|
||||||
|
pop.selectedGroupKey = highlightKey
|
||||||
|
pop.onlineCountGroupKey = highlightKey
|
||||||
|
pop.tableView.reloadData()
|
||||||
pop.completion = { text in
|
pop.completion = { text in
|
||||||
completion(text)
|
completion(text)
|
||||||
GroupListPopView.dismiss()
|
GroupListPopView.dismiss()
|
||||||
|
|
@ -248,7 +254,7 @@ extension GroupListPopView: UITableViewDataSource, UITableViewDelegate {
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
let cell: GroupListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
let model = groupList[indexPath.row]
|
let model = groupList[indexPath.row]
|
||||||
let onlineCount: Int? = (model.group_key == groupModel?.default_group_key)
|
let onlineCount: Int? = (model.group_key == onlineCountGroupKey)
|
||||||
? currentGroupOnlineCount
|
? currentGroupOnlineCount
|
||||||
: nil
|
: nil
|
||||||
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key, onlineCount: onlineCount)
|
cell.configure(model: model, isSelected: selectedGroupKey == model.group_key, onlineCount: onlineCount)
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ class HomeView2: UIView {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
unlockReceiveBtn.rx.tap
|
pigeonMsgBtn.rx.tap
|
||||||
.subscribe(onNext: { [weak self] in
|
.subscribe(onNext: { [weak self] in
|
||||||
self?.onUnlockReceiveTap?()
|
self?.onUnlockReceiveTap?()
|
||||||
})
|
})
|
||||||
|
|
@ -236,7 +236,7 @@ class HomeView2: UIView {
|
||||||
|
|
||||||
// MARK: - 解锁操作
|
// MARK: - 解锁操作
|
||||||
lazy var actionStackView: UIStackView = {
|
lazy var actionStackView: UIStackView = {
|
||||||
let stackView = UIStackView(arrangedSubviews: [unlockRequestBtn, unlockReceiveBtn])
|
let stackView = UIStackView(arrangedSubviews: [unlockRequestBtn, pigeonMsgBtn])
|
||||||
stackView.axis = .vertical
|
stackView.axis = .vertical
|
||||||
stackView.alignment = .center
|
stackView.alignment = .center
|
||||||
stackView.distribution = .fill
|
stackView.distribution = .fill
|
||||||
|
|
@ -245,7 +245,7 @@ class HomeView2: UIView {
|
||||||
unlockRequestBtn.layoutChain
|
unlockRequestBtn.layoutChain
|
||||||
.width(51)
|
.width(51)
|
||||||
.height(50)
|
.height(50)
|
||||||
unlockReceiveBtn.layoutChain
|
pigeonMsgBtn.layoutChain
|
||||||
.width(50)
|
.width(50)
|
||||||
.height(50)
|
.height(50)
|
||||||
|
|
||||||
|
|
@ -259,10 +259,11 @@ class HomeView2: UIView {
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var unlockReceiveBtn: UIButton = {
|
lazy var pigeonMsgBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setImage(UIImage(named: "Home/unlock_receive")?.withRenderingMode(.alwaysOriginal), for: .normal)
|
btn.setImage(UIImage(named: "Home/pigeon_msg")?.withRenderingMode(.alwaysOriginal), for: .normal)
|
||||||
btn.imageView?.contentMode = .scaleAspectFit
|
btn.imageView?.contentMode = .scaleAspectFit
|
||||||
|
btn.isHidden = true
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ class HomeViewController: BaseViewController {
|
||||||
private var viewModel = HomeViewModel()
|
private var viewModel = HomeViewModel()
|
||||||
/// 当前选中的成员(用于 GroupMemberListCell 选中态)
|
/// 当前选中的成员(用于 GroupMemberListCell 选中态)
|
||||||
private var selectedMemberId: String = ""
|
private var selectedMemberId: String = ""
|
||||||
|
private var inboxMessages: [PigeonSentMessage] = []
|
||||||
|
|
||||||
private let locationManager = CLLocationManager()
|
private let locationManager = CLLocationManager()
|
||||||
private var currentHeading: Double = 0
|
private var currentHeading: Double = 0
|
||||||
|
|
@ -109,6 +110,7 @@ class HomeViewController: BaseViewController {
|
||||||
|
|
||||||
let manager = AuthorizeManager.manager(type: .locationAlways)
|
let manager = AuthorizeManager.manager(type: .locationAlways)
|
||||||
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
|
rootView.warningView.isHidden = manager?.authorizeStatus() == .authorized
|
||||||
|
refreshPigeonInbox()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidDisappear(_ animated: Bool) {
|
override func viewDidDisappear(_ animated: Bool) {
|
||||||
|
|
@ -198,9 +200,14 @@ class HomeViewController: BaseViewController {
|
||||||
|
|
||||||
rootView.onUnlockReceiveTap = { [weak self] in
|
rootView.onUnlockReceiveTap = { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
ReceiveMessagePopView.show(items: self.makeMockReceiveMessages()) { _ in
|
let items = self.inboxMessages.map(self.makeReceiveDisplayItem)
|
||||||
// 预留阅后即焚完成回调,后续接入消息销毁接口。
|
guard !items.isEmpty else { return }
|
||||||
|
ReceiveMessagePopView.show(
|
||||||
|
items: items,
|
||||||
|
onRequestBurn: { [weak self] item, done in
|
||||||
|
self?.burnInboxMessage(item, done: done)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 气泡
|
// 气泡
|
||||||
|
|
@ -307,6 +314,7 @@ class HomeViewController: BaseViewController {
|
||||||
self?.requestGroupInfo()
|
self?.requestGroupInfo()
|
||||||
}
|
}
|
||||||
self.requestNotice()
|
self.requestNotice()
|
||||||
|
self.refreshPigeonInbox()
|
||||||
self.popupQueue = nil
|
self.popupQueue = nil
|
||||||
self.popupQueue = PopupQueueManager()
|
self.popupQueue = PopupQueueManager()
|
||||||
self.popupQueue?.start(from: self)
|
self.popupQueue?.start(from: self)
|
||||||
|
|
@ -457,54 +465,59 @@ class HomeViewController: BaseViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeMockReceiveMessages() -> [ReceiveMessageDisplayItem] {
|
private func refreshPigeonInbox() {
|
||||||
var candidates = viewModel.memberList
|
guard Defaults[\.loginToken] != nil else {
|
||||||
let fallbackImage = UIImage(named: "UserIcon/1")
|
inboxMessages = []
|
||||||
let mockMessages = [
|
rootView.pigeonMsgBtn.isHidden = true
|
||||||
"想对你说的话都藏在这里,请查收呀",
|
return
|
||||||
"今天也要开开心心,注意安全哦"
|
}
|
||||||
]
|
PigeonService.inbox()
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.inboxMessages = response.list
|
||||||
|
self.rootView.pigeonMsgBtn.isHidden = response.list.isEmpty
|
||||||
|
}, onError: { [weak self] _ in
|
||||||
|
self?.rootView.pigeonMsgBtn.isHidden = self?.inboxMessages.isEmpty ?? true
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
guard !candidates.isEmpty else {
|
private func makeReceiveDisplayItem(_ message: PigeonSentMessage) -> ReceiveMessageDisplayItem {
|
||||||
return [
|
|
||||||
ReceiveMessageDisplayItem(
|
ReceiveMessageDisplayItem(
|
||||||
senderName: "圈子成员",
|
messageUUID: message.message_uuid,
|
||||||
avatar: fallbackImage,
|
senderName: message.from_user?.displayName ?? "圈子成员",
|
||||||
image: fallbackImage,
|
avatar: message.from_user?.avatarImage,
|
||||||
message: mockMessages[0]
|
image: nil,
|
||||||
),
|
imageURL: message.msg_type == 2 ? nil : message.mediaURL,
|
||||||
ReceiveMessageDisplayItem(
|
message: message.text_msg,
|
||||||
senderName: "圈子成员",
|
kind: message.msg_type == 2 ? .voice : .image,
|
||||||
avatar: fallbackImage,
|
duration: 0,
|
||||||
image: fallbackImage,
|
audioURL: message.msg_type == 2 ? message.mediaURL : nil
|
||||||
message: mockMessages[1],
|
|
||||||
kind: .voice,
|
|
||||||
duration: 10
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
if let selectedIndex = candidates.firstIndex(where: { $0.user_id == selectedMemberId }) {
|
|
||||||
let selectedMember = candidates.remove(at: selectedIndex)
|
|
||||||
candidates.insert(selectedMember, at: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
while candidates.count < 2 {
|
|
||||||
candidates.append(contentsOf: candidates)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array(candidates.prefix(2)).enumerated().map { index, member in
|
|
||||||
let memberImage = member.userIcon
|
|
||||||
let displayImage = memberImage.size == .zero ? fallbackImage : memberImage
|
|
||||||
return ReceiveMessageDisplayItem(
|
|
||||||
senderName: member.nick_name,
|
|
||||||
avatar: displayImage,
|
|
||||||
image: displayImage,
|
|
||||||
message: mockMessages[index],
|
|
||||||
kind: index == 1 ? .voice : .image,
|
|
||||||
duration: index == 1 ? 10 : 0
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func burnInboxMessage(_ item: ReceiveMessageDisplayItem, done: @escaping (Bool) -> Void) {
|
||||||
|
guard !item.messageUUID.isEmpty else {
|
||||||
|
done(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DLToast.showLoading()
|
||||||
|
PigeonService.burn(messageUUID: item.messageUUID)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let self else {
|
||||||
|
done(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.inboxMessages.removeAll { $0.message_uuid == item.messageUUID }
|
||||||
|
self.rootView.pigeonMsgBtn.isHidden = self.inboxMessages.isEmpty
|
||||||
|
done(true)
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.show(text: error.localizedDescription)
|
||||||
|
done(false)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestUserInfo(completion: (() -> Void)? = nil) {
|
private func requestUserInfo(completion: (() -> Void)? = nil) {
|
||||||
|
|
@ -585,8 +598,8 @@ class HomeViewController: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey])
|
GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey])
|
||||||
.subscribe(onNext: { [weak self] _ in
|
.subscribe(onNext: { _ in
|
||||||
self?.requestGroupInfo()
|
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
||||||
}, onError: { error in
|
}, onError: { error in
|
||||||
DLToast.showError(text: error.localizedDescription)
|
DLToast.showError(text: error.localizedDescription)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ final class PhoneReportDetailView: UIView {
|
||||||
.heightToWidth(160/375)
|
.heightToWidth(160/375)
|
||||||
|
|
||||||
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
navView.layoutChain.top().edgesHorzontal().height(kNaviHeight)
|
||||||
navView.addRightButton(switchGroupBtn)
|
// navView.addRightButton(switchGroupBtn)
|
||||||
|
|
||||||
addSubview(scrollView)
|
addSubview(scrollView)
|
||||||
scrollView.showsVerticalScrollIndicator = false
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
enum ReceiveMessageKind {
|
enum ReceiveMessageKind {
|
||||||
case image
|
case image
|
||||||
|
|
@ -12,26 +13,32 @@ enum ReceiveMessageKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReceiveMessageDisplayItem {
|
struct ReceiveMessageDisplayItem {
|
||||||
|
let messageUUID: String
|
||||||
let senderName: String
|
let senderName: String
|
||||||
let avatar: UIImage?
|
let avatar: UIImage?
|
||||||
let image: UIImage?
|
let image: UIImage?
|
||||||
|
let imageURL: URL?
|
||||||
let message: String
|
let message: String
|
||||||
let kind: ReceiveMessageKind
|
let kind: ReceiveMessageKind
|
||||||
let duration: TimeInterval
|
let duration: TimeInterval
|
||||||
let audioURL: URL?
|
let audioURL: URL?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
|
messageUUID: String = "",
|
||||||
senderName: String,
|
senderName: String,
|
||||||
avatar: UIImage?,
|
avatar: UIImage?,
|
||||||
image: UIImage?,
|
image: UIImage?,
|
||||||
|
imageURL: URL? = nil,
|
||||||
message: String,
|
message: String,
|
||||||
kind: ReceiveMessageKind = .image,
|
kind: ReceiveMessageKind = .image,
|
||||||
duration: TimeInterval = 0,
|
duration: TimeInterval = 0,
|
||||||
audioURL: URL? = nil
|
audioURL: URL? = nil
|
||||||
) {
|
) {
|
||||||
|
self.messageUUID = messageUUID
|
||||||
self.senderName = senderName
|
self.senderName = senderName
|
||||||
self.avatar = avatar
|
self.avatar = avatar
|
||||||
self.image = image
|
self.image = image
|
||||||
|
self.imageURL = imageURL
|
||||||
self.message = message
|
self.message = message
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
|
@ -47,6 +54,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
|
|
||||||
private var messages: [ReceiveMessageDisplayItem] = []
|
private var messages: [ReceiveMessageDisplayItem] = []
|
||||||
private var item: ReceiveMessageDisplayItem?
|
private var item: ReceiveMessageDisplayItem?
|
||||||
|
private var onRequestBurn: ((ReceiveMessageDisplayItem, @escaping (Bool) -> Void) -> Void)?
|
||||||
private var onBurn: ((ReceiveMessageDisplayItem) -> Void)?
|
private var onBurn: ((ReceiveMessageDisplayItem) -> Void)?
|
||||||
private var isBurning = false
|
private var isBurning = false
|
||||||
private var effectLayers: [CALayer] = []
|
private var effectLayers: [CALayer] = []
|
||||||
|
|
@ -75,7 +83,8 @@ final class ReceiveMessagePopView: UIView {
|
||||||
|
|
||||||
static func show(
|
static func show(
|
||||||
items: [ReceiveMessageDisplayItem],
|
items: [ReceiveMessageDisplayItem],
|
||||||
onBurn: ((ReceiveMessageDisplayItem) -> Void)? = nil
|
onBurn: ((ReceiveMessageDisplayItem) -> Void)? = nil,
|
||||||
|
onRequestBurn: ((ReceiveMessageDisplayItem, @escaping (Bool) -> Void) -> Void)? = nil
|
||||||
) {
|
) {
|
||||||
guard let firstItem = items.first, let window = kKeyWindow else { return }
|
guard let firstItem = items.first, let window = kKeyWindow else { return }
|
||||||
|
|
||||||
|
|
@ -84,6 +93,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
popup.resetBurnState()
|
popup.resetBurnState()
|
||||||
popup.messages = items
|
popup.messages = items
|
||||||
popup.item = firstItem
|
popup.item = firstItem
|
||||||
|
popup.onRequestBurn = onRequestBurn
|
||||||
popup.onBurn = onBurn
|
popup.onBurn = onBurn
|
||||||
popup.configure(item: firstItem)
|
popup.configure(item: firstItem)
|
||||||
|
|
||||||
|
|
@ -94,9 +104,11 @@ final class ReceiveMessagePopView: UIView {
|
||||||
popup.overlayView.alpha = 0
|
popup.overlayView.alpha = 0
|
||||||
popup.letterView.alpha = 0
|
popup.letterView.alpha = 0
|
||||||
popup.reportButton.alpha = 0
|
popup.reportButton.alpha = 0
|
||||||
|
popup.closeButton.alpha = 0
|
||||||
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) {
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) {
|
||||||
popup.overlayView.alpha = 1
|
popup.overlayView.alpha = 1
|
||||||
popup.reportButton.alpha = 1
|
popup.reportButton.alpha = 1
|
||||||
|
popup.closeButton.alpha = 1
|
||||||
}
|
}
|
||||||
popup.animateInitialFlyIn()
|
popup.animateInitialFlyIn()
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +121,13 @@ final class ReceiveMessagePopView: UIView {
|
||||||
|
|
||||||
private let letterView = UIView()
|
private let letterView = UIView()
|
||||||
|
|
||||||
|
private let closeButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "Group/close"), for: .normal)
|
||||||
|
button.extendEdgeInsets = UIEdgeInsets(top: 15, left: 20, bottom: 20, right: 20)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
private let reportButton: UIButton = {
|
private let reportButton: UIButton = {
|
||||||
let button = UIButton(type: .custom)
|
let button = UIButton(type: .custom)
|
||||||
var configuration = UIButton.Configuration.plain()
|
var configuration = UIButton.Configuration.plain()
|
||||||
|
|
@ -239,6 +258,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
addSubview(overlayView)
|
addSubview(overlayView)
|
||||||
addSubview(letterView)
|
addSubview(letterView)
|
||||||
|
addSubview(closeButton)
|
||||||
addSubview(reportButton)
|
addSubview(reportButton)
|
||||||
overlayView.layoutChain.edges()
|
overlayView.layoutChain.edges()
|
||||||
|
|
||||||
|
|
@ -252,6 +272,12 @@ final class ReceiveMessagePopView: UIView {
|
||||||
.edgesHorzontal(26)
|
.edgesHorzontal(26)
|
||||||
.centerY()
|
.centerY()
|
||||||
|
|
||||||
|
closeButton.layoutChain
|
||||||
|
.topToBottomOfView(letterView, offset: 16)
|
||||||
|
.centerX()
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
|
||||||
letterView.addSubview(cardBackgroundView)
|
letterView.addSubview(cardBackgroundView)
|
||||||
letterView.addSubview(titleImageView)
|
letterView.addSubview(titleImageView)
|
||||||
letterView.addSubview(avatarView)
|
letterView.addSubview(avatarView)
|
||||||
|
|
@ -333,6 +359,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
|
|
||||||
burnButton.addTarget(self, action: #selector(tapBurn), for: .touchUpInside)
|
burnButton.addTarget(self, action: #selector(tapBurn), for: .touchUpInside)
|
||||||
reportButton.addTarget(self, action: #selector(tapReport), for: .touchUpInside)
|
reportButton.addTarget(self, action: #selector(tapReport), for: .touchUpInside)
|
||||||
|
closeButton.addTarget(self, action: #selector(tapClose), for: .touchUpInside)
|
||||||
let overlayTap = UITapGestureRecognizer(target: self, action: #selector(tapPlay))
|
let overlayTap = UITapGestureRecognizer(target: self, action: #selector(tapPlay))
|
||||||
voiceOverlay.addGestureRecognizer(overlayTap)
|
voiceOverlay.addGestureRecognizer(overlayTap)
|
||||||
}
|
}
|
||||||
|
|
@ -341,7 +368,12 @@ final class ReceiveMessagePopView: UIView {
|
||||||
stopVoicePlayback(reset: true)
|
stopVoicePlayback(reset: true)
|
||||||
nameLabel.text = item.senderName.isEmpty ? "圈子成员" : item.senderName
|
nameLabel.text = item.senderName.isEmpty ? "圈子成员" : item.senderName
|
||||||
avatarView.image = item.avatar ?? UIImage(named: "UserIcon/1")
|
avatarView.image = item.avatar ?? UIImage(named: "UserIcon/1")
|
||||||
|
messageImageView.kf.cancelDownloadTask()
|
||||||
|
if let imageURL = item.imageURL {
|
||||||
|
messageImageView.kf.setImage(with: imageURL, placeholder: item.image)
|
||||||
|
} else {
|
||||||
messageImageView.image = item.image
|
messageImageView.image = item.image
|
||||||
|
}
|
||||||
messageLabel.text = String(item.message.prefix(20))
|
messageLabel.text = String(item.message.prefix(20))
|
||||||
|
|
||||||
let isVoice = item.kind == .voice
|
let isVoice = item.kind == .voice
|
||||||
|
|
@ -468,6 +500,10 @@ final class ReceiveMessagePopView: UIView {
|
||||||
ReceiveMessageReportPopView.show()
|
ReceiveMessageReportPopView.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func tapClose() {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
private func startVoicePlayback() {
|
private func startVoicePlayback() {
|
||||||
if audioPlayer == nil {
|
if audioPlayer == nil {
|
||||||
prepareAudioPlayer()
|
prepareAudioPlayer()
|
||||||
|
|
@ -557,12 +593,29 @@ final class ReceiveMessagePopView: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func tapBurn() {
|
@objc private func tapBurn() {
|
||||||
guard !isBurning else { return }
|
guard !isBurning, let item else { return }
|
||||||
isBurning = true
|
|
||||||
burnButton.isUserInteractionEnabled = false
|
burnButton.isUserInteractionEnabled = false
|
||||||
voiceOverlay.isUserInteractionEnabled = false
|
voiceOverlay.isUserInteractionEnabled = false
|
||||||
stopVoicePlayback(reset: false)
|
stopVoicePlayback(reset: false)
|
||||||
playBurnAnimation()
|
|
||||||
|
let start = { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isBurning = true
|
||||||
|
self.playBurnAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let onRequestBurn else {
|
||||||
|
start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onRequestBurn(item) { success in
|
||||||
|
if success {
|
||||||
|
start()
|
||||||
|
} else {
|
||||||
|
self.burnButton.isUserInteractionEnabled = true
|
||||||
|
self.voiceOverlay.isUserInteractionEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func playBurnAnimation() {
|
private func playBurnAnimation() {
|
||||||
|
|
@ -836,6 +889,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
self.resetBurnState()
|
self.resetBurnState()
|
||||||
self.messages.removeAll()
|
self.messages.removeAll()
|
||||||
self.item = nil
|
self.item = nil
|
||||||
|
self.onRequestBurn = nil
|
||||||
self.onBurn = nil
|
self.onBurn = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -848,6 +902,7 @@ final class ReceiveMessagePopView: UIView {
|
||||||
self.overlayView.alpha = 0
|
self.overlayView.alpha = 0
|
||||||
self.letterView.alpha = 0
|
self.letterView.alpha = 0
|
||||||
self.reportButton.alpha = 0
|
self.reportButton.alpha = 0
|
||||||
|
self.closeButton.alpha = 0
|
||||||
self.letterView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
|
self.letterView.transform = CGAffineTransform(scaleX: 0.94, y: 0.94)
|
||||||
} completion: { _ in
|
} completion: { _ in
|
||||||
completion()
|
completion()
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ final class SOSPracticeView: UIView, UIScrollViewDelegate {
|
||||||
contentView.widthAnchor.constraint(equalTo: verticalScrollView.frameLayoutGuide.widthAnchor),
|
contentView.widthAnchor.constraint(equalTo: verticalScrollView.frameLayoutGuide.widthAnchor),
|
||||||
contentView.heightAnchor.constraint(greaterThanOrEqualTo: verticalScrollView.frameLayoutGuide.heightAnchor),
|
contentView.heightAnchor.constraint(greaterThanOrEqualTo: verticalScrollView.frameLayoutGuide.heightAnchor),
|
||||||
|
|
||||||
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 36),
|
imageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 94),
|
||||||
imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
imageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor),
|
||||||
imageView.widthAnchor.constraint(equalToConstant: 240),
|
imageView.widthAnchor.constraint(equalToConstant: 240),
|
||||||
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
|
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ final class SOSView: UIView {
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
|
||||||
setupUI()
|
setupUI()
|
||||||
setupActions()
|
setupActions()
|
||||||
configureContacts([])
|
configureContacts([])
|
||||||
|
|
@ -51,6 +50,7 @@ final class SOSView: UIView {
|
||||||
|
|
||||||
let hasCompletedPractice = Defaults[\.sosIsPracticeList].contains(AppContextManager.shared.userId)
|
let hasCompletedPractice = Defaults[\.sosIsPracticeList].contains(AppContextManager.shared.userId)
|
||||||
sosPracticeView.isHidden = hasCompletedPractice
|
sosPracticeView.isHidden = hasCompletedPractice
|
||||||
|
backgroundColor = hasCompletedPractice ? UIColor(hexStr: "#F9F9F9") : .white
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
@ -357,6 +357,7 @@ final class SOSView: UIView {
|
||||||
contactScrollView.leadingAnchor.constraint(equalTo: contactPanel.leadingAnchor, constant: 12),
|
contactScrollView.leadingAnchor.constraint(equalTo: contactPanel.leadingAnchor, constant: 12),
|
||||||
contactScrollView.trailingAnchor.constraint(equalTo: contactPanel.trailingAnchor, constant: -12),
|
contactScrollView.trailingAnchor.constraint(equalTo: contactPanel.trailingAnchor, constant: -12),
|
||||||
contactScrollView.bottomAnchor.constraint(equalTo: contactPanel.bottomAnchor, constant: -9),
|
contactScrollView.bottomAnchor.constraint(equalTo: contactPanel.bottomAnchor, constant: -9),
|
||||||
|
contactScrollView.heightAnchor.constraint(equalToConstant: 46),
|
||||||
|
|
||||||
contactStack.topAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.topAnchor),
|
contactStack.topAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.topAnchor),
|
||||||
contactStack.leadingAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.leadingAnchor),
|
contactStack.leadingAnchor.constraint(equalTo: contactScrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
|
@ -570,9 +571,9 @@ private final class SOSEmergencyContactCard: UIView {
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = .white
|
backgroundColor = .white
|
||||||
layer.cornerRadius = 13
|
layer.cornerRadius = 14
|
||||||
layer.borderWidth = 0.5
|
layer.borderWidth = 0.5
|
||||||
layer.borderColor = UIColor(hexStr: "#E6E6E6").cgColor
|
layer.borderColor = UIColor(hexStr: "#EDEDED").cgColor
|
||||||
|
|
||||||
phoneBadgeView.contentMode = .scaleAspectFit
|
phoneBadgeView.contentMode = .scaleAspectFit
|
||||||
addSubview(phoneBadgeView)
|
addSubview(phoneBadgeView)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
//
|
||||||
|
// LockDistractPermissionPopView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
|
|
||||||
|
final class LockDistractPermissionPopView: UIView {
|
||||||
|
|
||||||
|
private static let shared = LockDistractPermissionPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||||
|
|
||||||
|
private var disposeBag = DisposeBag()
|
||||||
|
|
||||||
|
static func show() {
|
||||||
|
guard let superView = kKeyWindow else { return }
|
||||||
|
let pop = LockDistractPermissionPopView.shared
|
||||||
|
if pop.superview != nil {
|
||||||
|
pop.removeFromSuperview()
|
||||||
|
}
|
||||||
|
pop.frame = superView.bounds
|
||||||
|
pop.alpha = 0
|
||||||
|
superView.addSubview(pop)
|
||||||
|
superView.bringSubviewToFront(pop)
|
||||||
|
pop.layoutIfNeeded()
|
||||||
|
UIView.animate(withDuration: 0.25) {
|
||||||
|
pop.alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dismiss() {
|
||||||
|
guard shared.superview != nil else { return }
|
||||||
|
UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseIn]) {
|
||||||
|
shared.alpha = 0
|
||||||
|
} completion: { _ in
|
||||||
|
shared.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
bind()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
private func bind() {
|
||||||
|
knowBtn.rx.tap
|
||||||
|
.subscribe(onNext: { _ in
|
||||||
|
LockDistractPermissionPopView.dismiss()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
|
||||||
|
tap.delegate = self
|
||||||
|
addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapMask() {
|
||||||
|
LockDistractPermissionPopView.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(dimView)
|
||||||
|
addSubview(card)
|
||||||
|
addSubview(birdView)
|
||||||
|
card.addSubview(gradientView)
|
||||||
|
card.addSubview(titleLab)
|
||||||
|
card.addSubview(row1)
|
||||||
|
card.addSubview(row2)
|
||||||
|
card.addSubview(pathLab)
|
||||||
|
card.addSubview(knowBtn)
|
||||||
|
|
||||||
|
dimView.layoutChain.edges()
|
||||||
|
|
||||||
|
card.layoutChain
|
||||||
|
.centerY()
|
||||||
|
.edgesHorzontal(38)
|
||||||
|
|
||||||
|
birdView.layoutChain
|
||||||
|
.bottomToTopOfView(card, offset: 28)
|
||||||
|
.centerX()
|
||||||
|
.width(120)
|
||||||
|
.height(88)
|
||||||
|
|
||||||
|
gradientView.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(72)
|
||||||
|
|
||||||
|
titleLab.layoutChain
|
||||||
|
.top(36)
|
||||||
|
.centerX()
|
||||||
|
|
||||||
|
row1.layoutChain
|
||||||
|
.topToBottomOfView(titleLab, offset: 20)
|
||||||
|
.left(22)
|
||||||
|
.right(22)
|
||||||
|
|
||||||
|
row2.layoutChain
|
||||||
|
.topToBottomOfView(row1, offset: 14)
|
||||||
|
.left(22)
|
||||||
|
.right(22)
|
||||||
|
|
||||||
|
pathLab.layoutChain
|
||||||
|
.topToBottomOfView(row2, offset: 8)
|
||||||
|
.left(40)
|
||||||
|
.right(22)
|
||||||
|
|
||||||
|
knowBtn.layoutChain
|
||||||
|
.topToBottomOfView(pathLab, offset: 22)
|
||||||
|
.edgesHorzontal(20)
|
||||||
|
.height(48)
|
||||||
|
.bottom(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var dimView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor.black.withAlphaComponent(0.55)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var card: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .white
|
||||||
|
view.layer.cornerRadius = 28
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var gradientView: UIView = {
|
||||||
|
GradientHostView()
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var birdView: UIImageView = {
|
||||||
|
let iv = UIImageView(image: UIImage(named: "LockDistract/horn_bird"))
|
||||||
|
iv.contentMode = .scaleAspectFit
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var titleLab: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.text = "锁定权限说明"
|
||||||
|
lab.font = FontManager.boboBold(20)
|
||||||
|
lab.textColor = UIColor(hexStr: "#293445")
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var row1: UIView = {
|
||||||
|
makeBulletRow("需要Ta开启App配对权限,不然无法锁定")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var row2: UIView = {
|
||||||
|
makeBulletRow("权限开启位置")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var pathLab: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.text = "【我的】 – 【权限检测】 – 【配对设置】"
|
||||||
|
lab.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
|
lab.textColor = UIColor(hexStr: "#8A94A6")
|
||||||
|
lab.numberOfLines = 0
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var knowBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setTitle("我知道了", for: .normal)
|
||||||
|
btn.setTitleColor(.white, for: .normal)
|
||||||
|
btn.titleLabel?.font = FontManager.boboBold(16)
|
||||||
|
btn.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||||
|
btn.cornerRadius = 16
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
private func makeBulletRow(_ text: String) -> UIView {
|
||||||
|
let row = UIView()
|
||||||
|
let star = UIImageView(image: UIImage(named: "LockDistract/section_star"))
|
||||||
|
star.contentMode = .scaleAspectFit
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.text = text
|
||||||
|
lab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
lab.textColor = UIColor(hexStr: "#293445")
|
||||||
|
lab.numberOfLines = 0
|
||||||
|
row.addSubview(star)
|
||||||
|
row.addSubview(lab)
|
||||||
|
star.layoutChain.left().top(2).width(12).height(12)
|
||||||
|
lab.layoutChain.leftToRightOfView(star, offset: 6).right().top().bottom()
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class GradientHostView: UIView {
|
||||||
|
override class var layerClass: AnyClass { CAGradientLayer.self }
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
guard let layer = layer as? CAGradientLayer else { return }
|
||||||
|
layer.colors = [
|
||||||
|
UIColor(hexStr: "#E7FFF2").cgColor,
|
||||||
|
UIColor.white.cgColor
|
||||||
|
]
|
||||||
|
layer.startPoint = CGPoint(x: 0.5, y: 0)
|
||||||
|
layer.endPoint = CGPoint(x: 0.5, y: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LockDistractPermissionPopView: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
!card.frame.contains(touch.location(in: self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ final class LockDistractVC: BaseViewController {
|
||||||
|
|
||||||
private var rootView: LockDistractView!
|
private var rootView: LockDistractView!
|
||||||
private var groupModel: GroupModel?
|
private var groupModel: GroupModel?
|
||||||
|
private var activeGroupKey: String = ""
|
||||||
private var members: [GroupMemberModel] = []
|
private var members: [GroupMemberModel] = []
|
||||||
private var selectedIndex = 0
|
private var selectedIndex = 0
|
||||||
|
|
||||||
|
|
@ -52,9 +53,9 @@ final class LockDistractVC: BaseViewController {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
// 未获取 / 锁定:本期不接业务
|
// 未获取 / 锁定:本期不接业务
|
||||||
rootView.notFetchedView.rx.tapGesture
|
rootView.notFetchedView.rx.tap
|
||||||
.subscribe(onNext: { _ in
|
.subscribe(onNext: { _ in
|
||||||
/// TODO
|
LockDistractPermissionPopView.show()
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,13 +63,20 @@ final class LockDistractVC: BaseViewController {
|
||||||
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
||||||
guard let self, let model = response.model else { return }
|
guard let self, let model = response.model else { return }
|
||||||
self.groupModel = model
|
self.groupModel = model
|
||||||
guard !model.default_group_key.isEmpty else {
|
self.activeGroupKey = model.default_group_key
|
||||||
self.applyMembers([])
|
self.updateGroupName()
|
||||||
|
self.loadUsers(for: self.activeGroupKey)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadUsers(for groupKey: String) {
|
||||||
|
guard !groupKey.isEmpty else {
|
||||||
|
applyMembers([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GroupService.groupUsers(groupKey: model.default_group_key)
|
GroupService.groupUsers(groupKey: groupKey)
|
||||||
.subscribe(onNext: { [weak self] response in
|
.subscribe(onNext: { [weak self] response in
|
||||||
guard response.isValid(for: model.default_group_key) else {
|
guard response.isValid(for: groupKey) else {
|
||||||
DLToast.showError(text: response.message ?? "获取圈子成员失败")
|
DLToast.showError(text: response.message ?? "获取圈子成员失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -76,8 +84,7 @@ final class LockDistractVC: BaseViewController {
|
||||||
}, onError: { error in
|
}, onError: { error in
|
||||||
DLToast.showError(text: error.localizedDescription)
|
DLToast.showError(text: error.localizedDescription)
|
||||||
})
|
})
|
||||||
.disposed(by: self.disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}).disposed(by: disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyMembers(_ members: [GroupMemberModel]) {
|
private func applyMembers(_ members: [GroupMemberModel]) {
|
||||||
|
|
@ -97,10 +104,18 @@ final class LockDistractVC: BaseViewController {
|
||||||
private func refreshMember() {
|
private func refreshMember() {
|
||||||
guard members.indices.contains(selectedIndex) else {
|
guard members.indices.contains(selectedIndex) else {
|
||||||
rootView.configureMember(name: " ", avatar: nil)
|
rootView.configureMember(name: " ", avatar: nil)
|
||||||
|
rootView.configureLocked(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let m = members[selectedIndex]
|
let m = members[selectedIndex]
|
||||||
rootView.configureMember(name: m.nick_name, avatar: m.userIcon)
|
let name = m.remark.isEmpty ? m.nick_name : m.remark
|
||||||
|
rootView.configureMember(name: name, avatar: m.userIcon)
|
||||||
|
rootView.configureLocked(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateGroupName() {
|
||||||
|
let name = groupModel?.groups.first(where: { $0.group_key == activeGroupKey })?.name ?? ""
|
||||||
|
rootView.setGroupName(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func switchGroup() {
|
private func switchGroup() {
|
||||||
|
|
@ -108,14 +123,15 @@ final class LockDistractVC: BaseViewController {
|
||||||
loadGroupMembers()
|
loadGroupMembers()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GroupListPopView.show(groupModel: groupModel, currentMembers: members) { [weak self] groupKey in
|
GroupListPopView.show(
|
||||||
guard let self, let key = groupKey else { return }
|
groupModel: groupModel,
|
||||||
GroupService.operate(opType: "setdefault", requestData: ["group_key": key])
|
currentMembers: members,
|
||||||
.subscribe(onNext: { [weak self] _ in
|
selectedKey: activeGroupKey
|
||||||
NotificationCenter.default.post(name: .RefreshGroupInfoNotification, object: nil)
|
) { [weak self] groupKey in
|
||||||
self?.loadGroupMembers()
|
guard let self, let key = groupKey, key != self.activeGroupKey else { return }
|
||||||
})
|
self.activeGroupKey = key
|
||||||
.disposed(by: self.disposeBag)
|
self.updateGroupName()
|
||||||
|
self.loadUsers(for: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,29 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
var disposeBag = DisposeBag()
|
var disposeBag = DisposeBag()
|
||||||
|
|
||||||
let wallpaperNames = [
|
let lockIconNames = [
|
||||||
"LockDistract/wallpaper_1",
|
"LockDistract/lock_icon_1",
|
||||||
"LockDistract/wallpaper_2",
|
"LockDistract/lock_icon_2",
|
||||||
"LockDistract/wallpaper_3"
|
"LockDistract/lock_icon_3",
|
||||||
|
"LockDistract/lock_icon_4",
|
||||||
|
"LockDistract/lock_icon_5"
|
||||||
]
|
]
|
||||||
|
|
||||||
private(set) var selectedWallpaperIndex = 0
|
private(set) var selectedLockIconIndex = 0
|
||||||
|
private(set) var selectedAppIndexes: Set<Int> = []
|
||||||
|
private(set) var isLocked = false
|
||||||
private let copyMaxLength = 20
|
private let copyMaxLength = 20
|
||||||
|
|
||||||
// MARK: - Init
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
setupUI()
|
setupUI()
|
||||||
setupCopyLimit()
|
setupCopyLimit()
|
||||||
|
applyLockedState()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
// MARK: - Public
|
|
||||||
|
|
||||||
func configureMember(name: String, avatar: UIImage?) {
|
func configureMember(name: String, avatar: UIImage?) {
|
||||||
memberNameLab.text = name.isEmpty ? " " : name
|
memberNameLab.text = name.isEmpty ? " " : name
|
||||||
memberAvatar.image = avatar ?? UIImage(named: "Common/default_avatar")
|
memberAvatar.image = avatar ?? UIImage(named: "Common/default_avatar")
|
||||||
|
|
@ -42,69 +43,84 @@ final class LockDistractView: UIView {
|
||||||
todayCountLab.text = "\(count)"
|
todayCountLab.text = "\(count)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - UI
|
func setGroupName(_ name: String) {
|
||||||
|
let title = name.isEmpty ? "切换圈子" : " \(name) "
|
||||||
|
switchGroupBtn.setTitle(title, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configureLocked(_ locked: Bool) {
|
||||||
|
isLocked = locked
|
||||||
|
applyLockedState()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyLockedState() {
|
||||||
|
avatarLockView.isHidden = !isLocked
|
||||||
|
styleSectionTitle.isHidden = isLocked
|
||||||
|
styleCard.isHidden = isLocked
|
||||||
|
lockedEmptyView.isHidden = !isLocked
|
||||||
|
appCollectionView.reloadData()
|
||||||
|
tipLab.text = isLocked
|
||||||
|
? "已被锁定情况下,先解锁才能锁定哟~"
|
||||||
|
: "锁定功能只有在圈主的情况下才能使用哟~"
|
||||||
|
lockBtn.setTitle(isLocked ? "请求解锁" : "锁定", for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
private func setupUI() {
|
private func setupUI() {
|
||||||
addSubview(navBgView)
|
|
||||||
addSubview(navView)
|
|
||||||
navView.addRightButton(switchGroupBtn)
|
|
||||||
addSubview(scrollView)
|
addSubview(scrollView)
|
||||||
scrollView.addSubview(contentView)
|
scrollView.addSubview(contentView)
|
||||||
addSubview(lockBtn)
|
addSubview(navView)
|
||||||
|
navView.addRightButton(switchGroupBtn)
|
||||||
|
addSubview(footerBar)
|
||||||
|
footerBar.addSubview(tipRow)
|
||||||
|
footerBar.addSubview(lockBtn)
|
||||||
|
|
||||||
contentView.addSubview(titleLab)
|
contentView.addSubview(navBgView)
|
||||||
contentView.addSubview(subtitleLab)
|
|
||||||
contentView.addSubview(heroPlaceholder)
|
|
||||||
contentView.addSubview(statsRow)
|
contentView.addSubview(statsRow)
|
||||||
statsRow.addArrangedSubview(memberCard)
|
statsRow.addArrangedSubview(memberCard)
|
||||||
statsRow.addArrangedSubview(todayCard)
|
statsRow.addArrangedSubview(todayCard)
|
||||||
contentView.addSubview(appsSectionTitle)
|
contentView.addSubview(appsSectionTitle)
|
||||||
contentView.addSubview(appsCard)
|
contentView.addSubview(appsCard)
|
||||||
appsCard.addSubview(appsHeaderRow)
|
appsCard.addSubview(appsHeaderRow)
|
||||||
appsCard.addSubview(unknownAppIcon)
|
appsCard.addSubview(appCollectionView)
|
||||||
contentView.addSubview(styleSectionTitle)
|
contentView.addSubview(bottomStack)
|
||||||
contentView.addSubview(styleCard)
|
bottomStack.addArrangedSubview(styleSectionTitle)
|
||||||
addSubview(tipRow)
|
bottomStack.addArrangedSubview(styleCard)
|
||||||
|
bottomStack.addArrangedSubview(lockedEmptyView)
|
||||||
navBgView.layoutChain
|
|
||||||
.edges(excludingEdge: .bottom)
|
|
||||||
.heightToWidth(160/375)
|
|
||||||
|
|
||||||
navView.layoutChain
|
navView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
.height(kNaviHeight)
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
footerBar.layoutChain
|
||||||
|
.edgesHorzontal()
|
||||||
|
.bottom()
|
||||||
|
|
||||||
lockBtn.layoutChain
|
lockBtn.layoutChain
|
||||||
.bottom(kSafeBottomMargin + 0)
|
.bottom(kSafeBottomMargin)
|
||||||
.edgesHorzontal(30)
|
.edgesHorzontal(30)
|
||||||
.height(56)
|
.height(56)
|
||||||
|
|
||||||
|
tipRow.layoutChain
|
||||||
|
.centerX()
|
||||||
|
.bottomToTopOfView(lockBtn, offset: -8)
|
||||||
|
.top(10)
|
||||||
|
|
||||||
scrollView.layoutChain
|
scrollView.layoutChain
|
||||||
.topToBottomOfView(navView)
|
.top()
|
||||||
.edgesHorzontal()
|
.edgesHorzontal()
|
||||||
.bottomToTopOfView(tipRow, offset: -12)
|
.bottomToTopOfView(footerBar)
|
||||||
|
|
||||||
contentView.layoutChain
|
contentView.layoutChain
|
||||||
.edges()
|
.edges()
|
||||||
.widthToView(scrollView)
|
.widthToView(scrollView)
|
||||||
|
|
||||||
titleLab.layoutChain
|
navBgView.layoutChain
|
||||||
.top(8)
|
.top()
|
||||||
.left(20)
|
.edgesHorzontal()
|
||||||
|
.heightToWidth(200.0 / 375.0)
|
||||||
subtitleLab.layoutChain
|
|
||||||
.topToBottomOfView(titleLab, offset: 6)
|
|
||||||
.left(20)
|
|
||||||
.right(140)
|
|
||||||
|
|
||||||
heroPlaceholder.layoutChain
|
|
||||||
.top(0)
|
|
||||||
.right(16)
|
|
||||||
.width(120)
|
|
||||||
.height(100)
|
|
||||||
|
|
||||||
statsRow.layoutChain
|
statsRow.layoutChain
|
||||||
.topToBottomOfView(subtitleLab, offset: 20)
|
.topToBottomOfView(navBgView, offset: -48)
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
.height(120)
|
.height(120)
|
||||||
|
|
||||||
|
|
@ -119,26 +135,23 @@ final class LockDistractView: UIView {
|
||||||
appsHeaderRow.layoutChain
|
appsHeaderRow.layoutChain
|
||||||
.top(16)
|
.top(16)
|
||||||
.edgesHorzontal(14)
|
.edgesHorzontal(14)
|
||||||
|
.height(28)
|
||||||
|
|
||||||
unknownAppIcon.layoutChain
|
appCollectionView.layoutChain
|
||||||
.topToBottomOfView(appsHeaderRow, offset: 14)
|
.topToBottomOfView(appsHeaderRow, offset: 12)
|
||||||
.left(14)
|
.edgesHorzontal(8)
|
||||||
.width(56)
|
.height(78)
|
||||||
.height(56)
|
|
||||||
.bottom(16)
|
.bottom(16)
|
||||||
|
|
||||||
styleSectionTitle.layoutChain
|
bottomStack.layoutChain
|
||||||
.topToBottomOfView(appsCard, offset: 22)
|
.topToBottomOfView(appsCard, offset: 22)
|
||||||
.left(20)
|
|
||||||
|
|
||||||
styleCard.layoutChain
|
|
||||||
.topToBottomOfView(styleSectionTitle, offset: 12)
|
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
.bottom(20)
|
.bottom(24)
|
||||||
|
|
||||||
tipRow.layoutChain
|
styleSectionTitle.layoutChain.left(4)
|
||||||
|
lockedEmptyView.layoutChain
|
||||||
.centerX()
|
.centerX()
|
||||||
.bottomToTopOfView(lockBtn, offset: -6)
|
.height(180)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupCopyLimit() {
|
private func setupCopyLimit() {
|
||||||
|
|
@ -154,13 +167,13 @@ final class LockDistractView: UIView {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Views
|
// MARK: - Views
|
||||||
|
|
||||||
lazy var navBgView: UIImageView = {
|
lazy var navBgView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.image = UIImage(named: "Common/navBar_bg_2")
|
iv.image = UIImage(named: "LockDistract/header_hero")
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
|
iv.clipsToBounds = true
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -168,10 +181,16 @@ final class LockDistractView: UIView {
|
||||||
BaseNavigationView(title: "")
|
BaseNavigationView(title: "")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var footerBar: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
lazy var switchGroupBtn: UIButton = {
|
lazy var switchGroupBtn: UIButton = {
|
||||||
let btn = UIButton(type: .custom)
|
let btn = UIButton(type: .custom)
|
||||||
btn.backgroundColor = .white.withAlphaComponent(0.9)
|
btn.backgroundColor = .white.withAlphaComponent(0.92)
|
||||||
btn.setTitle(" 切换圈子 ", for: .normal)
|
btn.setImage(UIImage(named: "Home/group_name_icon"), for: .normal)
|
||||||
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
btn.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
btn.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
|
btn.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
|
||||||
btn.cornerRadius = 8
|
btn.cornerRadius = 8
|
||||||
|
|
@ -181,6 +200,7 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
lazy var scrollView: UIScrollView = {
|
lazy var scrollView: UIScrollView = {
|
||||||
let sv = UIScrollView()
|
let sv = UIScrollView()
|
||||||
|
sv.contentInsetAdjustmentBehavior = .never
|
||||||
sv.showsVerticalScrollIndicator = false
|
sv.showsVerticalScrollIndicator = false
|
||||||
sv.alwaysBounceVertical = true
|
sv.alwaysBounceVertical = true
|
||||||
sv.keyboardDismissMode = .onDrag
|
sv.keyboardDismissMode = .onDrag
|
||||||
|
|
@ -189,29 +209,6 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
lazy var contentView = UIView()
|
lazy var contentView = UIView()
|
||||||
|
|
||||||
lazy var titleLab: UILabel = {
|
|
||||||
let lab = UILabel()
|
|
||||||
lab.text = "锁住分心"
|
|
||||||
lab.font = FontManager.boboBold(30)
|
|
||||||
lab.textColor = UIColor(hexStr: "#293445")
|
|
||||||
return lab
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var subtitleLab: UILabel = {
|
|
||||||
let lab = UILabel()
|
|
||||||
lab.text = "减少干扰,只专注重要的事"
|
|
||||||
lab.font = .systemFont(ofSize: 13, weight: .medium)
|
|
||||||
lab.textColor = UIColor(hexStr: "#8A94A6")
|
|
||||||
lab.numberOfLines = 2
|
|
||||||
return lab
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var heroPlaceholder: UIView = {
|
|
||||||
let view = UIView()
|
|
||||||
view.backgroundColor = .clear
|
|
||||||
return view
|
|
||||||
}()
|
|
||||||
|
|
||||||
lazy var statsRow: UIStackView = {
|
lazy var statsRow: UIStackView = {
|
||||||
let stack = UIStackView()
|
let stack = UIStackView()
|
||||||
stack.axis = .horizontal
|
stack.axis = .horizontal
|
||||||
|
|
@ -227,6 +224,7 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
view.addSubview(memberPrevBtn)
|
view.addSubview(memberPrevBtn)
|
||||||
view.addSubview(memberAvatar)
|
view.addSubview(memberAvatar)
|
||||||
|
view.addSubview(avatarLockView)
|
||||||
view.addSubview(memberNextBtn)
|
view.addSubview(memberNextBtn)
|
||||||
view.addSubview(memberNameChip)
|
view.addSubview(memberNameChip)
|
||||||
memberNameChip.addSubview(memberNameLab)
|
memberNameChip.addSubview(memberNameLab)
|
||||||
|
|
@ -237,17 +235,23 @@ final class LockDistractView: UIView {
|
||||||
.width(56)
|
.width(56)
|
||||||
.height(56)
|
.height(56)
|
||||||
|
|
||||||
|
avatarLockView.layoutChain
|
||||||
|
.topToView(memberAvatar)
|
||||||
|
.leftToView(memberAvatar)
|
||||||
|
.rightToView(memberAvatar)
|
||||||
|
.bottomToView(memberAvatar)
|
||||||
|
|
||||||
memberPrevBtn.layoutChain
|
memberPrevBtn.layoutChain
|
||||||
.centerY(memberAvatar)
|
.centerY(memberAvatar)
|
||||||
.left(8)
|
.rightToLeftOfView(memberAvatar, offset: -18)
|
||||||
.width(28)
|
.width(16)
|
||||||
.height(28)
|
.height(16)
|
||||||
|
|
||||||
memberNextBtn.layoutChain
|
memberNextBtn.layoutChain
|
||||||
.centerY(memberAvatar)
|
.centerY(memberAvatar)
|
||||||
.right(8)
|
.leftToRightOfView(memberAvatar, offset: 18)
|
||||||
.width(28)
|
.width(16)
|
||||||
.height(28)
|
.height(16)
|
||||||
|
|
||||||
memberNameChip.layoutChain
|
memberNameChip.layoutChain
|
||||||
.topToBottomOfView(memberAvatar, offset: 10)
|
.topToBottomOfView(memberAvatar, offset: 10)
|
||||||
|
|
@ -269,7 +273,16 @@ final class LockDistractView: UIView {
|
||||||
let iv = UIImageView(image: UIImage(named: "Common/default_avatar"))
|
let iv = UIImageView(image: UIImage(named: "Common/default_avatar"))
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFill
|
||||||
iv.clipsToBounds = true
|
iv.clipsToBounds = true
|
||||||
iv.cornerRadius = 16
|
iv.cornerRadius = 12
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var avatarLockView: UIImageView = {
|
||||||
|
let iv = UIImageView(image: UIImage(named: "LockDistract/avatar_lock"))
|
||||||
|
iv.contentMode = .scaleAspectFill
|
||||||
|
iv.clipsToBounds = true
|
||||||
|
iv.cornerRadius = 12
|
||||||
|
iv.isHidden = true
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -291,16 +304,16 @@ final class LockDistractView: UIView {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var memberPrevBtn: UIButton = {
|
lazy var memberPrevBtn: UIButton = {
|
||||||
let btn = UIButton(type: .system)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setImage(UIImage(systemName: "chevron.left"), for: .normal)
|
btn.setImage(UIImage(named: "LockDistract/member_prev"), for: .normal)
|
||||||
btn.tintColor = UIColor(hexStr: "#9CA3AF")
|
btn.extendEdgeInsets = UIEdgeInsets(top: 50, left: 30, bottom: 50, right: 18)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var memberNextBtn: UIButton = {
|
lazy var memberNextBtn: UIButton = {
|
||||||
let btn = UIButton(type: .system)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setImage(UIImage(systemName: "chevron.right"), for: .normal)
|
btn.setImage(UIImage(named: "LockDistract/member_next"), for: .normal)
|
||||||
btn.tintColor = UIColor(hexStr: "#9CA3AF")
|
btn.extendEdgeInsets = UIEdgeInsets(top: 50, left: 18, bottom: 50, right: 30)
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -371,18 +384,20 @@ final class LockDistractView: UIView {
|
||||||
return row
|
return row
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var notFetchedView: UIView = {
|
lazy var notFetchedView: UIButton = {
|
||||||
let view = UIView()
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.extendEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 4)
|
||||||
|
|
||||||
let warn = UIView()
|
let warn = UIView()
|
||||||
|
warn.isUserInteractionEnabled = false
|
||||||
warn.backgroundColor = UIColor(hexStr: "#FA5151")
|
warn.backgroundColor = UIColor(hexStr: "#FA5151")
|
||||||
warn.cornerRadius = 5
|
warn.cornerRadius = 5
|
||||||
let warnText = UILabel()
|
let warnText = UILabel()
|
||||||
warnText.text = "!"
|
warnText.text = "!"
|
||||||
warnText.textColor = .white
|
warnText.textColor = .white
|
||||||
warnText.font = .systemFont(ofSize: 8)
|
warnText.font = .systemFont(ofSize: 8)
|
||||||
|
warnText.isUserInteractionEnabled = false
|
||||||
view.addSubview(warn)
|
btn.addSubview(warn)
|
||||||
warn.addSubview(warnText)
|
warn.addSubview(warnText)
|
||||||
warnText.layoutChain.centerX().centerY()
|
warnText.layoutChain.centerX().centerY()
|
||||||
|
|
||||||
|
|
@ -390,35 +405,54 @@ final class LockDistractView: UIView {
|
||||||
notFetchedText.text = "未获取"
|
notFetchedText.text = "未获取"
|
||||||
notFetchedText.textColor = UIColor(hexStr: "#293445")
|
notFetchedText.textColor = UIColor(hexStr: "#293445")
|
||||||
notFetchedText.font = .systemFont(ofSize: 12, weight: .medium)
|
notFetchedText.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
view.addSubview(notFetchedText)
|
notFetchedText.isUserInteractionEnabled = false
|
||||||
|
btn.addSubview(notFetchedText)
|
||||||
|
|
||||||
let arrow = UIImageView(image: UIImage(named: "LockDistract/arrow_right"))
|
let arrow = UIImageView(image: UIImage(named: "LockDistract/arrow_right"))
|
||||||
|
arrow.isUserInteractionEnabled = false
|
||||||
|
btn.addSubview(arrow)
|
||||||
|
|
||||||
view.addSubview(arrow)
|
warn.layoutChain
|
||||||
view.addSubview(notFetchedText)
|
.left()
|
||||||
|
.centerY()
|
||||||
|
.width(10).height(10)
|
||||||
|
|
||||||
|
notFetchedText.layoutChain
|
||||||
|
.leftToRightOfView(warn, offset: 4)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
arrow.layoutChain
|
arrow.layoutChain
|
||||||
.right(14)
|
.leftToRightOfView(notFetchedText, offset: 2)
|
||||||
|
.right()
|
||||||
.width(10)
|
.width(10)
|
||||||
.heightToWidth(1)
|
.heightToWidth(1)
|
||||||
.centerY()
|
.centerY()
|
||||||
|
|
||||||
notFetchedText.layoutChain
|
return btn
|
||||||
.rightToLeftOfView(arrow)
|
|
||||||
.centerY()
|
|
||||||
|
|
||||||
warn.layoutChain
|
|
||||||
.rightToLeftOfView(notFetchedText, offset: -4)
|
|
||||||
.centerY()
|
|
||||||
.width(10).height(10)
|
|
||||||
|
|
||||||
return view
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var unknownAppIcon: UIImageView = {
|
lazy var appCollectionView: UICollectionView = {
|
||||||
let iv = UIImageView(image: UIImage(named: "LockDistract/app_unknown"))
|
let layout = UICollectionViewFlowLayout()
|
||||||
iv.contentMode = .scaleAspectFit
|
layout.scrollDirection = .horizontal
|
||||||
return iv
|
layout.itemSize = CGSize(width: 56, height: 78)
|
||||||
|
layout.minimumLineSpacing = 12
|
||||||
|
layout.sectionInset = UIEdgeInsets(top: 0, left: 6, bottom: 0, right: 6)
|
||||||
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
cv.backgroundColor = .clear
|
||||||
|
cv.showsHorizontalScrollIndicator = false
|
||||||
|
cv.register(LockDistractAppCell.self)
|
||||||
|
cv.dataSource = self
|
||||||
|
cv.delegate = self
|
||||||
|
cv.tag = 1
|
||||||
|
return cv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var bottomStack: UIStackView = {
|
||||||
|
let stack = UIStackView()
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.alignment = .fill
|
||||||
|
stack.spacing = 12
|
||||||
|
return stack
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var styleSectionTitle: UIView = {
|
lazy var styleSectionTitle: UIView = {
|
||||||
|
|
@ -431,14 +465,14 @@ final class LockDistractView: UIView {
|
||||||
card.cornerRadius = 26
|
card.cornerRadius = 26
|
||||||
|
|
||||||
let wallLab = UILabel()
|
let wallLab = UILabel()
|
||||||
wallLab.text = "锁定壁纸图片"
|
wallLab.text = "锁定图标"
|
||||||
wallLab.font = .systemFont(ofSize: 13, weight: .medium)
|
wallLab.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
wallLab.textColor = UIColor(hexStr: "#8A94A6")
|
wallLab.textColor = UIColor(hexStr: "#8A94A6")
|
||||||
card.addSubview(wallLab)
|
card.addSubview(wallLab)
|
||||||
wallLab.layoutChain.top(16).left(14)
|
wallLab.layoutChain.top(16).left(14)
|
||||||
|
|
||||||
card.addSubview(wallpaperCV)
|
card.addSubview(lockIconCV)
|
||||||
wallpaperCV.layoutChain
|
lockIconCV.layoutChain
|
||||||
.topToBottomOfView(wallLab, offset: 10)
|
.topToBottomOfView(wallLab, offset: 10)
|
||||||
.edgesHorzontal(14)
|
.edgesHorzontal(14)
|
||||||
.height(74)
|
.height(74)
|
||||||
|
|
@ -448,7 +482,7 @@ final class LockDistractView: UIView {
|
||||||
copyLab.font = .systemFont(ofSize: 13, weight: .medium)
|
copyLab.font = .systemFont(ofSize: 13, weight: .medium)
|
||||||
copyLab.textColor = UIColor(hexStr: "#8A94A6")
|
copyLab.textColor = UIColor(hexStr: "#8A94A6")
|
||||||
card.addSubview(copyLab)
|
card.addSubview(copyLab)
|
||||||
copyLab.layoutChain.topToBottomOfView(wallpaperCV, offset: 16).left(14)
|
copyLab.layoutChain.topToBottomOfView(lockIconCV, offset: 16).left(14)
|
||||||
|
|
||||||
let input = UIView()
|
let input = UIView()
|
||||||
input.backgroundColor = UIColor(hexStr: "#F5F6F8")
|
input.backgroundColor = UIColor(hexStr: "#F5F6F8")
|
||||||
|
|
@ -468,18 +502,18 @@ final class LockDistractView: UIView {
|
||||||
return card
|
return card
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var wallpaperCV: UICollectionView = {
|
lazy var lockIconCV: UICollectionView = {
|
||||||
let layout = UICollectionViewFlowLayout()
|
let layout = UICollectionViewFlowLayout()
|
||||||
layout.scrollDirection = .horizontal
|
layout.scrollDirection = .horizontal
|
||||||
layout.itemSize = CGSize(width: 50, height: 74)
|
layout.itemSize = CGSize(width: 56, height: 74)
|
||||||
layout.minimumLineSpacing = 12
|
layout.minimumLineSpacing = 10
|
||||||
layout.minimumInteritemSpacing = 0
|
|
||||||
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
cv.backgroundColor = .clear
|
cv.backgroundColor = .clear
|
||||||
cv.showsHorizontalScrollIndicator = false
|
cv.showsHorizontalScrollIndicator = false
|
||||||
cv.register(LockDistractWallpaperCell.self)
|
cv.register(LockDistractWallpaperCell.self)
|
||||||
cv.dataSource = self
|
cv.dataSource = self
|
||||||
cv.delegate = self
|
cv.delegate = self
|
||||||
|
cv.tag = 2
|
||||||
return cv
|
return cv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -501,19 +535,30 @@ final class LockDistractView: UIView {
|
||||||
return lab
|
return lab
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var tipRow: UIView = {
|
lazy var lockedEmptyView: UIImageView = {
|
||||||
let row = UIView()
|
let iv = UIImageView(image: UIImage(named: "LockDistract/locked_empty"))
|
||||||
let icon = UIImageView(image: UIImage(systemName: "info.circle.fill"))
|
iv.contentMode = .scaleAspectFit
|
||||||
icon.tintColor = UIColor(hexStr: "#16B3FF")
|
iv.isHidden = true
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tipLab: UILabel = {
|
||||||
let lab = UILabel()
|
let lab = UILabel()
|
||||||
lab.text = "锁定功能只有在圈主的情况下才能使用哟~"
|
lab.text = "锁定功能只有在圈主的情况下才能使用哟~"
|
||||||
lab.font = .systemFont(ofSize: 12, weight: .regular)
|
lab.font = .systemFont(ofSize: 12, weight: .regular)
|
||||||
lab.textColor = UIColor(hexStr: "#00ADFE")
|
lab.textColor = UIColor(hexStr: "#00ADFE")
|
||||||
lab.numberOfLines = 0
|
lab.numberOfLines = 0
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tipRow: UIView = {
|
||||||
|
let row = UIView()
|
||||||
|
let icon = UIImageView(image: UIImage(named: "LockDistract/info_circle"))
|
||||||
|
icon.contentMode = .scaleAspectFit
|
||||||
row.addSubview(icon)
|
row.addSubview(icon)
|
||||||
row.addSubview(lab)
|
row.addSubview(tipLab)
|
||||||
icon.layoutChain.left().top(1).width(14).height(14)
|
icon.layoutChain.left().centerY().width(14).height(14)
|
||||||
lab.layoutChain.leftToRightOfView(icon, offset: 6).right().top().bottom()
|
tipLab.layoutChain.leftToRightOfView(icon, offset: 6).right().top().bottom()
|
||||||
return row
|
return row
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -530,11 +575,7 @@ final class LockDistractView: UIView {
|
||||||
private func makeSectionTitle(_ text: String) -> UIView {
|
private func makeSectionTitle(_ text: String) -> UIView {
|
||||||
let row = UIView()
|
let row = UIView()
|
||||||
let star = UIImageView(image: UIImage(named: "LockDistract/section_star"))
|
let star = UIImageView(image: UIImage(named: "LockDistract/section_star"))
|
||||||
// 切图目前带黑底,先用 SF Symbol 保证白底可读;资源补透明图后可改回 LockDistract/section_star
|
star.contentMode = .scaleAspectFit
|
||||||
// let config = UIImage.SymbolConfiguration(pointSize: 12, weight: .bold)
|
|
||||||
// star.image = UIImage(systemName: "sparkle", withConfiguration: config)
|
|
||||||
// star.tintColor = UIColor(hexStr: "#16B3FF")
|
|
||||||
// star.contentMode = .scaleAspectFit
|
|
||||||
let lab = UILabel()
|
let lab = UILabel()
|
||||||
lab.text = text
|
lab.text = text
|
||||||
lab.font = .systemFont(ofSize: 16, weight: .bold)
|
lab.font = .systemFont(ofSize: 16, weight: .bold)
|
||||||
|
|
@ -550,19 +591,74 @@ final class LockDistractView: UIView {
|
||||||
|
|
||||||
extension LockDistractView: UICollectionViewDataSource, UICollectionViewDelegate {
|
extension LockDistractView: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
wallpaperNames.count
|
collectionView.tag == 1 ? 1 : lockIconNames.count
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
if collectionView.tag == 1 {
|
||||||
|
let cell = collectionView.dequeueReusableCell(for: indexPath) as LockDistractAppCell
|
||||||
|
cell.configure(
|
||||||
|
image: UIImage(named: "LockDistract/app_unknown"),
|
||||||
|
selected: selectedAppIndexes.contains(indexPath.item),
|
||||||
|
showsCheck: false
|
||||||
|
)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
let cell = collectionView.dequeueReusableCell(for: indexPath) as LockDistractWallpaperCell
|
let cell = collectionView.dequeueReusableCell(for: indexPath) as LockDistractWallpaperCell
|
||||||
cell.configure(imageName: wallpaperNames[indexPath.item],
|
cell.configure(
|
||||||
isSelected: indexPath.item == selectedWallpaperIndex)
|
imageName: lockIconNames[indexPath.item],
|
||||||
|
isSelected: indexPath.item == selectedLockIconIndex
|
||||||
|
)
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
selectedWallpaperIndex = indexPath.item
|
if collectionView.tag == 1 {
|
||||||
|
guard !isLocked, indexPath.item > 0 else { return }
|
||||||
|
if selectedAppIndexes.contains(indexPath.item) {
|
||||||
|
selectedAppIndexes.remove(indexPath.item)
|
||||||
|
} else {
|
||||||
|
selectedAppIndexes.insert(indexPath.item)
|
||||||
|
}
|
||||||
collectionView.reloadData()
|
collectionView.reloadData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedLockIconIndex = indexPath.item
|
||||||
|
collectionView.reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class LockDistractAppCell: UICollectionViewCell {
|
||||||
|
private let iconView: UIImageView = {
|
||||||
|
let iv = UIImageView()
|
||||||
|
iv.contentMode = .scaleAspectFit
|
||||||
|
iv.clipsToBounds = true
|
||||||
|
iv.cornerRadius = 12
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let checkBtn: UIButton = {
|
||||||
|
let btn = UIButton(type: .custom)
|
||||||
|
btn.setImage(UIImage(named: "Common/checkbox"), for: .normal)
|
||||||
|
btn.setImage(UIImage(named: "Common/checkbox_on"), for: .selected)
|
||||||
|
btn.isUserInteractionEnabled = false
|
||||||
|
return btn
|
||||||
|
}()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
contentView.addSubview(iconView)
|
||||||
|
contentView.addSubview(checkBtn)
|
||||||
|
iconView.layoutChain.top().centerX().width(48).height(48)
|
||||||
|
checkBtn.layoutChain.topToBottomOfView(iconView, offset: 6).centerX().width(16).height(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
func configure(image: UIImage?, selected: Bool, showsCheck: Bool) {
|
||||||
|
iconView.image = image
|
||||||
|
checkBtn.isSelected = selected
|
||||||
|
checkBtn.isHidden = !showsCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -576,9 +672,8 @@ final class LockDistractWallpaperCell: UICollectionViewCell {
|
||||||
|
|
||||||
private let imageView: UIImageView = {
|
private let imageView: UIImageView = {
|
||||||
let iv = UIImageView()
|
let iv = UIImageView()
|
||||||
iv.contentMode = .scaleAspectFill
|
iv.contentMode = .scaleAspectFit
|
||||||
iv.clipsToBounds = true
|
iv.clipsToBounds = true
|
||||||
iv.cornerRadius = 10
|
|
||||||
return iv
|
return iv
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -590,8 +685,8 @@ final class LockDistractWallpaperCell: UICollectionViewCell {
|
||||||
imageView.layoutChain
|
imageView.layoutChain
|
||||||
.centerY()
|
.centerY()
|
||||||
.centerX()
|
.centerX()
|
||||||
.width(40)
|
.width(48)
|
||||||
.height(40)
|
.height(48)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
@ -599,7 +694,7 @@ final class LockDistractWallpaperCell: UICollectionViewCell {
|
||||||
func configure(imageName: String, isSelected: Bool) {
|
func configure(imageName: String, isSelected: Bool) {
|
||||||
imageView.image = UIImage(named: imageName)
|
imageView.image = UIImage(named: imageName)
|
||||||
frameView.backgroundColor = UIColor(hexStr: isSelected ? "#EAF8FF" : "#F6F6F6")
|
frameView.backgroundColor = UIColor(hexStr: isSelected ? "#EAF8FF" : "#F6F6F6")
|
||||||
frameView.borderWidth = isSelected ? 1 : 0
|
frameView.borderWidth = isSelected ? 1.5 : 0
|
||||||
frameView.borderColor = UIColor(hexStr: isSelected ? "#00ADFE" : "")
|
frameView.borderColor = UIColor(hexStr: isSelected ? "#00ADFE" : "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,24 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
import RxSwift
|
||||||
|
|
||||||
final class PigeonMessageHistoryVC: BaseViewController {
|
final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
|
|
||||||
private let members: [GroupMemberModel]
|
private let groupKey: String
|
||||||
private var items: [PigeonHistoryItem] = []
|
private var items: [PigeonHistoryItem] = []
|
||||||
private var rootView: PigeonMessageHistoryView!
|
private var rootView: PigeonMessageHistoryView!
|
||||||
|
|
||||||
private var playingItemId: String?
|
private var playingItemId: String?
|
||||||
private var isVoicePlaying = false
|
private var isVoicePlaying = false
|
||||||
private var voiceElapsed: TimeInterval = 0
|
private var voiceElapsed: TimeInterval = 0
|
||||||
private var voiceDuration: TimeInterval = 10
|
private var voiceDuration: TimeInterval = 0
|
||||||
private var voiceTimer: Timer?
|
private var voiceTimer: Timer?
|
||||||
|
private var audioPlayer: AVAudioPlayer?
|
||||||
|
|
||||||
init(members: [GroupMemberModel]) {
|
init(groupKey: String) {
|
||||||
self.members = members
|
self.groupKey = groupKey
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,15 +36,10 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
items = Self.makeMockItems(members: members)
|
|
||||||
rootView.tableView.dataSource = self
|
rootView.tableView.dataSource = self
|
||||||
rootView.tableView.delegate = self
|
rootView.tableView.delegate = self
|
||||||
rootView.tableView.register(PigeonHistoryCell.self, forCellReuseIdentifier: PigeonHistoryCell.reuseId)
|
rootView.tableView.register(PigeonHistoryCell.self, forCellReuseIdentifier: PigeonHistoryCell.reuseId)
|
||||||
}
|
loadHistory()
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
|
@ -53,8 +51,26 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
navigationController?.popViewController(animated: true)
|
navigationController?.popViewController(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func loadHistory() {
|
||||||
|
DLToast.showLoading()
|
||||||
|
PigeonService.sent(groupKey: groupKey)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let self else { return }
|
||||||
|
self.items = response.list.map { $0.toHistoryItem() }
|
||||||
|
self.rootView.tableView.reloadData()
|
||||||
|
}, onError: { _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
private func togglePlayback(for item: PigeonHistoryItem) {
|
private func togglePlayback(for item: PigeonHistoryItem) {
|
||||||
guard item.kind == .voice else { return }
|
guard item.kind == .voice else { return }
|
||||||
|
guard item.mediaURL != nil else {
|
||||||
|
DLToast.show(text: "无法播放")
|
||||||
|
return
|
||||||
|
}
|
||||||
if playingItemId == item.id {
|
if playingItemId == item.id {
|
||||||
if isVoicePlaying {
|
if isVoicePlaying {
|
||||||
pauseVoicePlayback()
|
pauseVoicePlayback()
|
||||||
|
|
@ -70,19 +86,58 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
|
|
||||||
private func startVoicePlayback(item: PigeonHistoryItem) {
|
private func startVoicePlayback(item: PigeonHistoryItem) {
|
||||||
playingItemId = item.id
|
playingItemId = item.id
|
||||||
voiceDuration = max(1, item.duration)
|
if audioPlayer == nil || playingItemId != item.id {
|
||||||
|
preparePlayer(for: item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioPlayer?.play()
|
||||||
isVoicePlaying = true
|
isVoicePlaying = true
|
||||||
|
startTimer()
|
||||||
|
refreshVisiblePlayback()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preparePlayer(for item: PigeonHistoryItem) {
|
||||||
|
guard let url = item.mediaURL else { return }
|
||||||
|
DLToast.showLoading()
|
||||||
|
URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let self, self.playingItemId == item.id else { return }
|
||||||
|
guard let data, error == nil else {
|
||||||
|
DLToast.show(text: "无法播放")
|
||||||
|
self.playingItemId = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let player = try AVAudioPlayer(data: data)
|
||||||
|
player.prepareToPlay()
|
||||||
|
self.audioPlayer = player
|
||||||
|
self.voiceDuration = max(1, player.duration)
|
||||||
|
self.voiceElapsed = 0
|
||||||
|
player.play()
|
||||||
|
self.isVoicePlaying = true
|
||||||
|
self.startTimer()
|
||||||
|
self.refreshVisiblePlayback()
|
||||||
|
} catch {
|
||||||
|
DLToast.show(text: "无法播放")
|
||||||
|
self.playingItemId = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startTimer() {
|
||||||
voiceTimer?.invalidate()
|
voiceTimer?.invalidate()
|
||||||
let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in
|
let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in
|
||||||
self?.tickVoicePlayback()
|
self?.tickVoicePlayback()
|
||||||
}
|
}
|
||||||
RunLoop.main.add(timer, forMode: .common)
|
RunLoop.main.add(timer, forMode: .common)
|
||||||
voiceTimer = timer
|
voiceTimer = timer
|
||||||
refreshVisiblePlayback()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pauseVoicePlayback() {
|
private func pauseVoicePlayback() {
|
||||||
isVoicePlaying = false
|
isVoicePlaying = false
|
||||||
|
audioPlayer?.pause()
|
||||||
voiceTimer?.invalidate()
|
voiceTimer?.invalidate()
|
||||||
voiceTimer = nil
|
voiceTimer = nil
|
||||||
refreshVisiblePlayback()
|
refreshVisiblePlayback()
|
||||||
|
|
@ -92,6 +147,8 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
isVoicePlaying = false
|
isVoicePlaying = false
|
||||||
voiceTimer?.invalidate()
|
voiceTimer?.invalidate()
|
||||||
voiceTimer = nil
|
voiceTimer = nil
|
||||||
|
audioPlayer?.stop()
|
||||||
|
audioPlayer = nil
|
||||||
if reset {
|
if reset {
|
||||||
voiceElapsed = 0
|
voiceElapsed = 0
|
||||||
playingItemId = nil
|
playingItemId = nil
|
||||||
|
|
@ -100,9 +157,14 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tickVoicePlayback() {
|
private func tickVoicePlayback() {
|
||||||
|
if let player = audioPlayer {
|
||||||
|
voiceElapsed = player.currentTime
|
||||||
|
voiceDuration = max(player.duration, voiceElapsed)
|
||||||
|
} else {
|
||||||
voiceElapsed = min(voiceDuration, voiceElapsed + 0.05)
|
voiceElapsed = min(voiceDuration, voiceElapsed + 0.05)
|
||||||
|
}
|
||||||
refreshVisiblePlayback()
|
refreshVisiblePlayback()
|
||||||
guard voiceElapsed >= voiceDuration else { return }
|
guard voiceElapsed >= voiceDuration, voiceDuration > 0 else { return }
|
||||||
stopVoicePlayback(reset: true)
|
stopVoicePlayback(reset: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,70 +176,14 @@ final class PigeonMessageHistoryVC: BaseViewController {
|
||||||
}
|
}
|
||||||
let item = items[indexPath.row]
|
let item = items[indexPath.row]
|
||||||
let isCurrent = item.id == playingItemId
|
let isCurrent = item.id == playingItemId
|
||||||
|
let duration = isCurrent && voiceDuration > 0 ? voiceDuration : item.duration
|
||||||
historyCell.applyPlayback(
|
historyCell.applyPlayback(
|
||||||
isPlaying: isCurrent && isVoicePlaying,
|
isPlaying: isCurrent && isVoicePlaying,
|
||||||
elapsed: isCurrent ? voiceElapsed : 0,
|
elapsed: isCurrent ? voiceElapsed : 0,
|
||||||
duration: item.duration
|
duration: duration
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func makeMockItems(members: [GroupMemberModel]) -> [PigeonHistoryItem] {
|
|
||||||
let fallback = UIImage(named: "UserIcon/1")
|
|
||||||
let sender = AppContextManager.shared.avaterIcon.size == .zero
|
|
||||||
? fallback
|
|
||||||
: AppContextManager.shared.avaterIcon
|
|
||||||
var receivers = members
|
|
||||||
.filter { $0.user_id != AppContextManager.shared.userId }
|
|
||||||
.map { member -> UIImage in
|
|
||||||
member.userIcon.size == .zero ? (fallback ?? UIImage()) : member.userIcon
|
|
||||||
}
|
|
||||||
if receivers.isEmpty {
|
|
||||||
receivers = [fallback ?? UIImage()]
|
|
||||||
}
|
|
||||||
while receivers.count < 3 {
|
|
||||||
receivers.append(receivers[receivers.count % max(receivers.count, 1)])
|
|
||||||
}
|
|
||||||
|
|
||||||
let dateText = "8.14 周五"
|
|
||||||
let timeText = "下午 10:12"
|
|
||||||
let caption = "和红红火火恍恍惚惚或或或或或或或或"
|
|
||||||
return [
|
|
||||||
PigeonHistoryItem(
|
|
||||||
id: "image-1",
|
|
||||||
dateText: dateText,
|
|
||||||
timeText: timeText,
|
|
||||||
kind: .image,
|
|
||||||
caption: caption,
|
|
||||||
image: UIImage(named: "PigeonMessage/history_photo") ?? sender ?? fallback,
|
|
||||||
senderAvatar: sender,
|
|
||||||
receiverAvatars: [receivers[0]],
|
|
||||||
duration: 0
|
|
||||||
),
|
|
||||||
PigeonHistoryItem(
|
|
||||||
id: "voice-1",
|
|
||||||
dateText: dateText,
|
|
||||||
timeText: timeText,
|
|
||||||
kind: .voice,
|
|
||||||
caption: "",
|
|
||||||
image: nil,
|
|
||||||
senderAvatar: sender,
|
|
||||||
receiverAvatars: [receivers[0]],
|
|
||||||
duration: 10
|
|
||||||
),
|
|
||||||
PigeonHistoryItem(
|
|
||||||
id: "voice-2",
|
|
||||||
dateText: dateText,
|
|
||||||
timeText: timeText,
|
|
||||||
kind: .voice,
|
|
||||||
caption: "",
|
|
||||||
image: nil,
|
|
||||||
senderAvatar: sender,
|
|
||||||
receiverAvatars: Array(receivers.prefix(3)),
|
|
||||||
duration: 10
|
|
||||||
)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PigeonMessageHistoryVC: UITableViewDataSource, UITableViewDelegate {
|
extension PigeonMessageHistoryVC: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
enum PigeonHistoryKind {
|
enum PigeonHistoryKind {
|
||||||
case image
|
case image
|
||||||
|
|
@ -17,6 +18,7 @@ struct PigeonHistoryItem {
|
||||||
let kind: PigeonHistoryKind
|
let kind: PigeonHistoryKind
|
||||||
let caption: String
|
let caption: String
|
||||||
let image: UIImage?
|
let image: UIImage?
|
||||||
|
let mediaURL: URL?
|
||||||
let senderAvatar: UIImage?
|
let senderAvatar: UIImage?
|
||||||
let receiverAvatars: [UIImage]
|
let receiverAvatars: [UIImage]
|
||||||
let duration: TimeInterval
|
let duration: TimeInterval
|
||||||
|
|
@ -208,8 +210,15 @@ final class PigeonHistoryCell: UITableViewCell {
|
||||||
let isVoice = item.kind == .voice
|
let isVoice = item.kind == .voice
|
||||||
imageContainer.isHidden = isVoice
|
imageContainer.isHidden = isVoice
|
||||||
voiceBar.isHidden = !isVoice
|
voiceBar.isHidden = !isVoice
|
||||||
|
photoView.kf.cancelDownloadTask()
|
||||||
|
if let mediaURL = item.mediaURL, !isVoice {
|
||||||
|
photoView.kf.setImage(with: mediaURL, placeholder: nil)
|
||||||
|
} else {
|
||||||
photoView.image = item.image
|
photoView.image = item.image
|
||||||
captionLabel.text = "留言:\(String(item.caption.prefix(20)))"
|
}
|
||||||
|
let caption = item.caption.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
captionBackground.isHidden = caption.isEmpty
|
||||||
|
captionLabel.text = caption.isEmpty ? "" : "留言:\(String(caption.prefix(20)))"
|
||||||
applyPlayback(isPlaying: isPlaying, elapsed: elapsed, duration: item.duration)
|
applyPlayback(isPlaying: isPlaying, elapsed: elapsed, duration: item.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -413,7 +422,7 @@ final class PigeonHistoryAvatarStackView: UIView {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
senderView.contentMode = .scaleAspectFill
|
senderView.contentMode = .scaleAspectFill
|
||||||
senderView.clipsToBounds = true
|
senderView.clipsToBounds = true
|
||||||
senderView.layer.cornerRadius = 20
|
senderView.layer.cornerRadius = 14
|
||||||
senderView.layer.borderWidth = 2
|
senderView.layer.borderWidth = 2
|
||||||
senderView.layer.borderColor = UIColor.white.cgColor
|
senderView.layer.borderColor = UIColor.white.cgColor
|
||||||
arrowView.contentMode = .scaleAspectFit
|
arrowView.contentMode = .scaleAspectFit
|
||||||
|
|
@ -435,7 +444,7 @@ final class PigeonHistoryAvatarStackView: UIView {
|
||||||
let view = UIImageView(image: image)
|
let view = UIImageView(image: image)
|
||||||
view.contentMode = .scaleAspectFill
|
view.contentMode = .scaleAspectFill
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
view.layer.cornerRadius = 20
|
view.layer.cornerRadius = 14
|
||||||
view.layer.borderWidth = 2
|
view.layer.borderWidth = 2
|
||||||
view.layer.borderColor = UIColor.white.cgColor
|
view.layer.borderColor = UIColor.white.cgColor
|
||||||
addSubview(view)
|
addSubview(view)
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,15 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreLocation
|
|
||||||
import SwiftyUserDefaults
|
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
||||||
final class PigeonMessageVC: BaseViewController {
|
final class PigeonMessageVC: BaseViewController {
|
||||||
|
|
||||||
private var rootView: PigeonMessageView!
|
private var rootView: PigeonMessageView!
|
||||||
|
private var groupModel: GroupModel?
|
||||||
|
private var activeGroupKey: String = ""
|
||||||
private var members: [GroupMemberModel] = []
|
private var members: [GroupMemberModel] = []
|
||||||
private var selectedMember: GroupMemberModel?
|
private var selectedMembers: [GroupMemberModel] = []
|
||||||
private var selectedImage: UIImage?
|
private var selectedImage: UIImage?
|
||||||
|
|
||||||
private var audioRecorder: AVAudioRecorder?
|
private var audioRecorder: AVAudioRecorder?
|
||||||
|
|
@ -43,12 +43,11 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func bindActions() {
|
private func bindActions() {
|
||||||
|
rootView.switchGroupButton.addTarget(self, action: #selector(switchGroup), for: .touchUpInside)
|
||||||
rootView.historyButton.addTarget(self, action: #selector(showHistory), for: .touchUpInside)
|
rootView.historyButton.addTarget(self, action: #selector(showHistory), for: .touchUpInside)
|
||||||
rootView.changeRecipientButton.addTarget(
|
rootView.onSelectionChanged = { [weak self] members in
|
||||||
self,
|
self?.selectedMembers = members
|
||||||
action: #selector(changeRecipient),
|
}
|
||||||
for: .touchUpInside
|
|
||||||
)
|
|
||||||
rootView.addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside)
|
rootView.addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside)
|
||||||
rootView.deleteImageButton.addTarget(self, action: #selector(deleteImage), for: .touchUpInside)
|
rootView.deleteImageButton.addTarget(self, action: #selector(deleteImage), for: .touchUpInside)
|
||||||
rootView.playPauseButton.addTarget(self, action: #selector(togglePlayback), for: .touchUpInside)
|
rootView.playPauseButton.addTarget(self, action: #selector(togglePlayback), for: .touchUpInside)
|
||||||
|
|
@ -79,15 +78,26 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
DLToast.showError(text: response.message ?? "获取圈子信息失败")
|
DLToast.showError(text: response.message ?? "获取圈子信息失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard !model.default_group_key.isEmpty else {
|
self.groupModel = model
|
||||||
|
self.activeGroupKey = model.default_group_key
|
||||||
|
self.loadUsers(for: self.activeGroupKey, showLoading: false)
|
||||||
|
}, onError: { error in
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
self.applyMembers([])
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadUsers(for groupKey: String, showLoading: Bool) {
|
||||||
|
guard !groupKey.isEmpty else {
|
||||||
|
DLToast.dismiss()
|
||||||
|
applyMembers([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GroupService.groupUsers(groupKey: model.default_group_key)
|
if showLoading { DLToast.showLoading() }
|
||||||
|
GroupService.groupUsers(groupKey: groupKey)
|
||||||
.subscribe(onNext: { [weak self] response in
|
.subscribe(onNext: { [weak self] response in
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
guard response.isValid(for: model.default_group_key) else {
|
guard response.isValid(for: groupKey) else {
|
||||||
DLToast.showError(text: response.message ?? "获取圈子成员失败")
|
DLToast.showError(text: response.message ?? "获取圈子成员失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -96,80 +106,102 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
DLToast.showError(text: error.localizedDescription)
|
DLToast.showError(text: error.localizedDescription)
|
||||||
})
|
})
|
||||||
.disposed(by: self.disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}, onError: { error in
|
|
||||||
DLToast.dismiss()
|
|
||||||
DLToast.showError(text: error.localizedDescription)
|
|
||||||
}).disposed(by: disposeBag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyMembers(_ members: [GroupMemberModel]) {
|
private func applyMembers(_ members: [GroupMemberModel]) {
|
||||||
self.members = members
|
self.members = members
|
||||||
selectedMember = members.first
|
selectedMembers = []
|
||||||
refreshRecipient()
|
rootView.configureMembers(members, selectedIds: [])
|
||||||
}
|
|
||||||
|
|
||||||
private func refreshRecipient() {
|
|
||||||
rootView.configureRecipient(
|
|
||||||
selectedMember,
|
|
||||||
distanceText: distanceText(for: selectedMember)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func distanceText(for member: GroupMemberModel?) -> String {
|
|
||||||
guard let member,
|
|
||||||
let latitude = Defaults[\.currentLatitude],
|
|
||||||
let longitude = Defaults[\.currentLongitude] else {
|
|
||||||
return "与您相距 --"
|
|
||||||
}
|
|
||||||
let parts = member.last_position.components(separatedBy: ":")
|
|
||||||
guard parts.count >= 2,
|
|
||||||
let memberLatitude = Double(parts[0]),
|
|
||||||
let memberLongitude = Double(parts[1]) else {
|
|
||||||
return "与您相距 --"
|
|
||||||
}
|
|
||||||
let current = CLLocation(latitude: latitude, longitude: longitude)
|
|
||||||
let target = CLLocation(latitude: memberLatitude, longitude: memberLongitude)
|
|
||||||
let kilometers = current.distance(from: target) / 1000
|
|
||||||
if kilometers < 1 {
|
|
||||||
return "与您相距 \(Int(kilometers * 1000))m"
|
|
||||||
}
|
|
||||||
return String(format: "与您相距 %.1fkm", kilometers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func goBack() {
|
@objc private func goBack() {
|
||||||
navigationController?.popViewController(animated: true)
|
navigationController?.popViewController(animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func switchGroup() {
|
||||||
|
guard let groupModel else {
|
||||||
|
loadMembers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
GroupListPopView.show(
|
||||||
|
groupModel: groupModel,
|
||||||
|
currentMembers: members,
|
||||||
|
selectedKey: activeGroupKey
|
||||||
|
) { [weak self] groupKey in
|
||||||
|
guard let self, let key = groupKey, key != self.activeGroupKey else { return }
|
||||||
|
self.activeGroupKey = key
|
||||||
|
self.loadUsers(for: key, showLoading: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func showHistory() {
|
@objc private func showHistory() {
|
||||||
let historyVC = PigeonMessageHistoryVC(members: members)
|
let historyVC = PigeonMessageHistoryVC(groupKey: activeGroupKey)
|
||||||
navigationController?.pushViewController(historyVC, animated: true)
|
navigationController?.pushViewController(historyVC, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func changeRecipient() {
|
@objc private func selectImage() {
|
||||||
guard !members.isEmpty else {
|
PigeonUploadPopView.show(
|
||||||
DLToast.show(text: "暂无可选收件人")
|
onAlbum: { [weak self] in
|
||||||
return
|
self?.openAlbumPicker()
|
||||||
}
|
},
|
||||||
PigeonRecipientPickerView.show(
|
onCamera: { [weak self] in
|
||||||
members: members,
|
self?.openWatermarkCamera()
|
||||||
selectedId: selectedMember?.user_id ?? ""
|
|
||||||
) { [weak self] member in
|
|
||||||
self?.selectedMember = member
|
|
||||||
self?.refreshRecipient()
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func selectImage() {
|
private func openAlbumPicker() {
|
||||||
let picker = PhotoPickerVC(maxCount: 1)
|
let picker = PhotoPickerVC(maxCount: 1)
|
||||||
picker.onConfirm = { [weak self] images in
|
picker.onConfirm = { [weak self] images in
|
||||||
guard let image = images.first else { return }
|
guard let image = images.first else { return }
|
||||||
self?.selectedImage = image
|
self?.applySelectedImage(image)
|
||||||
self?.rootView.setSelectedImage(image)
|
|
||||||
}
|
}
|
||||||
present(picker, animated: true)
|
present(picker, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openWatermarkCamera() {
|
||||||
|
guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
|
||||||
|
DLToast.showError(text: "当前设备不支持拍照")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let manager = AuthorizeManager.manager(type: .camera) else { return }
|
||||||
|
switch manager.authorizeStatus() {
|
||||||
|
case .authorized:
|
||||||
|
pushWatermarkCamera()
|
||||||
|
case .notDetermined:
|
||||||
|
manager.authorize { [weak self] status in
|
||||||
|
if status == .authorized {
|
||||||
|
self?.pushWatermarkCamera()
|
||||||
|
} else if status != .authorized {
|
||||||
|
self?.showCameraDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .restricted, .denied:
|
||||||
|
showCameraDenied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pushWatermarkCamera() {
|
||||||
|
let cameraVC = PigeonWatermarkCameraVC()
|
||||||
|
cameraVC.onPhoto = { [weak self] image in
|
||||||
|
self?.applySelectedImage(image)
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(cameraVC, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applySelectedImage(_ image: UIImage) {
|
||||||
|
selectedImage = image
|
||||||
|
rootView.setSelectedImage(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showCameraDenied() {
|
||||||
|
Permission.openAppSetting(
|
||||||
|
title: "无法使用相机",
|
||||||
|
message: "请在系统设置中允许访问相机"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func deleteImage() {
|
@objc private func deleteImage() {
|
||||||
selectedImage = nil
|
selectedImage = nil
|
||||||
rootView.setSelectedImage(nil)
|
rootView.setSelectedImage(nil)
|
||||||
|
|
@ -248,7 +280,7 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
||||||
recorder.isMeteringEnabled = true
|
recorder.isMeteringEnabled = true
|
||||||
recorder.prepareToRecord()
|
recorder.prepareToRecord()
|
||||||
recorder.record(forDuration: 60)
|
recorder.record(forDuration: PigeonMessageView.maxVoiceDuration + 0.25)
|
||||||
|
|
||||||
recordURL = url
|
recordURL = url
|
||||||
audioRecorder = recorder
|
audioRecorder = recorder
|
||||||
|
|
@ -271,7 +303,7 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
let level = pow(10, CGFloat(recorder.averagePower(forChannel: 0)) / 38)
|
let level = pow(10, CGFloat(recorder.averagePower(forChannel: 0)) / 38)
|
||||||
self.rootView.pushMeterLevel(level)
|
self.rootView.pushMeterLevel(level)
|
||||||
self.rootView.setVoiceDuration(recorder.currentTime, hasRecording: false)
|
self.rootView.setVoiceDuration(recorder.currentTime, hasRecording: false)
|
||||||
if recorder.currentTime >= 59.95 {
|
if recorder.currentTime >= PigeonMessageView.maxVoiceDuration {
|
||||||
self.isPressingRecord = false
|
self.isPressingRecord = false
|
||||||
self.stopRecording(cancel: false)
|
self.stopRecording(cancel: false)
|
||||||
}
|
}
|
||||||
|
|
@ -303,7 +335,8 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recordedDuration = min(60, duration)
|
let maxDuration = PigeonMessageView.maxVoiceDuration
|
||||||
|
recordedDuration = duration + 0.05 >= maxDuration ? maxDuration : min(maxDuration, duration)
|
||||||
rootView.setVoiceDuration(recordedDuration, hasRecording: true)
|
rootView.setVoiceDuration(recordedDuration, hasRecording: true)
|
||||||
rootView.setPlaying(false)
|
rootView.setPlaying(false)
|
||||||
}
|
}
|
||||||
|
|
@ -363,28 +396,89 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sendMessage() {
|
@objc private func sendMessage() {
|
||||||
guard selectedMember != nil else {
|
guard !selectedMembers.isEmpty else {
|
||||||
DLToast.show(text: "请先选择收件人")
|
DLToast.show(text: "请先选择收件人")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard !activeGroupKey.isEmpty else {
|
||||||
|
DLToast.show(text: "请先选择圈子")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload: Observable<String>
|
||||||
|
let msgType: Int
|
||||||
switch rootView.mode {
|
switch rootView.mode {
|
||||||
case .image:
|
case .image:
|
||||||
guard selectedImage != nil else {
|
guard let image = selectedImage,
|
||||||
|
let jpeg = compressedPigeonJPEG(from: image) else {
|
||||||
DLToast.show(text: "请先选择一张图片")
|
DLToast.show(text: "请先选择一张图片")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectedImage = nil
|
upload = UploadService.upload(jpeg, kind: .jpeg, scene: "pigeon")
|
||||||
rootView.clearImageDraft()
|
msgType = 1
|
||||||
case .voice:
|
case .voice:
|
||||||
guard recordURL != nil, recordedDuration >= 1 else {
|
guard let recordURL, recordedDuration >= 1,
|
||||||
|
let voiceData = try? Data(contentsOf: recordURL) else {
|
||||||
DLToast.show(text: "请先完成录音")
|
DLToast.show(text: "请先完成录音")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cleanupAudio(deleteFile: true)
|
upload = UploadService.upload(voiceData, kind: .wav, scene: "pigeon")
|
||||||
rootView.clearVoiceDraft()
|
msgType = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
DLToast.showLoading()
|
||||||
|
upload
|
||||||
|
.flatMap { [weak self] fileId -> Observable<ResponseModel> in
|
||||||
|
guard let self else { return .empty() }
|
||||||
|
return PigeonService.send(
|
||||||
|
groupKey: self.activeGroupKey,
|
||||||
|
toUsers: self.selectedMembers.map(\.user_id),
|
||||||
|
msgType: msgType,
|
||||||
|
fileId: fileId,
|
||||||
|
textMsg: self.rootView.captionText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let self else { return }
|
||||||
|
switch self.rootView.mode {
|
||||||
|
case .image:
|
||||||
|
self.selectedImage = nil
|
||||||
|
self.rootView.clearImageDraft()
|
||||||
|
case .voice:
|
||||||
|
self.cleanupAudio(deleteFile: true)
|
||||||
|
self.rootView.clearVoiceDraft()
|
||||||
}
|
}
|
||||||
DLToast.show(text: "发送成功")
|
DLToast.show(text: "发送成功")
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.show(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func compressedPigeonJPEG(from image: UIImage) -> Data? {
|
||||||
|
let maxEdge: CGFloat = 1280
|
||||||
|
let quality: CGFloat = 0.55
|
||||||
|
let longest = max(image.size.width, image.size.height)
|
||||||
|
let output: UIImage
|
||||||
|
if longest > maxEdge, longest > 0 {
|
||||||
|
let scale = maxEdge / longest
|
||||||
|
let size = CGSize(
|
||||||
|
width: (image.size.width * scale).rounded(),
|
||||||
|
height: (image.size.height * scale).rounded()
|
||||||
|
)
|
||||||
|
let format = UIGraphicsImageRendererFormat.default()
|
||||||
|
format.scale = 1
|
||||||
|
format.opaque = true
|
||||||
|
output = UIGraphicsImageRenderer(size: size, format: format).image { _ in
|
||||||
|
image.draw(in: CGRect(origin: .zero, size: size))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output = image
|
||||||
|
}
|
||||||
|
return output.jpegData(compressionQuality: quality)
|
||||||
|
?? image.jpegData(compressionQuality: 0.8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func cleanupAudio(deleteFile: Bool) {
|
private func cleanupAudio(deleteFile: Bool) {
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@ enum PigeonMessageMode {
|
||||||
|
|
||||||
final class PigeonMessageView: UIView, UITextFieldDelegate {
|
final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
|
|
||||||
|
static let maxVoiceDuration: TimeInterval = 10
|
||||||
private static let designWidth: CGFloat = 375
|
private static let designWidth: CGFloat = 375
|
||||||
|
|
||||||
var onModeChanged: ((PigeonMessageMode) -> Void)?
|
var onModeChanged: ((PigeonMessageMode) -> Void)?
|
||||||
|
var onSelectionChanged: (([GroupMemberModel]) -> Void)?
|
||||||
var mode: PigeonMessageMode = .image {
|
var mode: PigeonMessageMode = .image {
|
||||||
didSet {
|
didSet {
|
||||||
updateMode()
|
updateMode()
|
||||||
|
|
@ -27,22 +29,35 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let historyButton = UIButton(type: .custom)
|
let historyButton = UIButton(type: .custom)
|
||||||
let changeRecipientButton = UIButton(type: .custom)
|
let switchGroupButton = UIButton(type: .custom)
|
||||||
let addImageButton = UIButton(type: .custom)
|
let addImageButton = UIButton(type: .custom)
|
||||||
let deleteImageButton = UIButton(type: .custom)
|
let deleteImageButton = UIButton(type: .custom)
|
||||||
let recordButton = UIButton(type: .custom)
|
let recordButton = UIButton(type: .custom)
|
||||||
let playPauseButton = UIButton(type: .custom)
|
let playPauseButton = UIButton(type: .custom)
|
||||||
let sendButton = UIButton(type: .custom)
|
let sendButton = UIButton(type: .custom)
|
||||||
|
|
||||||
private let recipientAvatarView = UIImageView()
|
private var members: [GroupMemberModel] = []
|
||||||
private let recipientNameLabel = UILabel()
|
private var selectedMemberIds: Set<String> = []
|
||||||
private let recipientIdLabel = UILabel()
|
|
||||||
private let recipientOnlineDotView = UIView()
|
private lazy var memberLayout: UICollectionViewFlowLayout = {
|
||||||
private let recipientOnlineLabel = UILabel()
|
let layout = UICollectionViewFlowLayout()
|
||||||
private let recipientStatusDividerView = UIView()
|
layout.scrollDirection = .horizontal
|
||||||
private let recipientLocationIconView = UIImageView()
|
layout.minimumLineSpacing = 0
|
||||||
private let recipientDistancePrefixLabel = UILabel()
|
layout.minimumInteritemSpacing = 0
|
||||||
private let recipientDistanceLabel = UILabel()
|
layout.itemSize = CGSize(width: 66, height: 84)
|
||||||
|
return layout
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var memberCollectionView: UICollectionView = {
|
||||||
|
let view = UICollectionView(frame: .zero, collectionViewLayout: memberLayout)
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.showsHorizontalScrollIndicator = false
|
||||||
|
view.dataSource = self
|
||||||
|
view.delegate = self
|
||||||
|
view.register(PigeonMemberCell.self, forCellWithReuseIdentifier: PigeonMemberCell.reuseId)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
private let imageModeButton = UIButton(type: .custom)
|
private let imageModeButton = UIButton(type: .custom)
|
||||||
private let voiceModeButton = UIButton(type: .custom)
|
private let voiceModeButton = UIButton(type: .custom)
|
||||||
private let imageEditorView = UIView()
|
private let imageEditorView = UIView()
|
||||||
|
|
@ -63,6 +78,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
private let waveformView = PigeonWaveformView()
|
private let waveformView = PigeonWaveformView()
|
||||||
private var addTileLeadingWithImage: NSLayoutConstraint?
|
private var addTileLeadingWithImage: NSLayoutConstraint?
|
||||||
private var addTileLeadingWithoutImage: NSLayoutConstraint?
|
private var addTileLeadingWithoutImage: NSLayoutConstraint?
|
||||||
|
private var selectedImageHeightConstraint: NSLayoutConstraint?
|
||||||
private var isRecording = false
|
private var isRecording = false
|
||||||
|
|
||||||
var captionText: String {
|
var captionText: String {
|
||||||
|
|
@ -73,7 +89,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
||||||
setupUI()
|
setupUI()
|
||||||
configureRecipient(nil, distanceText: "与您相距 --")
|
configureMembers([], selectedIds: [])
|
||||||
setSelectedImage(nil)
|
setSelectedImage(nil)
|
||||||
clearVoiceDraft()
|
clearVoiceDraft()
|
||||||
updateMode()
|
updateMode()
|
||||||
|
|
@ -91,19 +107,10 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
value * designScale
|
value * designScale
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureRecipient(_ member: GroupMemberModel?, distanceText: String) {
|
func configureMembers(_ members: [GroupMemberModel], selectedIds: Set<String>) {
|
||||||
recipientAvatarView.image = member?.userIcon ?? UIImage(named: "Common/default_avatar")
|
self.members = members
|
||||||
recipientNameLabel.text = member?.nick_name ?? "暂无圈子成员"
|
self.selectedMemberIds = selectedIds
|
||||||
recipientIdLabel.text = member.map { "ID:\($0.user_id)" } ?? "ID:--"
|
memberCollectionView.reloadData()
|
||||||
let isOnline = member?.is_online == true
|
|
||||||
recipientOnlineLabel.text = isOnline ? "在线" : "离线"
|
|
||||||
recipientOnlineDotView.backgroundColor = isOnline
|
|
||||||
? UIColor(hexStr: "#54D34D")
|
|
||||||
: UIColor(hexStr: "#C7CBD1")
|
|
||||||
recipientDistanceLabel.text = distanceText
|
|
||||||
.replacingOccurrences(of: "与您相距 ", with: "")
|
|
||||||
changeRecipientButton.isEnabled = member != nil
|
|
||||||
changeRecipientButton.alpha = member == nil ? 0.55 : 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSelectedImage(_ image: UIImage?) {
|
func setSelectedImage(_ image: UIImage?) {
|
||||||
|
|
@ -116,6 +123,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
// deleteImageButton.isHidden = image == nil
|
// deleteImageButton.isHidden = image == nil
|
||||||
addTile.alpha = 1
|
addTile.alpha = 1
|
||||||
updateAddTilePosition(hasImage: image != nil)
|
updateAddTilePosition(hasImage: image != nil)
|
||||||
|
setNeedsLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRecording(isRecording: Bool) {
|
func setRecording(isRecording: Bool) {
|
||||||
|
|
@ -132,9 +140,15 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVoiceDuration(_ seconds: TimeInterval, hasRecording: Bool) {
|
func setVoiceDuration(_ seconds: TimeInterval, hasRecording: Bool) {
|
||||||
let value = min(60, max(0, Int(seconds.rounded(.down))))
|
let maxDuration = Self.maxVoiceDuration
|
||||||
|
let value: Int
|
||||||
|
if seconds + 0.05 >= maxDuration {
|
||||||
|
value = Int(maxDuration)
|
||||||
|
} else {
|
||||||
|
value = min(Int(maxDuration), max(0, Int(seconds.rounded(.down))))
|
||||||
|
}
|
||||||
let currentTime = String(format: "%02d″", value)
|
let currentTime = String(format: "%02d″", value)
|
||||||
let timeText = "\(currentTime)/60″"
|
let timeText = "\(currentTime)/\(Int(maxDuration))″"
|
||||||
let attributedTime = NSMutableAttributedString(
|
let attributedTime = NSMutableAttributedString(
|
||||||
string: timeText,
|
string: timeText,
|
||||||
attributes: [.foregroundColor: UIColor(hexStr: "#AAAAAA")]
|
attributes: [.foregroundColor: UIColor(hexStr: "#AAAAAA")]
|
||||||
|
|
@ -248,6 +262,19 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
.top(scaled(16))
|
.top(scaled(16))
|
||||||
.leftToRightOfView(sectionSparkle, offset: scaled(8))
|
.leftToRightOfView(sectionSparkle, offset: scaled(8))
|
||||||
|
|
||||||
|
switchGroupButton.backgroundColor = .white
|
||||||
|
switchGroupButton.setTitle(" 切换圈子 ", for: .normal)
|
||||||
|
switchGroupButton.titleLabel?.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
switchGroupButton.setTitleColor(UIColor(hexStr: "#293445"), for: .normal)
|
||||||
|
switchGroupButton.layer.borderWidth = 1
|
||||||
|
switchGroupButton.layer.borderColor = UIColor(hexStr: "#E8E8E8").cgColor
|
||||||
|
switchGroupButton.cornerRadius = 14
|
||||||
|
contentView.addSubview(switchGroupButton)
|
||||||
|
switchGroupButton.layoutChain
|
||||||
|
.centerY(sectionTitle)
|
||||||
|
.right(scaled(15))
|
||||||
|
.height(28)
|
||||||
|
|
||||||
let recipientCard = UIView()
|
let recipientCard = UIView()
|
||||||
recipientCard.backgroundColor = .white
|
recipientCard.backgroundColor = .white
|
||||||
recipientCard.layer.cornerRadius = scaled(20)
|
recipientCard.layer.cornerRadius = scaled(20)
|
||||||
|
|
@ -255,7 +282,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
recipientCard.layoutChain
|
recipientCard.layoutChain
|
||||||
.topToBottomOfView(sectionTitle, offset: scaled(8))
|
.topToBottomOfView(sectionTitle, offset: scaled(8))
|
||||||
.edgesHorzontal(scaled(15))
|
.edgesHorzontal(scaled(15))
|
||||||
.height(scaled(100))
|
.height(100)
|
||||||
setupRecipientCard(recipientCard)
|
setupRecipientCard(recipientCard)
|
||||||
|
|
||||||
let modeContainer = UIView()
|
let modeContainer = UIView()
|
||||||
|
|
@ -292,107 +319,11 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupRecipientCard(_ card: UIView) {
|
private func setupRecipientCard(_ card: UIView) {
|
||||||
recipientAvatarView.contentMode = .scaleAspectFill
|
card.addSubview(memberCollectionView)
|
||||||
recipientAvatarView.clipsToBounds = true
|
memberCollectionView.layoutChain
|
||||||
recipientAvatarView.layer.cornerRadius = scaled(29)
|
.edgesHorzontal(8)
|
||||||
card.addSubview(recipientAvatarView)
|
|
||||||
recipientAvatarView.layoutChain
|
|
||||||
.left(scaled(18))
|
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(scaled(58))
|
.height(84)
|
||||||
.height(scaled(58))
|
|
||||||
|
|
||||||
recipientNameLabel.font = .systemFont(ofSize: scaled(17), weight: .bold)
|
|
||||||
recipientNameLabel.textColor = UIColor(hexStr: "#293445")
|
|
||||||
recipientNameLabel.lineBreakMode = .byTruncatingTail
|
|
||||||
card.addSubview(recipientNameLabel)
|
|
||||||
recipientNameLabel.layoutChain
|
|
||||||
.top(scaled(18))
|
|
||||||
.leftToRightOfView(recipientAvatarView, offset: scaled(16))
|
|
||||||
|
|
||||||
recipientIdLabel.font = .systemFont(ofSize: scaled(12))
|
|
||||||
recipientIdLabel.textColor = UIColor(hexStr: "#767676")
|
|
||||||
recipientIdLabel.lineBreakMode = .byTruncatingMiddle
|
|
||||||
card.addSubview(recipientIdLabel)
|
|
||||||
recipientIdLabel.layoutChain
|
|
||||||
.topToBottomOfView(recipientNameLabel, offset: scaled(4))
|
|
||||||
.leftToView(recipientNameLabel)
|
|
||||||
|
|
||||||
recipientOnlineDotView.layer.cornerRadius = scaled(3)
|
|
||||||
card.addSubview(recipientOnlineDotView)
|
|
||||||
recipientOnlineDotView.layoutChain
|
|
||||||
.topToBottomOfView(recipientIdLabel, offset: scaled(10))
|
|
||||||
.leftToView(recipientNameLabel)
|
|
||||||
.width(scaled(6))
|
|
||||||
.height(scaled(6))
|
|
||||||
|
|
||||||
recipientOnlineLabel.font = .systemFont(ofSize: scaled(12), weight: .medium)
|
|
||||||
recipientOnlineLabel.textColor = UIColor(hexStr: "#747A84")
|
|
||||||
card.addSubview(recipientOnlineLabel)
|
|
||||||
recipientOnlineLabel.layoutChain
|
|
||||||
.centerY(recipientOnlineDotView)
|
|
||||||
.leftToRightOfView(recipientOnlineDotView, offset: scaled(6))
|
|
||||||
|
|
||||||
recipientStatusDividerView.backgroundColor = UIColor(hexStr: "#D8DBDF")
|
|
||||||
card.addSubview(recipientStatusDividerView)
|
|
||||||
recipientStatusDividerView.layoutChain
|
|
||||||
.centerY(recipientOnlineDotView)
|
|
||||||
.leftToRightOfView(recipientOnlineLabel, offset: scaled(10))
|
|
||||||
.width(scaled(1))
|
|
||||||
.height(scaled(14))
|
|
||||||
|
|
||||||
recipientLocationIconView.image = UIImage(
|
|
||||||
systemName: "mappin.circle",
|
|
||||||
withConfiguration: UIImage.SymbolConfiguration(
|
|
||||||
pointSize: scaled(13),
|
|
||||||
weight: .regular
|
|
||||||
)
|
|
||||||
)
|
|
||||||
recipientLocationIconView.tintColor = UIColor(hexStr: "#858A92")
|
|
||||||
recipientLocationIconView.contentMode = .scaleAspectFit
|
|
||||||
card.addSubview(recipientLocationIconView)
|
|
||||||
recipientLocationIconView.layoutChain
|
|
||||||
.centerY(recipientOnlineDotView)
|
|
||||||
.leftToRightOfView(recipientStatusDividerView, offset: scaled(9))
|
|
||||||
.width(scaled(14))
|
|
||||||
.height(scaled(14))
|
|
||||||
|
|
||||||
recipientDistancePrefixLabel.text = "与您相距"
|
|
||||||
recipientDistancePrefixLabel.font = .systemFont(ofSize: scaled(12))
|
|
||||||
recipientDistancePrefixLabel.textColor = UIColor(hexStr: "#747A84")
|
|
||||||
card.addSubview(recipientDistancePrefixLabel)
|
|
||||||
recipientDistancePrefixLabel.layoutChain
|
|
||||||
.centerY(recipientOnlineDotView)
|
|
||||||
.leftToRightOfView(recipientLocationIconView, offset: scaled(4))
|
|
||||||
|
|
||||||
recipientDistanceLabel.font = .systemFont(ofSize: scaled(12))
|
|
||||||
recipientDistanceLabel.textColor = UIColor(hexStr: "#00ADFE")
|
|
||||||
card.addSubview(recipientDistanceLabel)
|
|
||||||
recipientDistanceLabel.layoutChain
|
|
||||||
.centerY(recipientOnlineDotView)
|
|
||||||
.leftToRightOfView(recipientDistancePrefixLabel, offset: scaled(4))
|
|
||||||
|
|
||||||
changeRecipientButton.setTitle("更换收件人", for: .normal)
|
|
||||||
changeRecipientButton.setTitleColor(UIColor(hexStr: "#00ADFE"), for: .normal)
|
|
||||||
changeRecipientButton.titleLabel?.font = .systemFont(ofSize: scaled(12), weight: .medium)
|
|
||||||
changeRecipientButton.backgroundColor = UIColor(hexStr: "#F6F6F6")
|
|
||||||
changeRecipientButton.layer.cornerRadius = scaled(15)
|
|
||||||
card.addSubview(changeRecipientButton)
|
|
||||||
changeRecipientButton.layoutChain
|
|
||||||
.right(scaled(12))
|
|
||||||
.centerY()
|
|
||||||
.width(scaled(70))
|
|
||||||
.height(scaled(30))
|
|
||||||
recipientNameLabel.layoutChain.rightToLeftOfView(
|
|
||||||
changeRecipientButton,
|
|
||||||
offset: -scaled(8),
|
|
||||||
relation: .lessThanOrEqual
|
|
||||||
)
|
|
||||||
recipientIdLabel.layoutChain.rightToLeftOfView(
|
|
||||||
changeRecipientButton,
|
|
||||||
offset: -scaled(8),
|
|
||||||
relation: .lessThanOrEqual
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupModeButtons(_ container: UIView) {
|
private func setupModeButtons(_ container: UIView) {
|
||||||
|
|
@ -439,10 +370,16 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
|
|
||||||
selectedImageMaskView.backgroundColor = .black.withAlphaComponent(0.2)
|
selectedImageMaskView.backgroundColor = .black.withAlphaComponent(0.2)
|
||||||
selectedImageMaskView.isHidden = true
|
selectedImageMaskView.isHidden = true
|
||||||
selectedImageView.contentMode = .scaleAspectFill
|
selectedImageView.contentMode = .scaleAspectFit
|
||||||
selectedImageView.clipsToBounds = true
|
selectedImageView.clipsToBounds = false
|
||||||
imageEditorView.addSubview(selectedImageView)
|
imageEditorView.addSubview(selectedImageView)
|
||||||
selectedImageView.layoutChain.edges()
|
selectedImageView.layoutChain
|
||||||
|
.top()
|
||||||
|
.left()
|
||||||
|
.right()
|
||||||
|
let imageHeight = selectedImageView.heightAnchor.constraint(equalTo: imageEditorView.heightAnchor)
|
||||||
|
imageHeight.isActive = true
|
||||||
|
selectedImageHeightConstraint = imageHeight
|
||||||
|
|
||||||
imageEditorView.addSubview(selectedImageMaskView)
|
imageEditorView.addSubview(selectedImageMaskView)
|
||||||
selectedImageMaskView.layoutChain.edges()
|
selectedImageMaskView.layoutChain.edges()
|
||||||
|
|
@ -559,7 +496,7 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
.right(scaled(66))
|
.right(scaled(66))
|
||||||
.height(scaled(24))
|
.height(scaled(24))
|
||||||
|
|
||||||
voiceTimeLabel.text = "00″/60″"
|
voiceTimeLabel.text = "00″/10″"
|
||||||
voiceTimeLabel.font = .systemFont(ofSize: scaled(16))
|
voiceTimeLabel.font = .systemFont(ofSize: scaled(16))
|
||||||
voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1")
|
voiceTimeLabel.textColor = UIColor(hexStr: "#13B6F1")
|
||||||
playbackBar.addSubview(voiceTimeLabel)
|
playbackBar.addSubview(voiceTimeLabel)
|
||||||
|
|
@ -682,6 +619,31 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
return current.replacingCharacters(in: stringRange, with: string).count <= 20
|
return current.replacingCharacters(in: stringRange, with: string).count <= 20
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
updateSelectedImageTopFill()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSelectedImageTopFill() {
|
||||||
|
guard let oldConstraint = selectedImageHeightConstraint else { return }
|
||||||
|
let editorWidth = imageEditorView.bounds.width
|
||||||
|
let editorHeight = imageEditorView.bounds.height
|
||||||
|
guard editorWidth > 1, editorHeight > 1 else { return }
|
||||||
|
let height: CGFloat
|
||||||
|
if let image = selectedImageView.image, image.size.width > 1 {
|
||||||
|
height = max(editorHeight, editorWidth * image.size.height / image.size.width)
|
||||||
|
} else {
|
||||||
|
height = editorHeight
|
||||||
|
}
|
||||||
|
if oldConstraint.secondItem == nil, abs(oldConstraint.constant - height) < 0.5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldConstraint.isActive = false
|
||||||
|
let constraint = selectedImageView.heightAnchor.constraint(equalToConstant: height)
|
||||||
|
constraint.isActive = true
|
||||||
|
selectedImageHeightConstraint = constraint
|
||||||
|
}
|
||||||
|
|
||||||
private func updateAddTilePosition(hasImage: Bool) {
|
private func updateAddTilePosition(hasImage: Bool) {
|
||||||
addTileLeadingWithImage?.isActive = false
|
addTileLeadingWithImage?.isActive = false
|
||||||
addTileLeadingWithoutImage?.isActive = false
|
addTileLeadingWithoutImage?.isActive = false
|
||||||
|
|
@ -713,6 +675,106 @@ final class PigeonMessageView: UIView, UITextFieldDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PigeonMessageView: UICollectionViewDataSource, UICollectionViewDelegate {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
members.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell = collectionView.dequeueReusableCell(
|
||||||
|
withReuseIdentifier: PigeonMemberCell.reuseId,
|
||||||
|
for: indexPath
|
||||||
|
) as! PigeonMemberCell
|
||||||
|
let member = members[indexPath.item]
|
||||||
|
cell.configure(member: member, selected: selectedMemberIds.contains(member.user_id))
|
||||||
|
cell.onToggle = { [weak self] in
|
||||||
|
self?.toggleMember(at: indexPath.item)
|
||||||
|
}
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
toggleMember(at: indexPath.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension PigeonMessageView {
|
||||||
|
func toggleMember(at index: Int) {
|
||||||
|
guard members.indices.contains(index) else { return }
|
||||||
|
let id = members[index].user_id
|
||||||
|
if selectedMemberIds.contains(id) {
|
||||||
|
selectedMemberIds.remove(id)
|
||||||
|
} else {
|
||||||
|
selectedMemberIds.insert(id)
|
||||||
|
}
|
||||||
|
memberCollectionView.reloadData()
|
||||||
|
onSelectionChanged?(members.filter { selectedMemberIds.contains($0.user_id) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PigeonMemberCell: UICollectionViewCell {
|
||||||
|
static let reuseId = "PigeonMemberCell"
|
||||||
|
|
||||||
|
var onToggle: (() -> Void)?
|
||||||
|
|
||||||
|
private let avatarView = UIImageView()
|
||||||
|
private let onlineDot = UIView()
|
||||||
|
private let nameLabel = UILabel()
|
||||||
|
private let checkBtn = UIButton(type: .custom)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
avatarView.contentMode = .scaleAspectFill
|
||||||
|
avatarView.clipsToBounds = true
|
||||||
|
avatarView.layer.cornerRadius = 10
|
||||||
|
contentView.addSubview(avatarView)
|
||||||
|
avatarView.layoutChain.top(5).centerX().width(38).height(38)
|
||||||
|
|
||||||
|
onlineDot.layer.cornerRadius = 4
|
||||||
|
onlineDot.layer.borderWidth = 1.5
|
||||||
|
onlineDot.layer.borderColor = UIColor.white.cgColor
|
||||||
|
onlineDot.isUserInteractionEnabled = false
|
||||||
|
contentView.addSubview(onlineDot)
|
||||||
|
onlineDot.layoutChain.topToView(avatarView, offset: -1).rightToView(avatarView, offset: 1).width(8).height(8)
|
||||||
|
|
||||||
|
nameLabel.font = .systemFont(ofSize: 8, weight: .medium)
|
||||||
|
nameLabel.textColor = UIColor(hexStr: "#767676")
|
||||||
|
nameLabel.textAlignment = .center
|
||||||
|
nameLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
nameLabel.backgroundColor = UIColor(hexStr: "#F8F8F8")
|
||||||
|
nameLabel.layer.cornerRadius = 7
|
||||||
|
nameLabel.clipsToBounds = true
|
||||||
|
contentView.addSubview(nameLabel)
|
||||||
|
nameLabel.layoutChain
|
||||||
|
.topToBottomOfView(avatarView, offset: 4)
|
||||||
|
.centerX()
|
||||||
|
.width(56)
|
||||||
|
.height(14)
|
||||||
|
|
||||||
|
checkBtn.setImage(UIImage(named: "Common/checkbox"), for: .normal)
|
||||||
|
checkBtn.setImage(UIImage(named: "Common/checkbox_on"), for: .selected)
|
||||||
|
checkBtn.imageView?.contentMode = .scaleAspectFit
|
||||||
|
checkBtn.addTarget(self, action: #selector(tapCheck), for: .touchUpInside)
|
||||||
|
contentView.addSubview(checkBtn)
|
||||||
|
checkBtn.layoutChain.topToBottomOfView(nameLabel, offset: 2).centerX().width(20).height(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
||||||
|
func configure(member: GroupMemberModel, selected: Bool) {
|
||||||
|
avatarView.image = member.userIcon.size.width > 0
|
||||||
|
? member.userIcon
|
||||||
|
: UIImage(named: "Common/default_avatar")
|
||||||
|
nameLabel.text = member.nick_name
|
||||||
|
onlineDot.backgroundColor = UIColor(hexStr: member.is_online ? "#67E151" : "#AAAAAA")
|
||||||
|
checkBtn.isSelected = selected
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapCheck() {
|
||||||
|
onToggle?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class PigeonGradientButton: UIButton {
|
final class PigeonGradientButton: UIButton {
|
||||||
private let gradient = CAGradientLayer()
|
private let gradient = CAGradientLayer()
|
||||||
|
|
||||||
|
|
@ -803,149 +865,3 @@ final class PigeonWaveformView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class PigeonRecipientPickerView: UIView {
|
|
||||||
private let members: [GroupMemberModel]
|
|
||||||
private let selectedId: String
|
|
||||||
private let onSelect: (GroupMemberModel) -> Void
|
|
||||||
private let tableView = UITableView(frame: .zero, style: .plain)
|
|
||||||
private let sheet = UIView()
|
|
||||||
|
|
||||||
static func show(
|
|
||||||
members: [GroupMemberModel],
|
|
||||||
selectedId: String,
|
|
||||||
onSelect: @escaping (GroupMemberModel) -> Void
|
|
||||||
) {
|
|
||||||
guard let window = kKeyWindow, !members.isEmpty else { return }
|
|
||||||
let picker = PigeonRecipientPickerView(
|
|
||||||
frame: CGRect(origin: .zero, size: kScreenSize),
|
|
||||||
members: members,
|
|
||||||
selectedId: selectedId,
|
|
||||||
onSelect: onSelect
|
|
||||||
)
|
|
||||||
window.addSubview(picker)
|
|
||||||
picker.alpha = 0
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
|
||||||
picker.alpha = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private init(
|
|
||||||
frame: CGRect,
|
|
||||||
members: [GroupMemberModel],
|
|
||||||
selectedId: String,
|
|
||||||
onSelect: @escaping (GroupMemberModel) -> Void
|
|
||||||
) {
|
|
||||||
self.members = members
|
|
||||||
self.selectedId = selectedId
|
|
||||||
self.onSelect = onSelect
|
|
||||||
super.init(frame: frame)
|
|
||||||
setupUI()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupUI() {
|
|
||||||
backgroundColor = UIColor.black.withAlphaComponent(0.48)
|
|
||||||
let backgroundTap = UITapGestureRecognizer(target: self, action: #selector(dismiss))
|
|
||||||
backgroundTap.delegate = self
|
|
||||||
addGestureRecognizer(backgroundTap)
|
|
||||||
|
|
||||||
sheet.backgroundColor = .white
|
|
||||||
sheet.layer.cornerRadius = 26
|
|
||||||
sheet.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
|
||||||
addSubview(sheet)
|
|
||||||
sheet.layoutChain.bottom().edgesHorzontal().height(min(460, CGFloat(members.count * 72 + 86)))
|
|
||||||
|
|
||||||
let title = UILabel()
|
|
||||||
title.text = "选择收件人"
|
|
||||||
title.font = .systemFont(ofSize: 20, weight: .bold)
|
|
||||||
title.textColor = UIColor(hexStr: "#293445")
|
|
||||||
sheet.addSubview(title)
|
|
||||||
title.layoutChain.top(20).left(20)
|
|
||||||
|
|
||||||
tableView.backgroundColor = .clear
|
|
||||||
tableView.separatorStyle = .none
|
|
||||||
tableView.rowHeight = 68
|
|
||||||
tableView.dataSource = self
|
|
||||||
tableView.delegate = self
|
|
||||||
tableView.register(PigeonRecipientCell.self, forCellReuseIdentifier: "PigeonRecipientCell")
|
|
||||||
sheet.addSubview(tableView)
|
|
||||||
tableView.layoutChain.top(58).edgesHorzontal().bottom(kSafeBottomMargin)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func dismiss() {
|
|
||||||
UIView.animate(withDuration: 0.18, animations: {
|
|
||||||
self.alpha = 0
|
|
||||||
}) { _ in
|
|
||||||
self.removeFromSuperview()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PigeonRecipientPickerView: UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate {
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
||||||
!(touch.view?.isDescendant(of: sheet) ?? false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
members.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
guard let cell = tableView.dequeueReusableCell(
|
|
||||||
withIdentifier: "PigeonRecipientCell",
|
|
||||||
for: indexPath
|
|
||||||
) as? PigeonRecipientCell else {
|
|
||||||
return UITableViewCell()
|
|
||||||
}
|
|
||||||
let member = members[indexPath.row]
|
|
||||||
cell.configure(member: member, selected: member.user_id == selectedId)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
onSelect(members[indexPath.row])
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class PigeonRecipientCell: UITableViewCell {
|
|
||||||
private let avatarView = UIImageView()
|
|
||||||
private let nameLabel = UILabel()
|
|
||||||
private let statusLabel = UILabel()
|
|
||||||
private let tickView = UIImageView(image: UIImage(named: "PigeonMessage/tick_circle"))
|
|
||||||
|
|
||||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
||||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
||||||
selectionStyle = .none
|
|
||||||
avatarView.contentMode = .scaleAspectFill
|
|
||||||
avatarView.clipsToBounds = true
|
|
||||||
avatarView.layer.cornerRadius = 20
|
|
||||||
nameLabel.font = .systemFont(ofSize: 16, weight: .semibold)
|
|
||||||
nameLabel.textColor = UIColor(hexStr: "#293445")
|
|
||||||
statusLabel.font = .systemFont(ofSize: 12)
|
|
||||||
statusLabel.textColor = UIColor(hexStr: "#9EA3AA")
|
|
||||||
contentView.addSubview(avatarView)
|
|
||||||
contentView.addSubview(nameLabel)
|
|
||||||
contentView.addSubview(statusLabel)
|
|
||||||
contentView.addSubview(tickView)
|
|
||||||
avatarView.layoutChain.left(20).centerY().width(48).height(48)
|
|
||||||
nameLabel.layoutChain.top(13).leftToRightOfView(avatarView, offset: 14)
|
|
||||||
statusLabel.layoutChain.topToBottomOfView(nameLabel, offset: 4).leftToView(nameLabel)
|
|
||||||
tickView.layoutChain.right(20).centerY().width(16).height(16)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func configure(member: GroupMemberModel, selected: Bool) {
|
|
||||||
avatarView.image = member.userIcon
|
|
||||||
nameLabel.text = member.nick_name
|
|
||||||
statusLabel.text = member.is_online ? "在线" : "离线"
|
|
||||||
tickView.isHidden = !selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
//
|
||||||
|
// PigeonUploadPopView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class PigeonUploadPopView: UIView, UIGestureRecognizerDelegate {
|
||||||
|
|
||||||
|
private static let shared = PigeonUploadPopView(frame: CGRect(origin: .zero, size: kScreenSize))
|
||||||
|
|
||||||
|
private var onAlbum: (() -> Void)?
|
||||||
|
private var onCamera: (() -> Void)?
|
||||||
|
|
||||||
|
static func show(onAlbum: @escaping () -> Void, onCamera: @escaping () -> Void) {
|
||||||
|
guard let superView = kKeyWindow else { return }
|
||||||
|
let pop = PigeonUploadPopView.shared
|
||||||
|
if pop.superview != nil {
|
||||||
|
pop.removeFromSuperview()
|
||||||
|
}
|
||||||
|
pop.onAlbum = onAlbum
|
||||||
|
pop.onCamera = onCamera
|
||||||
|
pop.frame = superView.bounds
|
||||||
|
pop.dimView.alpha = 0
|
||||||
|
pop.sheetView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||||
|
superView.addSubview(pop)
|
||||||
|
superView.bringSubviewToFront(pop)
|
||||||
|
pop.layoutIfNeeded()
|
||||||
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
||||||
|
pop.dimView.alpha = 1
|
||||||
|
pop.sheetView.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func dismiss(completion: (() -> Void)? = nil) {
|
||||||
|
let pop = PigeonUploadPopView.shared
|
||||||
|
guard pop.superview != nil else {
|
||||||
|
completion?()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.curveEaseIn]) {
|
||||||
|
pop.dimView.alpha = 0
|
||||||
|
pop.sheetView.transform = CGAffineTransform(translationX: 0, y: kScreenHeight)
|
||||||
|
} completion: { _ in
|
||||||
|
pop.sheetView.transform = .identity
|
||||||
|
pop.onAlbum = nil
|
||||||
|
pop.onCamera = nil
|
||||||
|
pop.removeFromSuperview()
|
||||||
|
completion?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapMask() {
|
||||||
|
PigeonUploadPopView.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapAlbum() {
|
||||||
|
let action = onAlbum
|
||||||
|
PigeonUploadPopView.dismiss {
|
||||||
|
action?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapCamera() {
|
||||||
|
let action = onCamera
|
||||||
|
PigeonUploadPopView.dismiss {
|
||||||
|
action?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
||||||
|
let point = touch.location(in: self)
|
||||||
|
return !sheetView.frame.contains(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(dimView)
|
||||||
|
addSubview(sheetView)
|
||||||
|
sheetView.addSubview(headerView)
|
||||||
|
sheetView.addSubview(albumButton)
|
||||||
|
sheetView.addSubview(cameraButton)
|
||||||
|
sheetView.addSubview(albumLabel)
|
||||||
|
sheetView.addSubview(cameraLabel)
|
||||||
|
|
||||||
|
dimView.layoutChain.edges()
|
||||||
|
|
||||||
|
sheetView.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.edgesHorzontal()
|
||||||
|
|
||||||
|
headerView.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.heightToWidth(200.0 / 750.0)
|
||||||
|
|
||||||
|
albumButton.layoutChain
|
||||||
|
.topToBottomOfView(headerView, offset: 8)
|
||||||
|
.centerX(sheetView, offset: -80)
|
||||||
|
.width(80)
|
||||||
|
.height(80)
|
||||||
|
|
||||||
|
cameraButton.layoutChain
|
||||||
|
.topToView(albumButton)
|
||||||
|
.centerX(sheetView, offset: 80)
|
||||||
|
.width(80)
|
||||||
|
.height(80)
|
||||||
|
|
||||||
|
albumLabel.layoutChain
|
||||||
|
.topToBottomOfView(albumButton, offset: 10)
|
||||||
|
.centerX(albumButton)
|
||||||
|
.bottom(22 + kSafeBottomMargin)
|
||||||
|
|
||||||
|
cameraLabel.layoutChain
|
||||||
|
.topToView(albumLabel)
|
||||||
|
.centerX(cameraButton)
|
||||||
|
|
||||||
|
let tap = UITapGestureRecognizer(target: self, action: #selector(tapMask))
|
||||||
|
tap.delegate = self
|
||||||
|
addGestureRecognizer(tap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var dimView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor.black.withAlphaComponent(0.45)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var sheetView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .white
|
||||||
|
view.layer.cornerRadius = 40
|
||||||
|
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var headerView: UIImageView = {
|
||||||
|
let view = UIImageView(image: UIImage(named: "PigeonMessage/upload_header"))
|
||||||
|
view.contentMode = .scaleToFill
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var albumButton: UIButton = {
|
||||||
|
makeOptionButton(imageName: "PigeonMessage/gallery", action: #selector(tapAlbum))
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var cameraButton: UIButton = {
|
||||||
|
makeOptionButton(imageName: "PigeonMessage/camera", action: #selector(tapCamera))
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var albumLabel: UILabel = {
|
||||||
|
makeOptionLabel("本地上传")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var cameraLabel: UILabel = {
|
||||||
|
makeOptionLabel("拍照上传")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private func makeOptionButton(imageName: String, action: Selector) -> UIButton {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.backgroundColor = UIColor(hexStr: "#F5F5F5")
|
||||||
|
button.layer.cornerRadius = 20
|
||||||
|
button.setImage(UIImage(named: imageName), for: .normal)
|
||||||
|
button.addTarget(self, action: action, for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeOptionLabel(_ text: String) -> UILabel {
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = text
|
||||||
|
label.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#293445")
|
||||||
|
label.textAlignment = .center
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
//
|
||||||
|
// PigeonWatermarkCameraVC.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreLocation
|
||||||
|
import SwiftyUserDefaults
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
import AMapSearchKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
final class PigeonWatermarkCameraVC: BaseViewController {
|
||||||
|
|
||||||
|
var onPhoto: ((UIImage) -> Void)?
|
||||||
|
|
||||||
|
private var rootView: PigeonWatermarkCameraView!
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
private let searchAPI = AMapSearchAPI()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = PigeonWatermarkCameraView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
searchAPI?.delegate = self
|
||||||
|
#endif
|
||||||
|
rootView.onClose = { [weak self] in
|
||||||
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
rootView.onConfirm = { [weak self] image in
|
||||||
|
guard let self else { return }
|
||||||
|
self.onPhoto?(image)
|
||||||
|
self.navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
requestAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
rootView.startSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
rootView.stopSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestAddress() {
|
||||||
|
guard let lat = Defaults[\.currentLatitude],
|
||||||
|
let lon = Defaults[\.currentLongitude] else { return }
|
||||||
|
let coord = CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||||
|
guard CLLocationCoordinate2DIsValid(coord) else { return }
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
let regeo = AMapReGeocodeSearchRequest()
|
||||||
|
regeo.location = AMapGeoPoint.location(
|
||||||
|
withLatitude: CGFloat(coord.latitude),
|
||||||
|
longitude: CGFloat(coord.longitude)
|
||||||
|
)
|
||||||
|
regeo.requireExtension = true
|
||||||
|
searchAPI?.aMapReGoecodeSearch(regeo)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !targetEnvironment(simulator)
|
||||||
|
extension PigeonWatermarkCameraVC: AMapSearchDelegate {
|
||||||
|
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||||||
|
let address = response.regeocode?.formattedAddress
|
||||||
|
?? response.regeocode?.pois?.first?.name
|
||||||
|
?? ""
|
||||||
|
rootView.updateAddress(address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
|
||||||
|
print("Pigeon watermark regeo error: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1,646 @@
|
||||||
|
//
|
||||||
|
// PigeonWatermarkCameraView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
final class PigeonWatermarkCameraView: UIView {
|
||||||
|
|
||||||
|
var onClose: (() -> Void)?
|
||||||
|
var onConfirm: ((UIImage) -> Void)?
|
||||||
|
|
||||||
|
private let session = AVCaptureSession()
|
||||||
|
private let sessionQueue = DispatchQueue(label: "pigeon.watermark.camera")
|
||||||
|
private var previewLayer: AVCaptureVideoPreviewLayer?
|
||||||
|
private var videoInput: AVCaptureDeviceInput?
|
||||||
|
private var photoOutput: AVCapturePhotoOutput?
|
||||||
|
|
||||||
|
private var cameraPosition: AVCaptureDevice.Position = .back
|
||||||
|
private var isTorchOn = false
|
||||||
|
private var isReviewing = false
|
||||||
|
private var isCapturing = false
|
||||||
|
private var didConfigureSession = false
|
||||||
|
private var capturedImage: UIImage?
|
||||||
|
private var captureDate = Date()
|
||||||
|
private var addressText = ""
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = UIColor(hexStr: "#F9F9F9")
|
||||||
|
setupUI()
|
||||||
|
applyReviewState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
let session = self.session
|
||||||
|
sessionQueue.async {
|
||||||
|
if session.isRunning {
|
||||||
|
session.stopRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSession() {
|
||||||
|
requestAccessAndConfigure()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopSession() {
|
||||||
|
let session = self.session
|
||||||
|
sessionQueue.async {
|
||||||
|
if session.isRunning {
|
||||||
|
session.stopRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAddress(_ address: String) {
|
||||||
|
addressText = address
|
||||||
|
if isReviewing {
|
||||||
|
refreshWatermark(at: captureDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
previewLayer?.frame = previewHost.bounds
|
||||||
|
shutterButton.setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
addSubview(gradientView)
|
||||||
|
addSubview(navView)
|
||||||
|
addSubview(previewContainer)
|
||||||
|
previewContainer.addSubview(previewHost)
|
||||||
|
previewContainer.addSubview(capturedImageView)
|
||||||
|
previewContainer.addSubview(watermarkView)
|
||||||
|
addSubview(flashButton)
|
||||||
|
addSubview(shutterButton)
|
||||||
|
addSubview(switchButton)
|
||||||
|
addSubview(retakeButton)
|
||||||
|
addSubview(confirmButton)
|
||||||
|
|
||||||
|
gradientView.layoutChain
|
||||||
|
.top()
|
||||||
|
.edgesHorzontal()
|
||||||
|
.height(135)
|
||||||
|
|
||||||
|
navView.layoutChain.edges(excludingEdge: .bottom).height(kNaviHeight)
|
||||||
|
navView.onBackTapped = { [weak self] in
|
||||||
|
self?.onClose?()
|
||||||
|
}
|
||||||
|
|
||||||
|
previewContainer.layoutChain
|
||||||
|
.top(kNaviHeight + 73)
|
||||||
|
.edgesHorzontal(30)
|
||||||
|
.heightToWidth(424/315)
|
||||||
|
|
||||||
|
previewHost.layoutChain.edges()
|
||||||
|
capturedImageView.layoutChain.edges()
|
||||||
|
|
||||||
|
watermarkView.layoutChain
|
||||||
|
.left(12)
|
||||||
|
.top(12)
|
||||||
|
watermarkView.rightAnchor.constraint(
|
||||||
|
lessThanOrEqualTo: previewContainer.rightAnchor,
|
||||||
|
constant: -12
|
||||||
|
).isActive = true
|
||||||
|
|
||||||
|
shutterButton.layoutChain
|
||||||
|
.centerX()
|
||||||
|
.bottom(kSafeBottomMargin + 18)
|
||||||
|
.width(84)
|
||||||
|
.height(84)
|
||||||
|
|
||||||
|
flashButton.layoutChain
|
||||||
|
.centerY(shutterButton)
|
||||||
|
.rightToLeftOfView(shutterButton, offset: -52)
|
||||||
|
.width(50)
|
||||||
|
.height(50)
|
||||||
|
|
||||||
|
switchButton.layoutChain
|
||||||
|
.centerY(shutterButton)
|
||||||
|
.leftToRightOfView(shutterButton, offset: 52)
|
||||||
|
.width(50)
|
||||||
|
.height(50)
|
||||||
|
|
||||||
|
retakeButton.layoutChain
|
||||||
|
.left(30)
|
||||||
|
.bottom(kSafeBottomMargin + 24)
|
||||||
|
.height(50)
|
||||||
|
.width(110)
|
||||||
|
|
||||||
|
confirmButton.layoutChain
|
||||||
|
.leftToRightOfView(retakeButton, offset: 16)
|
||||||
|
.right(30)
|
||||||
|
.bottomToView(retakeButton)
|
||||||
|
.height(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestAccessAndConfigure() {
|
||||||
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||||
|
case .authorized:
|
||||||
|
configureSession()
|
||||||
|
case .notDetermined:
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if granted {
|
||||||
|
self?.configureSession()
|
||||||
|
} else {
|
||||||
|
DLToast.show(text: "请在设置中开启相机权限")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
DLToast.show(text: "请在设置中开启相机权限")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureSession() {
|
||||||
|
sessionQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if !self.didConfigureSession {
|
||||||
|
self.session.beginConfiguration()
|
||||||
|
self.session.sessionPreset = .photo
|
||||||
|
_ = self.attachVideoInput(position: self.cameraPosition)
|
||||||
|
let output = AVCapturePhotoOutput()
|
||||||
|
if self.session.canAddOutput(output) {
|
||||||
|
self.session.addOutput(output)
|
||||||
|
self.photoOutput = output
|
||||||
|
}
|
||||||
|
self.session.commitConfiguration()
|
||||||
|
self.didConfigureSession = true
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let layer = AVCaptureVideoPreviewLayer(session: self.session)
|
||||||
|
layer.videoGravity = .resizeAspectFill
|
||||||
|
layer.frame = self.previewHost.bounds
|
||||||
|
self.previewHost.layer.insertSublayer(layer, at: 0)
|
||||||
|
self.previewLayer = layer
|
||||||
|
self.applyVideoOrientation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.applyVideoOrientation()
|
||||||
|
if !self.session.isRunning {
|
||||||
|
self.session.startRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func attachVideoInput(position: AVCaptureDevice.Position) -> Bool {
|
||||||
|
if let videoInput {
|
||||||
|
session.removeInput(videoInput)
|
||||||
|
self.videoInput = nil
|
||||||
|
}
|
||||||
|
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position),
|
||||||
|
let input = try? AVCaptureDeviceInput(device: device),
|
||||||
|
session.canAddInput(input) else { return false }
|
||||||
|
session.addInput(input)
|
||||||
|
videoInput = input
|
||||||
|
cameraPosition = position
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyVideoOrientation() {
|
||||||
|
if let conn = photoOutput?.connection(with: .video), conn.isVideoOrientationSupported {
|
||||||
|
conn.videoOrientation = .portrait
|
||||||
|
if conn.isVideoMirroringSupported {
|
||||||
|
conn.automaticallyAdjustsVideoMirroring = false
|
||||||
|
conn.isVideoMirrored = cameraPosition == .front
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self, let conn = self.previewLayer?.connection, conn.isVideoOrientationSupported else { return }
|
||||||
|
conn.videoOrientation = .portrait
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapFlash() {
|
||||||
|
guard !isReviewing else { return }
|
||||||
|
guard cameraPosition == .back, let device = videoInput?.device, device.hasTorch else {
|
||||||
|
DLToast.show(text: "当前镜头不支持闪光灯")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try device.lockForConfiguration()
|
||||||
|
isTorchOn.toggle()
|
||||||
|
device.torchMode = isTorchOn ? .on : .off
|
||||||
|
device.unlockForConfiguration()
|
||||||
|
flashButton.alpha = isTorchOn ? 1 : 0.72
|
||||||
|
} catch {
|
||||||
|
DLToast.show(text: "闪光灯开启失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapSwitch() {
|
||||||
|
guard !isReviewing else { return }
|
||||||
|
let next: AVCaptureDevice.Position = cameraPosition == .back ? .front : .back
|
||||||
|
sessionQueue.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.session.beginConfiguration()
|
||||||
|
_ = self.attachVideoInput(position: next)
|
||||||
|
if self.isTorchOn, next == .front {
|
||||||
|
self.isTorchOn = false
|
||||||
|
}
|
||||||
|
self.applyVideoOrientation()
|
||||||
|
self.session.commitConfiguration()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.flashButton.alpha = self.isTorchOn ? 1 : 0.72
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapShutter() {
|
||||||
|
guard !isReviewing, !isCapturing, let photoOutput else { return }
|
||||||
|
isCapturing = true
|
||||||
|
let settings = AVCapturePhotoSettings()
|
||||||
|
if let device = videoInput?.device, device.hasFlash, cameraPosition == .back {
|
||||||
|
settings.flashMode = isTorchOn ? .on : .off
|
||||||
|
}
|
||||||
|
photoOutput.capturePhoto(with: settings, delegate: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapRetake() {
|
||||||
|
capturedImage = nil
|
||||||
|
capturedImageView.image = nil
|
||||||
|
isReviewing = false
|
||||||
|
isCapturing = false
|
||||||
|
applyReviewState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapConfirm() {
|
||||||
|
guard let image = makeWatermarkedImage() else { return }
|
||||||
|
onConfirm?(image)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyReviewState(_ reviewing: Bool) {
|
||||||
|
capturedImageView.isHidden = !reviewing
|
||||||
|
watermarkView.isHidden = !reviewing
|
||||||
|
flashButton.isHidden = reviewing
|
||||||
|
shutterButton.isHidden = reviewing
|
||||||
|
switchButton.isHidden = reviewing
|
||||||
|
retakeButton.isHidden = !reviewing
|
||||||
|
confirmButton.isHidden = !reviewing
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshWatermark(at date: Date) {
|
||||||
|
let timeFormatter = DateFormatter()
|
||||||
|
timeFormatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
timeFormatter.dateFormat = "HH:mm"
|
||||||
|
timeLabel.text = timeFormatter.string(from: date)
|
||||||
|
|
||||||
|
let dateFormatter = DateFormatter()
|
||||||
|
dateFormatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
dateFormatter.dateFormat = "yyyy-M-d"
|
||||||
|
dateLabel.text = dateFormatter.string(from: date)
|
||||||
|
|
||||||
|
let weekday = Calendar(identifier: .gregorian).component(.weekday, from: date)
|
||||||
|
let names = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"]
|
||||||
|
weekdayLabel.text = names[max(0, min(6, weekday - 1))]
|
||||||
|
|
||||||
|
let hasAddress = !addressText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
|
locationRow.isHidden = !hasAddress
|
||||||
|
addressLabel.text = addressText
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeWatermarkedImage() -> UIImage? {
|
||||||
|
guard let capturedImage else { return nil }
|
||||||
|
layoutIfNeeded()
|
||||||
|
let target = previewContainer.bounds.size
|
||||||
|
let watermarkSize = watermarkView.bounds.size
|
||||||
|
let watermarkFrame = watermarkView.frame
|
||||||
|
guard target.width > 1, target.height > 1, watermarkSize.width > 1 else { return capturedImage }
|
||||||
|
|
||||||
|
watermarkBlurView.isHidden = true
|
||||||
|
watermarkTintView.isHidden = true
|
||||||
|
let borderWidth = watermarkView.layer.borderWidth
|
||||||
|
watermarkView.layer.borderWidth = 0
|
||||||
|
layoutIfNeeded()
|
||||||
|
|
||||||
|
let scale = UIScreen.main.scale
|
||||||
|
let clearFormat = UIGraphicsImageRendererFormat.default()
|
||||||
|
clearFormat.scale = scale
|
||||||
|
clearFormat.opaque = false
|
||||||
|
let contentImage = UIGraphicsImageRenderer(size: watermarkSize, format: clearFormat).image { ctx in
|
||||||
|
ctx.cgContext.clear(CGRect(origin: .zero, size: watermarkSize))
|
||||||
|
watermarkView.drawHierarchy(
|
||||||
|
in: CGRect(origin: .zero, size: watermarkSize),
|
||||||
|
afterScreenUpdates: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
watermarkBlurView.isHidden = false
|
||||||
|
watermarkTintView.isHidden = false
|
||||||
|
watermarkView.layer.borderWidth = borderWidth
|
||||||
|
|
||||||
|
let photoFormat = UIGraphicsImageRendererFormat.default()
|
||||||
|
photoFormat.scale = scale
|
||||||
|
photoFormat.opaque = true
|
||||||
|
return UIGraphicsImageRenderer(size: target, format: photoFormat).image { _ in
|
||||||
|
Self.drawAspectFill(capturedImage, in: CGRect(origin: .zero, size: target))
|
||||||
|
let card = UIBezierPath(roundedRect: watermarkFrame, cornerRadius: 14)
|
||||||
|
UIColor.white.withAlphaComponent(0.28).setFill()
|
||||||
|
card.fill()
|
||||||
|
UIColor.white.setStroke()
|
||||||
|
card.lineWidth = 1
|
||||||
|
card.stroke()
|
||||||
|
contentImage.draw(in: watermarkFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func drawAspectFill(_ image: UIImage, in rect: CGRect) {
|
||||||
|
let imgSize = image.size
|
||||||
|
guard imgSize.width > 0, imgSize.height > 0 else { return }
|
||||||
|
let scale = max(rect.width / imgSize.width, rect.height / imgSize.height)
|
||||||
|
let scaled = CGSize(width: imgSize.width * scale, height: imgSize.height * scale)
|
||||||
|
let origin = CGPoint(
|
||||||
|
x: rect.midX - scaled.width / 2,
|
||||||
|
y: rect.midY - scaled.height / 2
|
||||||
|
)
|
||||||
|
image.draw(in: CGRect(origin: origin, size: scaled))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var gradientView: PigeonCameraGradientView = {
|
||||||
|
let view = PigeonCameraGradientView()
|
||||||
|
view.isUserInteractionEnabled = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var navView: BaseNavigationView = {
|
||||||
|
BaseNavigationView(title: "水印拍照")
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var previewContainer: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#DDE1E5")
|
||||||
|
view.layer.cornerRadius = 24
|
||||||
|
view.clipsToBounds = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var previewHost: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .black
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var capturedImageView: UIImageView = {
|
||||||
|
let view = UIImageView()
|
||||||
|
view.contentMode = .scaleAspectFill
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.isHidden = true
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var watermarkBlurView: UIVisualEffectView = {
|
||||||
|
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialLight))
|
||||||
|
view.isUserInteractionEnabled = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var watermarkTintView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor.white.withAlphaComponent(0.08)
|
||||||
|
view.isOpaque = false
|
||||||
|
view.isUserInteractionEnabled = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var timeChipView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = UIColor(hexStr: "#00ADFE")
|
||||||
|
view.layer.cornerRadius = 10
|
||||||
|
|
||||||
|
let logo = UIView()
|
||||||
|
logo.backgroundColor = .white
|
||||||
|
logo.layer.cornerRadius = 6
|
||||||
|
|
||||||
|
timeLabel.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
timeLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||||
|
|
||||||
|
view.addSubview(logo)
|
||||||
|
view.addSubview(timeLabel)
|
||||||
|
logo.layoutChain
|
||||||
|
.left(6)
|
||||||
|
.centerY()
|
||||||
|
.width(22)
|
||||||
|
.height(22)
|
||||||
|
timeLabel.layoutChain
|
||||||
|
.leftToRightOfView(logo, offset: 6)
|
||||||
|
.right(8)
|
||||||
|
.top(6)
|
||||||
|
.bottom(6)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var watermarkView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.isOpaque = false
|
||||||
|
view.layer.cornerRadius = 14
|
||||||
|
view.layer.borderWidth = 1
|
||||||
|
view.layer.borderColor = UIColor.white.cgColor
|
||||||
|
view.clipsToBounds = true
|
||||||
|
view.isHidden = true
|
||||||
|
|
||||||
|
view.addSubview(watermarkBlurView)
|
||||||
|
view.addSubview(watermarkTintView)
|
||||||
|
watermarkBlurView.layoutChain.edges()
|
||||||
|
watermarkTintView.layoutChain.edges()
|
||||||
|
|
||||||
|
let dateColumn = UIStackView(arrangedSubviews: [dateLabel, weekdayLabel])
|
||||||
|
dateColumn.axis = .vertical
|
||||||
|
dateColumn.spacing = 1
|
||||||
|
|
||||||
|
let topRow = UIStackView(arrangedSubviews: [timeChipView, dateColumn])
|
||||||
|
topRow.axis = .horizontal
|
||||||
|
topRow.alignment = .center
|
||||||
|
topRow.spacing = 8
|
||||||
|
|
||||||
|
let stack = UIStackView(arrangedSubviews: [topRow, locationRow])
|
||||||
|
stack.axis = .vertical
|
||||||
|
stack.alignment = .fill
|
||||||
|
stack.spacing = 8
|
||||||
|
view.addSubview(stack)
|
||||||
|
stack.layoutChain
|
||||||
|
.top(10)
|
||||||
|
.left(10)
|
||||||
|
.right(12)
|
||||||
|
.bottom(10)
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var timeLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 22, weight: .semibold)
|
||||||
|
label.textColor = .white
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var dateLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#293445")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var weekdayLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#293445")
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var addressLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.font = .systemFont(ofSize: 11, weight: .medium)
|
||||||
|
label.textColor = UIColor(hexStr: "#293445")
|
||||||
|
label.numberOfLines = 2
|
||||||
|
label.lineBreakMode = .byTruncatingTail
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var locationRow: UIView = {
|
||||||
|
let row = UIView()
|
||||||
|
row.isHidden = true
|
||||||
|
let pin = UIImageView(image: UIImage(named: "PigeonMessage/sparkle"))
|
||||||
|
pin.contentMode = .scaleAspectFit
|
||||||
|
row.addSubview(pin)
|
||||||
|
row.addSubview(addressLabel)
|
||||||
|
pin.layoutChain
|
||||||
|
.left()
|
||||||
|
.top(1)
|
||||||
|
.width(11)
|
||||||
|
.height(11)
|
||||||
|
addressLabel.layoutChain
|
||||||
|
.leftToRightOfView(pin, offset: 4)
|
||||||
|
.right()
|
||||||
|
.top()
|
||||||
|
.bottom()
|
||||||
|
return row
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var flashButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "PigeonMessage/flash"), for: .normal)
|
||||||
|
button.alpha = 0.72
|
||||||
|
button.addTarget(self, action: #selector(tapFlash), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var switchButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setImage(UIImage(named: "PigeonMessage/camera_switch"), for: .normal)
|
||||||
|
button.addTarget(self, action: #selector(tapSwitch), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var shutterButton: PigeonShutterButton = {
|
||||||
|
let button = PigeonShutterButton()
|
||||||
|
button.addTarget(self, action: #selector(tapShutter), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var retakeButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setTitle("重拍", for: .normal)
|
||||||
|
button.setTitleColor(UIColor(hexStr: "#13B6F1"), for: .normal)
|
||||||
|
button.titleLabel?.font = FontManager.boboBold(18)
|
||||||
|
button.backgroundColor = .white
|
||||||
|
button.layer.cornerRadius = 16
|
||||||
|
button.layer.borderWidth = 1.5
|
||||||
|
button.layer.borderColor = UIColor(hexStr: "#13B6F1").cgColor
|
||||||
|
button.addTarget(self, action: #selector(tapRetake), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
private lazy var confirmButton: UIButton = {
|
||||||
|
let button = UIButton(type: .custom)
|
||||||
|
button.setTitle("确定", for: .normal)
|
||||||
|
button.setTitleColor(.white, for: .normal)
|
||||||
|
button.titleLabel?.font = FontManager.boboBold(18)
|
||||||
|
button.setBackgroundImage(UIImage(named: "Common/button_bg_2"), for: .normal)
|
||||||
|
button.clipsToBounds = true
|
||||||
|
button.cornerRadius = 16
|
||||||
|
button.addTarget(self, action: #selector(tapConfirm), for: .touchUpInside)
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension PigeonWatermarkCameraView: AVCapturePhotoCaptureDelegate {
|
||||||
|
func photoOutput(
|
||||||
|
_ output: AVCapturePhotoOutput,
|
||||||
|
didFinishProcessingPhoto photo: AVCapturePhoto,
|
||||||
|
error: Error?
|
||||||
|
) {
|
||||||
|
isCapturing = false
|
||||||
|
if let error {
|
||||||
|
DLToast.show(text: "拍照失败:\(error.localizedDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data = photo.fileDataRepresentation(), let image = UIImage(data: data) else {
|
||||||
|
DLToast.show(text: "拍照失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.captureDate = Date()
|
||||||
|
self.capturedImage = image
|
||||||
|
self.capturedImageView.image = image
|
||||||
|
self.refreshWatermark(at: self.captureDate)
|
||||||
|
self.isReviewing = true
|
||||||
|
self.applyReviewState(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PigeonCameraGradientView: UIView {
|
||||||
|
override class var layerClass: AnyClass { CAGradientLayer.self }
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
guard let gradient = layer as? CAGradientLayer else { return }
|
||||||
|
gradient.colors = [
|
||||||
|
UIColor(hexStr: "#BAF4FF").cgColor,
|
||||||
|
UIColor(hexStr: "#BAF4FF", alpha: 0).cgColor
|
||||||
|
]
|
||||||
|
gradient.startPoint = CGPoint(x: 0.5, y: 0)
|
||||||
|
gradient.endPoint = CGPoint(x: 0.5, y: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PigeonShutterButton: UIControl {
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .clear
|
||||||
|
isOpaque = false
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
setNeedsDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func draw(_ rect: CGRect) {
|
||||||
|
let color = UIColor(hexStr: "#13B6F1")
|
||||||
|
let ring = UIBezierPath(ovalIn: rect.insetBy(dx: 3, dy: 3))
|
||||||
|
color.setStroke()
|
||||||
|
ring.lineWidth = 4
|
||||||
|
ring.stroke()
|
||||||
|
color.setFill()
|
||||||
|
UIBezierPath(ovalIn: rect.insetBy(dx: 11, dy: 11)).fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
//
|
||||||
|
// PigeonService.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import RxSwift
|
||||||
|
|
||||||
|
struct PigeonService {
|
||||||
|
static func send(
|
||||||
|
groupKey: String,
|
||||||
|
toUsers: [String],
|
||||||
|
msgType: Int,
|
||||||
|
fileId: String,
|
||||||
|
textMsg: String
|
||||||
|
) -> Observable<ResponseModel> {
|
||||||
|
let api = PigeonAPI.send(
|
||||||
|
groupKey: groupKey,
|
||||||
|
toUsers: toUsers,
|
||||||
|
msgType: msgType,
|
||||||
|
msg: fileId,
|
||||||
|
textMsg: textMsg,
|
||||||
|
expireTime: 0
|
||||||
|
).multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(ResponseModel.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func sent(groupKey: String) -> Observable<PigeonSentResponse> {
|
||||||
|
let api = PigeonAPI.sent(groupKey: groupKey).multiTarget
|
||||||
|
return APIProvider.request(token: api)
|
||||||
|
.map(PigeonSentResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func inbox() -> Observable<PigeonSentResponse> {
|
||||||
|
let api = PigeonAPI.inbox.multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(PigeonSentResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func burn(messageUUID: String) -> Observable<ResponseModel> {
|
||||||
|
let api = PigeonAPI.burn(messageUUID: messageUUID).multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(ResponseModel.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
//
|
||||||
|
// UploadService.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import RxSwift
|
||||||
|
import SwiftyJSON
|
||||||
|
import CommonCrypto
|
||||||
|
|
||||||
|
enum UploadFileKind {
|
||||||
|
case jpeg
|
||||||
|
case mp4
|
||||||
|
case wav
|
||||||
|
|
||||||
|
var fileExtension: String {
|
||||||
|
switch self {
|
||||||
|
case .jpeg: return "jpg"
|
||||||
|
case .mp4: return "mp4"
|
||||||
|
case .wav: return "wav"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimeType: String {
|
||||||
|
switch self {
|
||||||
|
case .jpeg: return "image/jpeg"
|
||||||
|
case .mp4: return "video/mp4"
|
||||||
|
case .wav: return "audio/wav"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UploadService {
|
||||||
|
|
||||||
|
private static let boundary = "YLQH"
|
||||||
|
private static let jiaMiKey = "857d69d374694c1de46486d3bdaaac43"
|
||||||
|
|
||||||
|
static func upload(_ data: Data, kind: UploadFileKind, scene: String? = nil) -> Observable<String> {
|
||||||
|
Observable.create { observer in
|
||||||
|
do {
|
||||||
|
let request = try makeRequest(fileData: data, kind: kind, scene: scene)
|
||||||
|
let session = URLSession(configuration: .default)
|
||||||
|
let task = session.uploadTask(with: request.request, from: request.body) { data, response, error in
|
||||||
|
if let error {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.onError(error)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.onError(makeError("上传失败"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let decrypted = ApiManager.decryptIfNeeded(data) ?? data
|
||||||
|
let json = JSON(decrypted)
|
||||||
|
let code = json["code"].intValue
|
||||||
|
guard code == 0 || json["success"].boolValue else {
|
||||||
|
let message = json["message"].string ?? "上传失败"
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.onError(makeError(message))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let nestedId = json["data"]["id"].stringValue
|
||||||
|
let nestedFileId = json["data"]["file_id"].stringValue
|
||||||
|
let dataString = json["data"].stringValue
|
||||||
|
let fileId: String? = {
|
||||||
|
if !nestedId.isEmpty { return nestedId }
|
||||||
|
if !nestedFileId.isEmpty { return nestedFileId }
|
||||||
|
if !dataString.isEmpty, json["data"].type != .dictionary { return dataString }
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
guard let fileId, !fileId.isEmpty else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.onError(makeError("上传成功但未返回文件 id"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
observer.onNext(fileId)
|
||||||
|
observer.onCompleted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return Disposables.create {
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
observer.onError(error)
|
||||||
|
return Disposables.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeRequest(fileData: Data, kind: UploadFileKind, scene: String?) throws -> (request: URLRequest, body: Data) {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyyMMddHHmmss"
|
||||||
|
let fileName = "\(formatter.string(from: Date())).\(kind.fileExtension)"
|
||||||
|
let body = multipartBody(fileData: fileData, fileName: fileName, mimeType: kind.mimeType)
|
||||||
|
|
||||||
|
var query: [String: String] = [
|
||||||
|
"timestamp": String(format: "%.0lf", Date().timeIntervalSince1970),
|
||||||
|
"nonce": randomString(length: 32)
|
||||||
|
]
|
||||||
|
if let scene, !scene.isEmpty {
|
||||||
|
query["scene"] = scene
|
||||||
|
}
|
||||||
|
let queryString = sortedQueryString(query)
|
||||||
|
let signature = sign(queryString: queryString, body: body)
|
||||||
|
let signedQuery = queryString + "&signature=" + signature
|
||||||
|
let encodedQuery = signedQuery.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? signedQuery
|
||||||
|
|
||||||
|
let base = AppNetworkConfig.shared.baseURL
|
||||||
|
guard let url = URL(string: base + "api/user/upload?" + encodedQuery) else {
|
||||||
|
throw makeError("上传地址无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.timeoutInterval = 180
|
||||||
|
var headers = ApiManager.shared.httpHeader()
|
||||||
|
headers["Content-Type"] = "multipart/form-data; boundary=\(boundary)"
|
||||||
|
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
||||||
|
return (request, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func multipartBody(fileData: Data, fileName: String, mimeType: String) -> Data {
|
||||||
|
var data = Data()
|
||||||
|
func append(_ string: String) {
|
||||||
|
if let part = string.data(using: .utf8) {
|
||||||
|
data.append(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append("--\(boundary)\r\n")
|
||||||
|
append("Content-Disposition:form-data; name=\"file\"; filename=\"\(fileName)\"\r\n")
|
||||||
|
append("Content-Type:\(mimeType)\r\n")
|
||||||
|
append("\r\n")
|
||||||
|
data.append(fileData)
|
||||||
|
append("\r\n")
|
||||||
|
append("--\(boundary)--\r\n")
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sign(queryString: String, body: Data) -> String {
|
||||||
|
var combined = Data()
|
||||||
|
combined.append(Data((queryString + "&").utf8))
|
||||||
|
combined.append(body)
|
||||||
|
combined.append(Data(("&" + md5(jiaMiKey)).utf8))
|
||||||
|
return md5(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func sortedQueryString(_ dict: [String: String]) -> String {
|
||||||
|
dict.sorted { $0.key < $1.key }
|
||||||
|
.map { "\($0.key)=\($0.value)" }
|
||||||
|
.joined(separator: "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func randomString(length: Int) -> String {
|
||||||
|
let chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
return String((0..<length).map { _ in chars.randomElement()! })
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func md5(_ string: String) -> String {
|
||||||
|
md5(Data(string.utf8))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func md5(_ data: Data) -> String {
|
||||||
|
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
|
||||||
|
data.withUnsafeBytes { buffer in
|
||||||
|
_ = CC_MD5(buffer.baseAddress, CC_LONG(data.count), &digest)
|
||||||
|
}
|
||||||
|
return digest.map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func makeError(_ message: String) -> NSError {
|
||||||
|
NSError(
|
||||||
|
domain: "UploadService",
|
||||||
|
code: -1,
|
||||||
|
userInfo: [NSLocalizedDescriptionKey: message]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||