13
Podfile
|
|
@ -2,6 +2,15 @@
|
||||||
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
|
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
|
||||||
platform :ios, '16.0'
|
platform :ios, '16.0'
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
|
|
||||||
|
# AdHoc is a custom configuration. Without this mapping CocoaPods only
|
||||||
|
# generates Debug/Release xcfilelists, and AdHoc builds fail to embed pods.
|
||||||
|
project 'QuickLocation.xcodeproj', {
|
||||||
|
'Debug' => :debug,
|
||||||
|
'Release' => :release,
|
||||||
|
'AdHoc' => :release,
|
||||||
|
}
|
||||||
|
|
||||||
target 'QuickLocation' do
|
target 'QuickLocation' do
|
||||||
# Comment the next line if you don't want to use dynamic frameworks
|
# Comment the next line if you don't want to use dynamic frameworks
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
|
|
@ -139,6 +148,10 @@ post_install do |installer|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if File.basename(path) == 'Pods-QuickLocation.adhoc.xcconfig' && content !~ /SWIFT_ACTIVE_COMPILATION_CONDITIONS/
|
||||||
|
content += "\nSWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) AdHoc\n"
|
||||||
|
end
|
||||||
|
|
||||||
File.write(path, content)
|
File.write(path, content)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||||
|
|
||||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
PODFILE CHECKSUM: 3272194e421676580050601835b59f7c4d288888
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54
|
||||||
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
ZXSDK: 786338c0a18e98e03eda00699c3bfd2700b97117
|
||||||
|
|
||||||
PODFILE CHECKSUM: f676321d820ce55c18e69318732ac59e39a57e43
|
PODFILE CHECKSUM: 3272194e421676580050601835b59f7c4d288888
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,53 @@ code_sign_if_enabled() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if [[ "$CONFIGURATION" == "AdHoc" ]]; then
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/CocoaLumberjack/CocoaLumberjack.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/CocoaMQTT/CocoaMQTT.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Differentiator/Differentiator.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/HXPHPicker/HXPHPicker.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardCore/IQKeyboardCore.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardNotification/IQKeyboardNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardReturnManager/IQKeyboardReturnManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbar/IQKeyboardToolbar.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQKeyboardToolbarManager/IQKeyboardToolbarManager.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextInputViewNotification/IQTextInputViewNotification.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/IQTextView/IQTextView.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/KingfisherWebP/KingfisherWebP.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/MBProgressHUD/MBProgressHUD.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/MJExtension/MJExtension.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/MarqueeLabel/MarqueeLabel.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Moya/Moya.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/MqttCocoaAsyncSocket/MqttCocoaAsyncSocket.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/ObjectMapper/ObjectMapper.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/Popover/Popover.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxDataSources/RxDataSources.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxGesture/RxGesture.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/RxSwiftExt/RxSwiftExt.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SDCycleScrollView/SDCycleScrollView.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SGQRCode/SGQRCode.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SwiftDate/SwiftDate.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SwiftKeychainWrapper/SwiftKeychainWrapper.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/TagListView/TagListView.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/URLNavigator/URLNavigator.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/YBImageBrowser/YBImageBrowser.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework"
|
||||||
|
install_framework "${BUILT_PRODUCTS_DIR}/lottie-ios/Lottie.framework"
|
||||||
|
fi
|
||||||
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
if [[ "$CONFIGURATION" == "Debug" ]]; then
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
|
||||||
install_framework "${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework"
|
install_framework "${BUILT_PRODUCTS_DIR}/BRPickerView/BRPickerView.framework"
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,11 @@ if [[ "$CONFIGURATION" == "Release" ]]; then
|
||||||
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMap.bundle"
|
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMap.bundle"
|
||||||
install_resource "${PODS_ROOT}/AlipaySDK-iOS/AlipaySDK.bundle"
|
install_resource "${PODS_ROOT}/AlipaySDK-iOS/AlipaySDK.bundle"
|
||||||
fi
|
fi
|
||||||
|
if [[ "$CONFIGURATION" == "AdHoc" ]]; then
|
||||||
|
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMapNavi.bundle"
|
||||||
|
install_resource "${PODS_ROOT}/AMapNavi-NO-IDFA/AMapNaviKit.framework/AMap.bundle"
|
||||||
|
install_resource "${PODS_ROOT}/AlipaySDK-iOS/AlipaySDK.bundle"
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||||
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
rsync -avr --copy-links --no-relative --exclude '*/.svn/*' --files-from="$RESOURCES_TO_COPY" / "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,6 @@
|
||||||
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 */; };
|
||||||
|
|
@ -90,7 +87,6 @@
|
||||||
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 */; };
|
||||||
|
|
@ -166,13 +162,6 @@
|
||||||
305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */; };
|
305A79902FCAC61A00227D26 /* InviteMemberVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */; };
|
||||||
3062E8B52FCE6BBA00CEF511 /* ScanVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */; };
|
3062E8B52FCE6BBA00CEF511 /* ScanVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */; };
|
||||||
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B62FCE6BFE00CEF511 /* ScanView.swift */; };
|
3062E8B72FCE6BFE00CEF511 /* ScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B62FCE6BFE00CEF511 /* ScanView.swift */; };
|
||||||
30A1C0A12FEB000100CA0001 /* CameraCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */; };
|
|
||||||
30A1C0A32FEB000100CA0001 /* CameraCaptureVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */; };
|
|
||||||
30B2D0B12FEB111100CA0001 /* PhotoPickerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */; };
|
|
||||||
30B2D0B32FEB111100CA0001 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */; };
|
|
||||||
30B2D0B52FEB111100CA0001 /* PhotoPickerAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */; };
|
|
||||||
30B2D0B72FEB111100CA0001 /* PhotoPickerPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */; };
|
|
||||||
30B2D0B92FEB111100CA0001 /* PhotoPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */; };
|
|
||||||
3062E8BA2FCEAC6500CEF511 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */; };
|
3062E8BA2FCEAC6500CEF511 /* CreateGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */; };
|
||||||
3062E8BC2FCEAC7100CEF511 /* CreateGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */; };
|
3062E8BC2FCEAC7100CEF511 /* CreateGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */; };
|
||||||
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */; };
|
3062E8BE2FCEBD0E00CEF511 /* GroupIconListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */; };
|
||||||
|
|
@ -185,6 +174,8 @@
|
||||||
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E32FD18A20004C37CC /* GroupChatView.swift */; };
|
307073E52FD18A20004C37CC /* GroupChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E32FD18A20004C37CC /* GroupChatView.swift */; };
|
||||||
307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; };
|
307073E62FD18A20004C37CC /* GroupChatVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E22FD18A20004C37CC /* GroupChatVC.swift */; };
|
||||||
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; };
|
307073EA2FD2715A004C37CC /* GroupChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 307073E92FD2715A004C37CC /* GroupChatViewModel.swift */; };
|
||||||
|
30A1C0A12FEB000100CA0001 /* CameraCaptureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */; };
|
||||||
|
30A1C0A32FEB000100CA0001 /* CameraCaptureVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */; };
|
||||||
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */; };
|
30A7A9112FCAEE3D00105780 /* GroupListPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */; };
|
||||||
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */; };
|
30A87A4B2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */; };
|
||||||
30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */; };
|
30A87A4D2FEE1DB40095E7C6 /* ItineraryDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */; };
|
||||||
|
|
@ -208,6 +199,11 @@
|
||||||
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */; };
|
30ACD54E2FF7942E00174861 /* PromotionalActivitiesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */; };
|
||||||
30ACD5502FF7943A00174861 /* PromotionalActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */; };
|
30ACD5502FF7943A00174861 /* PromotionalActivitiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */; };
|
||||||
30ACD67B2FF7B36A00174861 /* PopupQueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */; };
|
30ACD67B2FF7B36A00174861 /* PopupQueueManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */; };
|
||||||
|
30B2D0B12FEB111100CA0001 /* PhotoPickerStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */; };
|
||||||
|
30B2D0B32FEB111100CA0001 /* PhotoPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */; };
|
||||||
|
30B2D0B52FEB111100CA0001 /* PhotoPickerAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */; };
|
||||||
|
30B2D0B72FEB111100CA0001 /* PhotoPickerPreviewVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */; };
|
||||||
|
30B2D0B92FEB111100CA0001 /* PhotoPickerVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */; };
|
||||||
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
|
30B74B3A2FF2115A00F6744D /* GroupScheduleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B392FF2115A00F6744D /* GroupScheduleView.swift */; };
|
||||||
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
|
30B74B3C2FF2117900F6744D /* GroupScheduleVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */; };
|
||||||
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
|
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */; };
|
||||||
|
|
@ -259,7 +255,7 @@
|
||||||
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; };
|
30D74BF42FEB6F5B0050EB2C /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */; };
|
||||||
30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; };
|
30D74D1F2FEBB09B0050EB2C /* CreateScheduleVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */; };
|
||||||
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
|
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */; };
|
||||||
55B240013034000100786001 /* MemberWeatherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023034000100786002 /* MemberWeatherService.swift */; };
|
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0103034101000786010 /* PhoneStatusCollector.swift */; };
|
||||||
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
|
30D87CDD2FDFF07500E958FD /* InteractionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDC2FDFF07500E958FD /* InteractionView.swift */; };
|
||||||
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; };
|
30D87CDF2FDFF1A100E958FD /* QuickMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */; };
|
||||||
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; };
|
30D87D042FE1336300E958FD /* NavigationVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30D87D012FE1336300E958FD /* NavigationVC.swift */; };
|
||||||
|
|
@ -319,6 +315,7 @@
|
||||||
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
|
55B217B13022F0A000784711 /* GroupItineraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217B03022F0A000784710 /* GroupItineraryView.swift */; };
|
||||||
55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; };
|
55B217C13022F0A000784721 /* MinePhotoWallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C03022F0A000784720 /* MinePhotoWallView.swift */; };
|
||||||
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; };
|
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C23022F0A000784722 /* MemberPhoneReportView.swift */; };
|
||||||
|
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */; };
|
||||||
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C43022F0A000784724 /* MemberAppUsageView.swift */; };
|
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217C43022F0A000784724 /* MemberAppUsageView.swift */; };
|
||||||
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; };
|
55B217D13022F0A000784727 /* UnlockCountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D03022F0A000784726 /* UnlockCountManager.swift */; };
|
||||||
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D13023010000784730 /* TodayTrackDetailVC.swift */; };
|
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D13023010000784730 /* TodayTrackDetailVC.swift */; };
|
||||||
|
|
@ -327,22 +324,15 @@
|
||||||
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D73023010000784736 /* PhoneReportDetailVC.swift */; };
|
55B217D83023010000784737 /* PhoneReportDetailVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D73023010000784736 /* PhoneReportDetailVC.swift */; };
|
||||||
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.swift */; };
|
55B217DA3023010000784739 /* PhoneReportDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217D93023010000784738 /* PhoneReportDetailView.swift */; };
|
||||||
55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */; };
|
55B217DC302301000078473B /* PhoneReportDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */; };
|
||||||
55B240013040000100784901 /* AppUsageReportVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240003040000100784900 /* AppUsageReportVC.swift */; };
|
|
||||||
55B240033040000100784903 /* AppUsageReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023040000100784902 /* AppUsageReportView.swift */; };
|
|
||||||
55B240053040000100784905 /* AppUsageReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240043040000100784904 /* AppUsageReportViewModel.swift */; };
|
|
||||||
55B218A13024A00100784702 /* AboutVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784701 /* AboutVC.swift */; };
|
55B218A13024A00100784702 /* AboutVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784701 /* AboutVC.swift */; };
|
||||||
55B218A13024A00100784704 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784703 /* AboutView.swift */; };
|
55B218A13024A00100784704 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784703 /* AboutView.swift */; };
|
||||||
55B218A13024A00100784706 /* CancelAccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784705 /* CancelAccountVC.swift */; };
|
55B218A13024A00100784706 /* CancelAccountVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784705 /* CancelAccountVC.swift */; };
|
||||||
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
|
55B218A13024A00100784708 /* CancelAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784707 /* CancelAccountView.swift */; };
|
||||||
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
|
55B218A13024A0010078470A /* MemberInfoVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A00100784709 /* MemberInfoVC.swift */; };
|
||||||
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
|
55B218A13024A0010078470C /* MemberInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218A13024A0010078470B /* MemberInfoView.swift */; };
|
||||||
55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B233003035000100785040 /* GenderSelectPopVC.swift */; };
|
|
||||||
55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234003036000100785050 /* RelationSelectPopVC.swift */; };
|
|
||||||
55B234033036000100785053 /* RelationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234023036000100785052 /* RelationStore.swift */; };
|
|
||||||
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
|
55B218B13024B00100784722 /* FeatureIntroVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784721 /* FeatureIntroVC.swift */; };
|
||||||
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
|
55B218B13024B00100784724 /* FeatureIntroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B218B13024B00100784723 /* FeatureIntroView.swift */; };
|
||||||
55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; };
|
55B219A2302A000100784751 /* UnlockRequestPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219A1302A000100784750 /* UnlockRequestPopView.swift */; };
|
||||||
55B250023050000100784902 /* LockedAppPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013050000100784901 /* LockedAppPopView.swift */; };
|
|
||||||
55B219B2302A000100784761 /* ReceiveMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B1302A000100784760 /* ReceiveMessagePopView.swift */; };
|
55B219B2302A000100784761 /* ReceiveMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B1302A000100784760 /* ReceiveMessagePopView.swift */; };
|
||||||
55B219B4302A000100784763 /* ReceiveMessageReportPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */; };
|
55B219B4302A000100784763 /* ReceiveMessageReportPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */; };
|
||||||
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
|
55B219C13024C00100784722 /* BubbleHeroView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C13024C00100784721 /* BubbleHeroView.swift */; };
|
||||||
|
|
@ -355,6 +345,7 @@
|
||||||
55B219C8302A000100784778 /* PigeonMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */; };
|
55B219C8302A000100784778 /* PigeonMessageHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */; };
|
||||||
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A01302B0001007847A1 /* SOSAnimationViews.swift */; };
|
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A01302B0001007847A1 /* SOSAnimationViews.swift */; };
|
||||||
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */; };
|
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */; };
|
||||||
|
55B21A06302B0001007847A6 /* SOSListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B21A05302B0001007847A5 /* SOSListViewController.swift */; };
|
||||||
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
|
55B21D6E3023117900784774 /* 优设标题黑_猫啃网.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */; };
|
||||||
55B220013031000100784801 /* FeedbackVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220003031000100784800 /* FeedbackVC.swift */; };
|
55B220013031000100784801 /* FeedbackVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220003031000100784800 /* FeedbackVC.swift */; };
|
||||||
55B220033031000100784803 /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220023031000100784802 /* FeedbackView.swift */; };
|
55B220033031000100784803 /* FeedbackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B220023031000100784802 /* FeedbackView.swift */; };
|
||||||
|
|
@ -365,6 +356,27 @@
|
||||||
55B221270000000100784827 /* MoodStatusCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B221260000000100784826 /* MoodStatusCatalog.swift */; };
|
55B221270000000100784827 /* MoodStatusCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B221260000000100784826 /* MoodStatusCatalog.swift */; };
|
||||||
55B222010000000100784831 /* AddEmojiPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222000000000100784830 /* AddEmojiPopView.swift */; };
|
55B222010000000100784831 /* AddEmojiPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222000000000100784830 /* AddEmojiPopView.swift */; };
|
||||||
55B222030000000100784833 /* AddMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222020000000100784832 /* AddMessagePopView.swift */; };
|
55B222030000000100784833 /* AddMessagePopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B222020000000100784832 /* AddMessagePopView.swift */; };
|
||||||
|
55B230013032000100785002 /* UploadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230003032000100785001 /* UploadService.swift */; };
|
||||||
|
55B230033032000100785004 /* PigeonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230023032000100785003 /* PigeonAPI.swift */; };
|
||||||
|
55B230053032000100785006 /* PigeonService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230043032000100785005 /* PigeonService.swift */; };
|
||||||
|
55B230073032000100785008 /* PigeonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B230063032000100785007 /* PigeonModel.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 */; };
|
||||||
|
55B233013035000100785041 /* GenderSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B233003035000100785040 /* GenderSelectPopVC.swift */; };
|
||||||
|
55B234013036000100785051 /* RelationSelectPopVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234003036000100785050 /* RelationSelectPopVC.swift */; };
|
||||||
|
55B234033036000100785053 /* RelationStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B234023036000100785052 /* RelationStore.swift */; };
|
||||||
|
55B240013034000100786001 /* MemberWeatherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023034000100786002 /* MemberWeatherService.swift */; };
|
||||||
|
55B240013040000100784901 /* AppUsageReportVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240003040000100784900 /* AppUsageReportVC.swift */; };
|
||||||
|
55B240033040000100784903 /* AppUsageReportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240023040000100784902 /* AppUsageReportView.swift */; };
|
||||||
|
55B240053040000100784905 /* AppUsageReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B240043040000100784904 /* AppUsageReportViewModel.swift */; };
|
||||||
|
55B250023038000100786502 /* MailboxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013038000100786501 /* MailboxModel.swift */; };
|
||||||
|
55B250023050000100784902 /* LockedAppPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250013050000100784901 /* LockedAppPopView.swift */; };
|
||||||
|
55B250043038000100786504 /* MailboxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250033038000100786503 /* MailboxView.swift */; };
|
||||||
|
55B250063038000100786506 /* MailboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250053038000100786505 /* MailboxViewController.swift */; };
|
||||||
|
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B250073038000100786507 /* MailboxDetailViewController.swift */; };
|
||||||
|
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B261000000000100786600 /* PrivacySettingsViewController.swift */; };
|
||||||
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
55BF752D2FFE53F70055DA57 /* LocationPermissionPopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */; };
|
||||||
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
55BF75352FFF91690055DA57 /* InteractionEmojiCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */; };
|
||||||
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
|
55C219C13024C00100784731 /* SearchLocationHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C219C13024C00100784730 /* SearchLocationHeaderView.swift */; };
|
||||||
|
|
@ -386,10 +398,6 @@
|
||||||
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 */; };
|
||||||
|
|
@ -437,6 +445,7 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-QuickLocation.adhoc.xcconfig"; path = "Target Support Files/Pods-QuickLocation/Pods-QuickLocation.adhoc.xcconfig"; sourceTree = "<group>"; };
|
||||||
08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShieldConfigurationExtension.swift; path = ShieldConfigurationExtension/ShieldConfigurationExtension.swift; sourceTree = "<group>"; };
|
08002267FC4390B20D3510B3 /* ShieldConfigurationExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ShieldConfigurationExtension.swift; path = ShieldConfigurationExtension/ShieldConfigurationExtension.swift; sourceTree = "<group>"; };
|
||||||
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShieldConfigurationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
0C47E0012491DB1371FD5E53 /* ShieldConfigurationExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShieldConfigurationExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShared.swift; path = AppRestrictShared/AppRestrictShared.swift; sourceTree = "<group>"; };
|
1AB29D0618C4BE76F2CA5261 /* AppRestrictShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AppRestrictShared.swift; path = AppRestrictShared/AppRestrictShared.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -452,9 +461,6 @@
|
||||||
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>"; };
|
||||||
|
|
@ -526,7 +532,6 @@
|
||||||
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>"; };
|
||||||
|
|
@ -607,13 +612,6 @@
|
||||||
305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteMemberVC.swift; sourceTree = "<group>"; };
|
305A798F2FCAC61A00227D26 /* InviteMemberVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteMemberVC.swift; sourceTree = "<group>"; };
|
||||||
3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanVC.swift; sourceTree = "<group>"; };
|
3062E8B42FCE6BBA00CEF511 /* ScanVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanVC.swift; sourceTree = "<group>"; };
|
||||||
3062E8B62FCE6BFE00CEF511 /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = "<group>"; };
|
3062E8B62FCE6BFE00CEF511 /* ScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanView.swift; sourceTree = "<group>"; };
|
||||||
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureView.swift; sourceTree = "<group>"; };
|
|
||||||
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureVC.swift; sourceTree = "<group>"; };
|
|
||||||
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerStore.swift; sourceTree = "<group>"; };
|
|
||||||
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
|
|
||||||
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlbumView.swift; sourceTree = "<group>"; };
|
|
||||||
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerPreviewVC.swift; sourceTree = "<group>"; };
|
|
||||||
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerVC.swift; sourceTree = "<group>"; };
|
|
||||||
3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
|
3062E8B92FCEAC6500CEF511 /* CreateGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupView.swift; sourceTree = "<group>"; };
|
||||||
3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupVC.swift; sourceTree = "<group>"; };
|
3062E8BB2FCEAC7100CEF511 /* CreateGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateGroupVC.swift; sourceTree = "<group>"; };
|
||||||
3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupIconListVC.swift; sourceTree = "<group>"; };
|
3062E8BD2FCEBD0E00CEF511 /* GroupIconListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupIconListVC.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -626,6 +624,8 @@
|
||||||
307073E22FD18A20004C37CC /* GroupChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatVC.swift; sourceTree = "<group>"; };
|
307073E22FD18A20004C37CC /* GroupChatVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatVC.swift; sourceTree = "<group>"; };
|
||||||
307073E32FD18A20004C37CC /* GroupChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatView.swift; sourceTree = "<group>"; };
|
307073E32FD18A20004C37CC /* GroupChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatView.swift; sourceTree = "<group>"; };
|
||||||
307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = "<group>"; };
|
307073E92FD2715A004C37CC /* GroupChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureView.swift; sourceTree = "<group>"; };
|
||||||
|
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCaptureVC.swift; sourceTree = "<group>"; };
|
||||||
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; };
|
30A7A9102FCAEE3D00105780 /* GroupListPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupListPopView.swift; sourceTree = "<group>"; };
|
||||||
30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailVC.swift; sourceTree = "<group>"; };
|
30A87A4A2FEE1DAB0095E7C6 /* ItineraryDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailVC.swift; sourceTree = "<group>"; };
|
||||||
30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailView.swift; sourceTree = "<group>"; };
|
30A87A4C2FEE1DB40095E7C6 /* ItineraryDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItineraryDetailView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -649,6 +649,11 @@
|
||||||
30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesVC.swift; sourceTree = "<group>"; };
|
30ACD54D2FF7942E00174861 /* PromotionalActivitiesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesVC.swift; sourceTree = "<group>"; };
|
||||||
30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesView.swift; sourceTree = "<group>"; };
|
30ACD54F2FF7943A00174861 /* PromotionalActivitiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalActivitiesView.swift; sourceTree = "<group>"; };
|
||||||
30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupQueueManager.swift; sourceTree = "<group>"; };
|
30ACD67A2FF7B36A00174861 /* PopupQueueManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupQueueManager.swift; sourceTree = "<group>"; };
|
||||||
|
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerStore.swift; sourceTree = "<group>"; };
|
||||||
|
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerView.swift; sourceTree = "<group>"; };
|
||||||
|
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerAlbumView.swift; sourceTree = "<group>"; };
|
||||||
|
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerPreviewVC.swift; sourceTree = "<group>"; };
|
||||||
|
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPickerVC.swift; sourceTree = "<group>"; };
|
||||||
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
|
30B74B392FF2115A00F6744D /* GroupScheduleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleView.swift; sourceTree = "<group>"; };
|
||||||
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
|
30B74B3B2FF2117900F6744D /* GroupScheduleVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupScheduleVC.swift; sourceTree = "<group>"; };
|
||||||
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
|
30B74B402FF2437E00F6744D /* GroupMemberListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberListVC.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -701,7 +706,7 @@
|
||||||
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
|
30D74BF12FEB6F5B0050EB2C /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
|
||||||
30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; };
|
30D74D1E2FEBB09B0050EB2C /* CreateScheduleVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateScheduleVM.swift; sourceTree = "<group>"; };
|
||||||
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
|
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTService.swift; sourceTree = "<group>"; };
|
||||||
55B240023034000100786002 /* MemberWeatherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberWeatherService.swift; sourceTree = "<group>"; };
|
55C1A0103034101000786010 /* PhoneStatusCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneStatusCollector.swift; sourceTree = "<group>"; };
|
||||||
30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; };
|
30D87CDC2FDFF07500E958FD /* InteractionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionView.swift; sourceTree = "<group>"; };
|
||||||
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMessageView.swift; sourceTree = "<group>"; };
|
30D87CDE2FDFF1A100E958FD /* QuickMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickMessageView.swift; sourceTree = "<group>"; };
|
||||||
30D87D012FE1336300E958FD /* NavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationVC.swift; sourceTree = "<group>"; };
|
30D87D012FE1336300E958FD /* NavigationVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationVC.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -765,6 +770,7 @@
|
||||||
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
|
55B217B03022F0A000784710 /* GroupItineraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupItineraryView.swift; sourceTree = "<group>"; };
|
||||||
55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; };
|
55B217C03022F0A000784720 /* MinePhotoWallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinePhotoWallView.swift; sourceTree = "<group>"; };
|
||||||
55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; };
|
55B217C23022F0A000784722 /* MemberPhoneReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberPhoneReportView.swift; sourceTree = "<group>"; };
|
||||||
|
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivilegeDeniedOverlay.swift; sourceTree = "<group>"; };
|
||||||
55B217C43022F0A000784724 /* MemberAppUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAppUsageView.swift; sourceTree = "<group>"; };
|
55B217C43022F0A000784724 /* MemberAppUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberAppUsageView.swift; sourceTree = "<group>"; };
|
||||||
55B217D03022F0A000784726 /* UnlockCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockCountManager.swift; sourceTree = "<group>"; };
|
55B217D03022F0A000784726 /* UnlockCountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockCountManager.swift; sourceTree = "<group>"; };
|
||||||
55B217D13023010000784730 /* TodayTrackDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayTrackDetailVC.swift; sourceTree = "<group>"; };
|
55B217D13023010000784730 /* TodayTrackDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayTrackDetailVC.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -773,22 +779,15 @@
|
||||||
55B217D73023010000784736 /* PhoneReportDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailVC.swift; sourceTree = "<group>"; };
|
55B217D73023010000784736 /* PhoneReportDetailVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailVC.swift; sourceTree = "<group>"; };
|
||||||
55B217D93023010000784738 /* PhoneReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailView.swift; sourceTree = "<group>"; };
|
55B217D93023010000784738 /* PhoneReportDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailView.swift; sourceTree = "<group>"; };
|
||||||
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailViewModel.swift; sourceTree = "<group>"; };
|
55B217DB302301000078473A /* PhoneReportDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneReportDetailViewModel.swift; sourceTree = "<group>"; };
|
||||||
55B240003040000100784900 /* AppUsageReportVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportVC.swift; sourceTree = "<group>"; };
|
|
||||||
55B240023040000100784902 /* AppUsageReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportView.swift; sourceTree = "<group>"; };
|
|
||||||
55B240043040000100784904 /* AppUsageReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportViewModel.swift; sourceTree = "<group>"; };
|
|
||||||
55B218A13024A00100784701 /* AboutVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutVC.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784701 /* AboutVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutVC.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A00100784703 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784703 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A00100784705 /* CancelAccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountVC.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784705 /* CancelAccountVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountVC.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784707 /* CancelAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelAccountView.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
|
55B218A13024A00100784709 /* MemberInfoVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoVC.swift; sourceTree = "<group>"; };
|
||||||
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
|
55B218A13024A0010078470B /* MemberInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberInfoView.swift; sourceTree = "<group>"; };
|
||||||
55B233003035000100785040 /* GenderSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderSelectPopVC.swift; sourceTree = "<group>"; };
|
|
||||||
55B234003036000100785050 /* RelationSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationSelectPopVC.swift; sourceTree = "<group>"; };
|
|
||||||
55B234023036000100785052 /* RelationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationStore.swift; sourceTree = "<group>"; };
|
|
||||||
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
55B218B13024B00100784721 /* FeatureIntroVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroVC.swift; sourceTree = "<group>"; };
|
||||||
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
55B218B13024B00100784723 /* FeatureIntroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureIntroView.swift; sourceTree = "<group>"; };
|
||||||
55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = "<group>"; };
|
55B219A1302A000100784750 /* UnlockRequestPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnlockRequestPopView.swift; sourceTree = "<group>"; };
|
||||||
55B250013050000100784901 /* LockedAppPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedAppPopView.swift; sourceTree = "<group>"; };
|
|
||||||
55B219B1302A000100784760 /* ReceiveMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessagePopView.swift; sourceTree = "<group>"; };
|
55B219B1302A000100784760 /* ReceiveMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessagePopView.swift; sourceTree = "<group>"; };
|
||||||
55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessageReportPopView.swift; sourceTree = "<group>"; };
|
55B219B3302A000100784762 /* ReceiveMessageReportPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveMessageReportPopView.swift; sourceTree = "<group>"; };
|
||||||
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
|
55B219C13024C00100784721 /* BubbleHeroView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleHeroView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -801,6 +800,7 @@
|
||||||
55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonMessageHistoryView.swift; sourceTree = "<group>"; };
|
55B219C7302A000100784777 /* PigeonMessageHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonMessageHistoryView.swift; sourceTree = "<group>"; };
|
||||||
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSAnimationViews.swift; sourceTree = "<group>"; };
|
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSAnimationViews.swift; sourceTree = "<group>"; };
|
||||||
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSReceiveAlertView.swift; sourceTree = "<group>"; };
|
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSReceiveAlertView.swift; sourceTree = "<group>"; };
|
||||||
|
55B21A05302B0001007847A5 /* SOSListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOSListViewController.swift; sourceTree = "<group>"; };
|
||||||
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
|
55B21D6D3023117900784774 /* 优设标题黑_猫啃网.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "优设标题黑_猫啃网.ttf"; sourceTree = "<group>"; };
|
||||||
55B220003031000100784800 /* FeedbackVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackVC.swift; sourceTree = "<group>"; };
|
55B220003031000100784800 /* FeedbackVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackVC.swift; sourceTree = "<group>"; };
|
||||||
55B220023031000100784802 /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = "<group>"; };
|
55B220023031000100784802 /* FeedbackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -811,6 +811,27 @@
|
||||||
55B221260000000100784826 /* MoodStatusCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodStatusCatalog.swift; sourceTree = "<group>"; };
|
55B221260000000100784826 /* MoodStatusCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoodStatusCatalog.swift; sourceTree = "<group>"; };
|
||||||
55B222000000000100784830 /* AddEmojiPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiPopView.swift; sourceTree = "<group>"; };
|
55B222000000000100784830 /* AddEmojiPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEmojiPopView.swift; sourceTree = "<group>"; };
|
||||||
55B222020000000100784832 /* AddMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMessagePopView.swift; sourceTree = "<group>"; };
|
55B222020000000100784832 /* AddMessagePopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddMessagePopView.swift; sourceTree = "<group>"; };
|
||||||
|
55B230003032000100785001 /* UploadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadService.swift; sourceTree = "<group>"; };
|
||||||
|
55B230023032000100785003 /* PigeonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonAPI.swift; sourceTree = "<group>"; };
|
||||||
|
55B230043032000100785005 /* PigeonService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonService.swift; sourceTree = "<group>"; };
|
||||||
|
55B230063032000100785007 /* PigeonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PigeonModel.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>"; };
|
||||||
|
55B233003035000100785040 /* GenderSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenderSelectPopVC.swift; sourceTree = "<group>"; };
|
||||||
|
55B234003036000100785050 /* RelationSelectPopVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationSelectPopVC.swift; sourceTree = "<group>"; };
|
||||||
|
55B234023036000100785052 /* RelationStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelationStore.swift; sourceTree = "<group>"; };
|
||||||
|
55B240003040000100784900 /* AppUsageReportVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportVC.swift; sourceTree = "<group>"; };
|
||||||
|
55B240023034000100786002 /* MemberWeatherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberWeatherService.swift; sourceTree = "<group>"; };
|
||||||
|
55B240023040000100784902 /* AppUsageReportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportView.swift; sourceTree = "<group>"; };
|
||||||
|
55B240043040000100784904 /* AppUsageReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUsageReportViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
55B250013038000100786501 /* MailboxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxModel.swift; sourceTree = "<group>"; };
|
||||||
|
55B250013050000100784901 /* LockedAppPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedAppPopView.swift; sourceTree = "<group>"; };
|
||||||
|
55B250033038000100786503 /* MailboxView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxView.swift; sourceTree = "<group>"; };
|
||||||
|
55B250053038000100786505 /* MailboxViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxViewController.swift; sourceTree = "<group>"; };
|
||||||
|
55B250073038000100786507 /* MailboxDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailboxDetailViewController.swift; sourceTree = "<group>"; };
|
||||||
|
55B261000000000100786600 /* PrivacySettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewController.swift; sourceTree = "<group>"; };
|
||||||
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
55BF752C2FFE53F70055DA57 /* LocationPermissionPopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPermissionPopView.swift; sourceTree = "<group>"; };
|
||||||
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
55BF75342FFF91690055DA57 /* InteractionEmojiCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionEmojiCell.swift; sourceTree = "<group>"; };
|
||||||
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
|
55C219C13024C00100784730 /* SearchLocationHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLocationHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -825,10 +846,6 @@
|
||||||
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>"; };
|
||||||
|
|
@ -1010,6 +1027,7 @@
|
||||||
305A74E72FCA8C7000227D26 /* Component */ = {
|
305A74E72FCA8C7000227D26 /* Component */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
55C1A0203034102000786020 /* PrivilegeDeniedOverlay.swift */,
|
||||||
30A1C0A42FEB000100CA0001 /* CameraCapture */,
|
30A1C0A42FEB000100CA0001 /* CameraCapture */,
|
||||||
30B2D0BA2FEB111100CA0001 /* PhotoPicker */,
|
30B2D0BA2FEB111100CA0001 /* PhotoPicker */,
|
||||||
305A74D72FCA8C7000227D26 /* AddImageCell */,
|
305A74D72FCA8C7000227D26 /* AddImageCell */,
|
||||||
|
|
@ -1020,27 +1038,6 @@
|
||||||
path = Component;
|
path = Component;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
30A1C0A42FEB000100CA0001 /* CameraCapture */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */,
|
|
||||||
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */,
|
|
||||||
);
|
|
||||||
path = CameraCapture;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
30B2D0BA2FEB111100CA0001 /* PhotoPicker */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */,
|
|
||||||
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */,
|
|
||||||
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */,
|
|
||||||
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */,
|
|
||||||
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */,
|
|
||||||
);
|
|
||||||
path = PhotoPicker;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
305A74EA2FCA8C7000227D26 /* RTL */ = {
|
305A74EA2FCA8C7000227D26 /* RTL */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1238,6 +1235,7 @@
|
||||||
305A75302FCA8C7000227D26 /* Model */ = {
|
305A75302FCA8C7000227D26 /* Model */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
55B250013038000100786501 /* MailboxModel.swift */,
|
||||||
305A752E2FCA8C7000227D26 /* GroupModel.swift */,
|
305A752E2FCA8C7000227D26 /* GroupModel.swift */,
|
||||||
55B230063032000100785007 /* PigeonModel.swift */,
|
55B230063032000100785007 /* PigeonModel.swift */,
|
||||||
305A752F2FCA8C7000227D26 /* SystemResponse.swift */,
|
305A752F2FCA8C7000227D26 /* SystemResponse.swift */,
|
||||||
|
|
@ -1294,6 +1292,7 @@
|
||||||
305A76262FCA8C7000227D26 /* Group */ = {
|
305A76262FCA8C7000227D26 /* Group */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
55B250093038000100786509 /* Mailbox */,
|
||||||
307073E02FD15F50004C37CC /* GroupIMService.swift */,
|
307073E02FD15F50004C37CC /* GroupIMService.swift */,
|
||||||
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
|
305A76242FCA8C7000227D26 /* GroupViewController.swift */,
|
||||||
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
305A76252FCA8C7000227D26 /* GroupViewModel.swift */,
|
||||||
|
|
@ -1393,6 +1392,7 @@
|
||||||
55B218A13024A00100784711 /* CancelAccount */,
|
55B218A13024A00100784711 /* CancelAccount */,
|
||||||
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
|
30EFF3D42FDA8EF200EB35D4 /* EmergencyContact */,
|
||||||
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
|
30EFF3E32FDAA92200EB35D4 /* PrivacyPolicy */,
|
||||||
|
55B261020000000100786602 /* PrivacySettings */,
|
||||||
30C4C0122FDB9178009215C1 /* CheckPermission */,
|
30C4C0122FDB9178009215C1 /* CheckPermission */,
|
||||||
);
|
);
|
||||||
path = Mine;
|
path = Mine;
|
||||||
|
|
@ -1644,6 +1644,15 @@
|
||||||
path = GroupChat;
|
path = GroupChat;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
30A1C0A42FEB000100CA0001 /* CameraCapture */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
30A1C0A02FEB000100CA0001 /* CameraCaptureView.swift */,
|
||||||
|
30A1C0A22FEB000100CA0001 /* CameraCaptureVC.swift */,
|
||||||
|
);
|
||||||
|
path = CameraCapture;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */ = {
|
30A87A492FEE1CB20095E7C6 /* ItineraryDetail */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1714,6 +1723,18 @@
|
||||||
path = PopupWindow;
|
path = PopupWindow;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
30B2D0BA2FEB111100CA0001 /* PhotoPicker */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
30B2D0B02FEB111100CA0001 /* PhotoPickerStore.swift */,
|
||||||
|
30B2D0B22FEB111100CA0001 /* PhotoPickerView.swift */,
|
||||||
|
30B2D0B42FEB111100CA0001 /* PhotoPickerAlbumView.swift */,
|
||||||
|
30B2D0B62FEB111100CA0001 /* PhotoPickerPreviewVC.swift */,
|
||||||
|
30B2D0B82FEB111100CA0001 /* PhotoPickerVC.swift */,
|
||||||
|
);
|
||||||
|
path = PhotoPicker;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
|
30B74B382FF2105C00F6744D /* GroupSchedule */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -1828,6 +1849,7 @@
|
||||||
30CCDE5B2FE3A1A800F5214A /* SOSPracticeView.swift */,
|
30CCDE5B2FE3A1A800F5214A /* SOSPracticeView.swift */,
|
||||||
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */,
|
55B21A01302B0001007847A1 /* SOSAnimationViews.swift */,
|
||||||
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */,
|
55B21A03302B0001007847A3 /* SOSReceiveAlertView.swift */,
|
||||||
|
55B21A05302B0001007847A5 /* SOSListViewController.swift */,
|
||||||
);
|
);
|
||||||
path = SOS;
|
path = SOS;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -1875,6 +1897,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */,
|
30D87CDA2FDFA9EE00E958FD /* MQTTService.swift */,
|
||||||
|
55C1A0103034101000786010 /* PhoneStatusCollector.swift */,
|
||||||
55B240023034000100786002 /* MemberWeatherService.swift */,
|
55B240023034000100786002 /* MemberWeatherService.swift */,
|
||||||
);
|
);
|
||||||
path = MQTT;
|
path = MQTT;
|
||||||
|
|
@ -2055,16 +2078,6 @@
|
||||||
path = PhoneReportDetail;
|
path = PhoneReportDetail;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
55B240063040000100784906 /* AppUsageReport */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
55B240003040000100784900 /* AppUsageReportVC.swift */,
|
|
||||||
55B240023040000100784902 /* AppUsageReportView.swift */,
|
|
||||||
55B240043040000100784904 /* AppUsageReportViewModel.swift */,
|
|
||||||
);
|
|
||||||
path = AppUsageReport;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
55B218A13024A00100784710 /* About */ = {
|
55B218A13024A00100784710 /* About */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -2138,6 +2151,34 @@
|
||||||
path = MoodStatus;
|
path = MoodStatus;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
55B240063040000100784906 /* AppUsageReport */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55B240003040000100784900 /* AppUsageReportVC.swift */,
|
||||||
|
55B240023040000100784902 /* AppUsageReportView.swift */,
|
||||||
|
55B240043040000100784904 /* AppUsageReportViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = AppUsageReport;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
55B250093038000100786509 /* Mailbox */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55B250053038000100786505 /* MailboxViewController.swift */,
|
||||||
|
55B250033038000100786503 /* MailboxView.swift */,
|
||||||
|
55B250073038000100786507 /* MailboxDetailViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Mailbox;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
55B261020000000100786602 /* PrivacySettings */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
55B261000000000100786600 /* PrivacySettingsViewController.swift */,
|
||||||
|
);
|
||||||
|
path = PrivacySettings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
74B9CD2ABDBC4C8CE3107CDE /* iOS */ = {
|
74B9CD2ABDBC4C8CE3107CDE /* iOS */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -2151,6 +2192,7 @@
|
||||||
children = (
|
children = (
|
||||||
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */,
|
DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */,
|
||||||
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */,
|
E483B9929C03809ADEDE8341 /* Pods-QuickLocation.release.xcconfig */,
|
||||||
|
01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -2397,6 +2439,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
55B261010000000100786601 /* PrivacySettingsViewController.swift in Sources */,
|
||||||
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */,
|
30B74B452FF24D1B00F6744D /* GroupMemberListVM.swift in Sources */,
|
||||||
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
|
30A87A712FEF7BED0095E7C6 /* SearchLocationResultView.swift in Sources */,
|
||||||
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
|
305A76882FCA8C7000227D26 /* MoyaProvider+Rx.swift in Sources */,
|
||||||
|
|
@ -2412,6 +2455,7 @@
|
||||||
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 */,
|
||||||
|
55B250023038000100786502 /* MailboxModel.swift in Sources */,
|
||||||
55B230033032000100785004 /* PigeonAPI.swift in Sources */,
|
55B230033032000100785004 /* PigeonAPI.swift in Sources */,
|
||||||
55B230013032000100785002 /* UploadService.swift in Sources */,
|
55B230013032000100785002 /* UploadService.swift in Sources */,
|
||||||
55B230053032000100785006 /* PigeonService.swift in Sources */,
|
55B230053032000100785006 /* PigeonService.swift in Sources */,
|
||||||
|
|
@ -2500,6 +2544,7 @@
|
||||||
30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */,
|
30CCDE582FE39F8C00F5214A /* SOSView.swift in Sources */,
|
||||||
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */,
|
55B21A02302B0001007847A2 /* SOSAnimationViews.swift in Sources */,
|
||||||
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */,
|
55B21A04302B0001007847A4 /* SOSReceiveAlertView.swift in Sources */,
|
||||||
|
55B21A06302B0001007847A6 /* SOSListViewController.swift in Sources */,
|
||||||
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
|
30EFF3E02FDA9CE300EB35D4 /* EmergencyContactModel.swift in Sources */,
|
||||||
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
305A76B62FCA8C7000227D26 /* UITextField+Extensions.swift in Sources */,
|
||||||
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
30EFF3A82FD7C6A400EB35D4 /* GroupSettingViewModel.swift in Sources */,
|
||||||
|
|
@ -2537,6 +2582,7 @@
|
||||||
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */,
|
55B2179330218CE100784774 /* GroupMemberView2.swift in Sources */,
|
||||||
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */,
|
55B217C53022F0A000784725 /* MemberAppUsageView.swift in Sources */,
|
||||||
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */,
|
55B217C33022F0A000784723 /* MemberPhoneReportView.swift in Sources */,
|
||||||
|
55C1A0213034102000786021 /* PrivilegeDeniedOverlay.swift in Sources */,
|
||||||
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */,
|
55B217D23023010000784731 /* TodayTrackDetailVC.swift in Sources */,
|
||||||
55B217D43023010000784733 /* TodayTrackDetailView.swift in Sources */,
|
55B217D43023010000784733 /* TodayTrackDetailView.swift in Sources */,
|
||||||
55B217D63023010000784735 /* TodayTrackDetailViewModel.swift in Sources */,
|
55B217D63023010000784735 /* TodayTrackDetailViewModel.swift in Sources */,
|
||||||
|
|
@ -2630,6 +2676,9 @@
|
||||||
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
30EFF3BB2FD90D7600EB35D4 /* ConfirmPopVC.swift in Sources */,
|
||||||
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
|
30BAB8512FCD331C00C33B5C /* GroupAPI.swift in Sources */,
|
||||||
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
|
305A76E12FCA8C7000227D26 /* GroupViewController.swift in Sources */,
|
||||||
|
55B250043038000100786504 /* MailboxView.swift in Sources */,
|
||||||
|
55B250063038000100786506 /* MailboxViewController.swift in Sources */,
|
||||||
|
55B250083038000100786508 /* MailboxDetailViewController.swift in Sources */,
|
||||||
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */,
|
30D74AB22FEA1D5D0050EB2C /* ScheduleViewModel.swift in Sources */,
|
||||||
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */,
|
30EFF3B52FD8F1D000EB35D4 /* ReviewMemberListVC.swift in Sources */,
|
||||||
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */,
|
30BAB84D2FCD2FDE00C33B5C /* InviteJoinView.swift in Sources */,
|
||||||
|
|
@ -2700,6 +2749,7 @@
|
||||||
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */,
|
30B74B412FF2437E00F6744D /* GroupMemberListVC.swift in Sources */,
|
||||||
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
|
305A76FD2FCA8C7000227D26 /* EmptyDataSetSource.swift in Sources */,
|
||||||
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
|
30D87CDB2FDFA9EE00E958FD /* MQTTService.swift in Sources */,
|
||||||
|
55C1A0113034101000786011 /* PhoneStatusCollector.swift in Sources */,
|
||||||
55B240013034000100786001 /* MemberWeatherService.swift in Sources */,
|
55B240013034000100786001 /* MemberWeatherService.swift in Sources */,
|
||||||
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
|
30EFF3CD2FDA668A00EB35D4 /* MyProfileView.swift in Sources */,
|
||||||
305A76FE2FCA8C7000227D26 /* EmptyDataSetView.swift in Sources */,
|
305A76FE2FCA8C7000227D26 /* EmptyDataSetView.swift in Sources */,
|
||||||
|
|
@ -2922,6 +2972,7 @@
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "AdHoc $(inherited)";
|
||||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|
@ -3167,7 +3218,7 @@
|
||||||
};
|
};
|
||||||
559B7BA33037017D0031C511 /* AdHoc */ = {
|
559B7BA33037017D0031C511 /* AdHoc */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = DA16D49AA46D4F6838340B55 /* Pods-QuickLocation.debug.xcconfig */;
|
baseConfigurationReference = 01BC26D9997CBD9CF070EA6A /* Pods-QuickLocation.adhoc.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
|
@ -3225,9 +3276,12 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = DeviceActivityMonitorExtension/DeviceActivityMonitorExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = LRDLWZ2Y83;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = DeviceActivityMonitorExtension/Info.plist;
|
INFOPLIST_FILE = DeviceActivityMonitorExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
|
@ -3239,6 +3293,8 @@
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.DeviceActivityMonitor;
|
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.DeviceActivityMonitor;
|
||||||
PRODUCT_NAME = DeviceActivityMonitorExtension;
|
PRODUCT_NAME = DeviceActivityMonitorExtension;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "jsdw DeviceActivityMonitor AdHoc";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||||
|
|
@ -3253,9 +3309,12 @@
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CLANG_ENABLE_OBJC_WEAK = NO;
|
CLANG_ENABLE_OBJC_WEAK = NO;
|
||||||
CODE_SIGN_ENTITLEMENTS = ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShieldConfigurationExtension/ShieldConfigurationExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_IDENTITY = "iPhone Distribution";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = LRDLWZ2Y83;
|
DEVELOPMENT_TEAM = "";
|
||||||
|
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = LRDLWZ2Y83;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = ShieldConfigurationExtension/Info.plist;
|
INFOPLIST_FILE = ShieldConfigurationExtension/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
|
@ -3267,6 +3326,8 @@
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.ShieldConfiguration;
|
PRODUCT_BUNDLE_IDENTIFIER = cn.zuomeng.jisuloca.ShieldConfiguration;
|
||||||
PRODUCT_NAME = ShieldConfigurationExtension;
|
PRODUCT_NAME = ShieldConfigurationExtension;
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "jsdw ShieldConfiguration AdHoc";
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
SWIFT_DEFAULT_ACTOR_ISOLATION = nonisolated;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "2660"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
buildArchitectures = "Automatic">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3E4359072FC48D26003470A5"
|
||||||
|
BuildableName = "QuickLocation.app"
|
||||||
|
BlueprintName = "QuickLocation"
|
||||||
|
ReferencedContainer = "container:QuickLocation.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
shouldAutocreateTestPlan = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES"
|
||||||
|
queueDebuggingEnableBacktraceRecording = "Yes">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3E4359072FC48D26003470A5"
|
||||||
|
BuildableName = "QuickLocation.app"
|
||||||
|
BlueprintName = "QuickLocation"
|
||||||
|
ReferencedContainer = "container:QuickLocation.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "3E4359072FC48D26003470A5"
|
||||||
|
BuildableName = "QuickLocation.app"
|
||||||
|
BlueprintName = "QuickLocation"
|
||||||
|
ReferencedContainer = "container:QuickLocation.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
buildConfiguration = "Debug">
|
buildConfiguration = "Debug">
|
||||||
</AnalyzeAction>
|
</AnalyzeAction>
|
||||||
<ArchiveAction
|
<ArchiveAction
|
||||||
buildConfiguration = "Release"
|
buildConfiguration = "AdHoc"
|
||||||
revealArchiveInOrganizer = "YES">
|
revealArchiveInOrganizer = "YES">
|
||||||
</ArchiveAction>
|
</ArchiveAction>
|
||||||
</Scheme>
|
</Scheme>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ enum SystemAPI {
|
||||||
|
|
||||||
/// SOS
|
/// SOS
|
||||||
case sos(enable: Bool)
|
case sos(enable: Bool)
|
||||||
|
|
||||||
|
case sosList
|
||||||
|
|
||||||
/// 查找
|
/// 查找
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|
@ -49,6 +51,8 @@ extension SystemAPI: MultiTargetProtocol {
|
||||||
return "api/weixin/service"
|
return "api/weixin/service"
|
||||||
case .sos:
|
case .sos:
|
||||||
return "mapi/sos/operate"
|
return "mapi/sos/operate"
|
||||||
|
case .sosList:
|
||||||
|
return "mapi/sos/getlist"
|
||||||
case .search:
|
case .search:
|
||||||
return "mapi/car/search"
|
return "mapi/car/search"
|
||||||
case .phoneArea:
|
case .phoneArea:
|
||||||
|
|
@ -58,7 +62,7 @@ extension SystemAPI: MultiTargetProtocol {
|
||||||
|
|
||||||
var method: Moya.Method {
|
var method: Moya.Method {
|
||||||
switch self {
|
switch self {
|
||||||
case .userConfig, .wechatService, .phoneArea:
|
case .userConfig, .wechatService, .phoneArea, .sosList:
|
||||||
return .get
|
return .get
|
||||||
default:
|
default:
|
||||||
return .post
|
return .post
|
||||||
|
|
@ -82,6 +86,9 @@ extension SystemAPI: MultiTargetProtocol {
|
||||||
var params = Parameters()
|
var params = Parameters()
|
||||||
params["enable"] = enable
|
params["enable"] = enable
|
||||||
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||||
|
|
||||||
|
case .sosList:
|
||||||
|
return .requestParameters(parameters: parameters, encoding: URLEncoding())
|
||||||
|
|
||||||
case let .search(op_type, number):
|
case let .search(op_type, number):
|
||||||
var params = Parameters()
|
var params = Parameters()
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,12 @@ enum UserAPI {
|
||||||
/// 关系列表
|
/// 关系列表
|
||||||
case relations
|
case relations
|
||||||
|
|
||||||
|
/// 消息列表
|
||||||
|
case mailboxList(page: Int, size: Int)
|
||||||
|
|
||||||
|
/// 消息详情、清空消息
|
||||||
|
case mailboxOperate(opType: String, id: Int?)
|
||||||
|
|
||||||
/// 意见反馈
|
/// 意见反馈
|
||||||
case feedback(type: String, content: String, contact: String, images: [String])
|
case feedback(type: String, content: String, contact: String, images: [String])
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +215,10 @@ extension UserAPI: MultiTargetProtocol {
|
||||||
return "mapi/user/signin/setemail"
|
return "mapi/user/signin/setemail"
|
||||||
case .relations:
|
case .relations:
|
||||||
return "mapi/user/relations"
|
return "mapi/user/relations"
|
||||||
|
case .mailboxList:
|
||||||
|
return "mapi/user/mailbox/list"
|
||||||
|
case .mailboxOperate:
|
||||||
|
return "mapi/user/mailbox/operate"
|
||||||
case .feedback:
|
case .feedback:
|
||||||
return "api/user/feedback"
|
return "api/user/feedback"
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +315,7 @@ extension UserAPI: MultiTargetProtocol {
|
||||||
return .requestParameters(
|
return .requestParameters(
|
||||||
parameters: [
|
parameters: [
|
||||||
"os": os,
|
"os": os,
|
||||||
"token": tokens
|
"token": tokens.count > 0 ? tokens : ["cn.zuom8.jisuloca"]
|
||||||
],
|
],
|
||||||
encoding: JSONEncoding()
|
encoding: JSONEncoding()
|
||||||
)
|
)
|
||||||
|
|
@ -455,6 +465,19 @@ extension UserAPI: MultiTargetProtocol {
|
||||||
case .relations:
|
case .relations:
|
||||||
return .requestPlain
|
return .requestPlain
|
||||||
|
|
||||||
|
case let .mailboxList(page, size):
|
||||||
|
return .requestParameters(
|
||||||
|
parameters: ["page": page, "size": size],
|
||||||
|
encoding: JSONEncoding()
|
||||||
|
)
|
||||||
|
|
||||||
|
case let .mailboxOperate(opType, id):
|
||||||
|
var params: Parameters = ["op_type": opType]
|
||||||
|
if let id {
|
||||||
|
params["id"] = id
|
||||||
|
}
|
||||||
|
return .requestParameters(parameters: params, encoding: JSONEncoding())
|
||||||
|
|
||||||
case let .feedback(type, content, contact, images):
|
case let .feedback(type, content, contact, images):
|
||||||
let params: Parameters = [
|
let params: Parameters = [
|
||||||
"type": type,
|
"type": type,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "detail_bird@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "detail_bird@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_bird.imageset/detail_bird@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_bird.imageset/detail_bird@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "detail_card@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "detail_card@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_card.imageset/detail_card@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_card.imageset/detail_card@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "detail_close@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "detail_close@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_close.imageset/detail_close@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 530 B |
BIN
QuickLocation/Assets.xcassets/Mailbox/detail_close.imageset/detail_close@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 915 B |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "interaction@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "interaction@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mailbox/interaction.imageset/interaction@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
QuickLocation/Assets.xcassets/Mailbox/interaction.imageset/interaction@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "notice@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "notice@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "system@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "system@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{ "idiom" : "universal", "scale" : "1x" },
|
||||||
|
{ "filename" : "system_message@2x.png", "idiom" : "universal", "scale" : "2x" },
|
||||||
|
{ "filename" : "system_message@3x.png", "idiom" : "universal", "scale" : "3x" }
|
||||||
|
],
|
||||||
|
"info" : { "author" : "xcode", "version" : 1 }
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/Mailbox/system_message.imageset/system_message@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
QuickLocation/Assets.xcassets/Mailbox/system_message.imageset/system_message@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
||||||
22
QuickLocation/Assets.xcassets/PrivacySettings/group_lock.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "group_lock@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "group_lock@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivacySettings/group_lock.imageset/group_lock@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
QuickLocation/Assets.xcassets/PrivacySettings/group_lock.imageset/group_lock@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
22
QuickLocation/Assets.xcassets/PrivacySettings/phone_report.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "phone_report@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "phone_report@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivacySettings/phone_report.imageset/phone_report@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
QuickLocation/Assets.xcassets/PrivacySettings/phone_report.imageset/phone_report@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
22
QuickLocation/Assets.xcassets/PrivacySettings/pigeon_message.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "pigeon_message@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "pigeon_message@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivacySettings/pigeon_message.imageset/pigeon_message@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
QuickLocation/Assets.xcassets/PrivacySettings/pigeon_message.imageset/pigeon_message@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
22
QuickLocation/Assets.xcassets/PrivacySettings/privacy_entry.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "privacy_entry@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "privacy_entry@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
QuickLocation/Assets.xcassets/PrivacySettings/privacy_entry.imageset/privacy_entry@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
QuickLocation/Assets.xcassets/PrivacySettings/privacy_entry.imageset/privacy_entry@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -51,6 +51,8 @@ extension DefaultsKeys {
|
||||||
var appRestrictPendingDeleteUserIds: DefaultsKey<[String]> { .init("appRestrictPendingDeleteUserIds", defaultValue: []) }
|
var appRestrictPendingDeleteUserIds: DefaultsKey<[String]> { .init("appRestrictPendingDeleteUserIds", defaultValue: []) }
|
||||||
/// 当前本地配对数据所属账号
|
/// 当前本地配对数据所属账号
|
||||||
var appRestrictPairingOwnerUserId: DefaultsKey<String> { .init("appRestrictPairingOwnerUserId", defaultValue: "") }
|
var appRestrictPairingOwnerUserId: DefaultsKey<String> { .init("appRestrictPairingOwnerUserId", defaultValue: "") }
|
||||||
|
/// 飞鸽发送成功时间戳(秒),key = userId
|
||||||
|
var pigeonSendAtByUser: DefaultsKey<[String: [Double]]> { .init("pigeonSendAtByUser", defaultValue: [:]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 通知常量
|
/// 通知常量
|
||||||
|
|
@ -77,6 +79,8 @@ extension Notification.Name {
|
||||||
static let appRestrictPairingDataDidChange = Notification.Name("appRestrictPairingDataDidChange")
|
static let appRestrictPairingDataDidChange = Notification.Name("appRestrictPairingDataDidChange")
|
||||||
/// MQTT 应用锁定状态变化
|
/// MQTT 应用锁定状态变化
|
||||||
static let lockDistractAppsDidChange = Notification.Name("lockDistractAppsDidChange")
|
static let lockDistractAppsDidChange = Notification.Name("lockDistractAppsDidChange")
|
||||||
|
/// 本机手机状态(电量/网络/亮度/音量等)变化
|
||||||
|
static let phoneStatusDidChange = Notification.Name("phoneStatusDidChange")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GroupDataChangeUserInfoKey {
|
enum GroupDataChangeUserInfoKey {
|
||||||
|
|
@ -91,6 +95,10 @@ enum GroupDataChangeSource {
|
||||||
static let mqtt = "mqtt"
|
static let mqtt = "mqtt"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum LockDistractAppsChangeUserInfoKey {
|
||||||
|
static let groupKey = "group_key"
|
||||||
|
static let userId = "user_id"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 普通常量
|
// MARK: - 普通常量
|
||||||
/// KeyWindow
|
/// KeyWindow
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// PrivilegeDeniedOverlay.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class PrivilegeDeniedOverlay: UIView {
|
||||||
|
|
||||||
|
var onAction: (() -> Void)?
|
||||||
|
|
||||||
|
private let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
|
||||||
|
private let dimView = UIView()
|
||||||
|
private let titleLab = UILabel()
|
||||||
|
private let messageLab = UILabel()
|
||||||
|
private let actionBtn = UIButton(type: .custom)
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
isHidden = true
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
blurView.isUserInteractionEnabled = false
|
||||||
|
addSubview(blurView)
|
||||||
|
blurView.layoutChain.edges()
|
||||||
|
|
||||||
|
dimView.backgroundColor = UIColor.white.withAlphaComponent(0.45)
|
||||||
|
dimView.isUserInteractionEnabled = false
|
||||||
|
addSubview(dimView)
|
||||||
|
dimView.layoutChain.edges()
|
||||||
|
|
||||||
|
titleLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
|
titleLab.textColor = UIColor(hexStr: "#293445")
|
||||||
|
titleLab.textAlignment = .center
|
||||||
|
addSubview(titleLab)
|
||||||
|
|
||||||
|
messageLab.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
|
messageLab.textColor = UIColor(hexStr: "#293445")
|
||||||
|
messageLab.textAlignment = .center
|
||||||
|
addSubview(messageLab)
|
||||||
|
|
||||||
|
actionBtn.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
|
||||||
|
actionBtn.setTitleColor(.white, for: .normal)
|
||||||
|
actionBtn.backgroundColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
actionBtn.cornerRadius = 10
|
||||||
|
actionBtn.addTarget(self, action: #selector(tapAction), for: .touchUpInside)
|
||||||
|
addSubview(actionBtn)
|
||||||
|
|
||||||
|
titleLab.layoutChain.centerX()
|
||||||
|
titleLab.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -28).isActive = true
|
||||||
|
messageLab.layoutChain
|
||||||
|
.topToBottomOfView(titleLab, offset: 6)
|
||||||
|
.centerX()
|
||||||
|
actionBtn.layoutChain
|
||||||
|
.topToBottomOfView(messageLab, offset: 16)
|
||||||
|
.centerX()
|
||||||
|
.width(160)
|
||||||
|
.height(44)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(title: String, message: String, actionTitle: String) {
|
||||||
|
titleLab.text = title
|
||||||
|
messageLab.text = message
|
||||||
|
actionBtn.setTitle(actionTitle, for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
isHidden = !visible
|
||||||
|
isUserInteractionEnabled = visible
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func tapAction() {
|
||||||
|
onAction?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,13 @@
|
||||||
<key>NSIncludesSubdomains</key>
|
<key>NSIncludesSubdomains</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>cdn.u8t.cn</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSIncludesSubdomains</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UIAppFonts</key>
|
<key>UIAppFonts</key>
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,8 @@ struct SystemConfigModel: Mappable {
|
||||||
var timeConfig: PopupWindowTimeModel?
|
var timeConfig: PopupWindowTimeModel?
|
||||||
/// 微信分享配置
|
/// 微信分享配置
|
||||||
var shareConfig: WxShareConfigModel?
|
var shareConfig: WxShareConfigModel?
|
||||||
|
/// 飞鸽传书发送次数
|
||||||
|
var pigeonConfig: PigeonTimeSettingModel?
|
||||||
/// 版本更新
|
/// 版本更新
|
||||||
var versionUpgradeModel: VersionUpgradeModel?
|
var versionUpgradeModel: VersionUpgradeModel?
|
||||||
|
|
||||||
|
|
@ -107,6 +109,7 @@ struct SystemConfigModel: Mappable {
|
||||||
groupBannerList <- map["client.team.ad", nested: false]
|
groupBannerList <- map["client.team.ad", nested: false]
|
||||||
popupConfig <- map["client.popup.display", nested: false]
|
popupConfig <- map["client.popup.display", nested: false]
|
||||||
timeConfig <- map["client.time.setting", nested: false]
|
timeConfig <- map["client.time.setting", nested: false]
|
||||||
|
pigeonConfig <- map["client.time.setting.pigeon", nested: false]
|
||||||
shareConfig <- map["client.weixin.share", nested: false]
|
shareConfig <- map["client.weixin.share", nested: false]
|
||||||
guideDisplay <- map["client.guide_display", nested: false]
|
guideDisplay <- map["client.guide_display", nested: false]
|
||||||
globalDisplay <- map["client.global_display", nested: false]
|
globalDisplay <- map["client.global_display", nested: false]
|
||||||
|
|
@ -161,6 +164,22 @@ struct PopupWindowTimeModel: Mappable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PigeonTimeSettingModel: Mappable {
|
||||||
|
/// 时间窗口内最多发送次数
|
||||||
|
var nums: Int = 0
|
||||||
|
/// 窗口长度(秒)
|
||||||
|
var times: Int = 0
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
nums <- (map["nums"], kStrTransformInt)
|
||||||
|
times <- (map["times"], kStrTransformInt)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEnabled: Bool { nums > 0 && times > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
struct WxShareConfigModel: Mappable {
|
struct WxShareConfigModel: Mappable {
|
||||||
/// 链接
|
/// 链接
|
||||||
var link: String = ""
|
var link: String = ""
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ enum Route: String {
|
||||||
case emergencyContactAdd = "emergencyContactAdd"
|
case emergencyContactAdd = "emergencyContactAdd"
|
||||||
/// 隐私与协议
|
/// 隐私与协议
|
||||||
case privacyPolicy = "privacyPolicy"
|
case privacyPolicy = "privacyPolicy"
|
||||||
|
/// 隐私设置
|
||||||
|
case privacySettings = "privacySettings"
|
||||||
/// 权限检测
|
/// 权限检测
|
||||||
case checkPermission = "checkPermission"
|
case checkPermission = "checkPermission"
|
||||||
/// 创建行程
|
/// 创建行程
|
||||||
|
|
@ -101,6 +103,10 @@ enum Route: String {
|
||||||
case signIn = "signIn"
|
case signIn = "signIn"
|
||||||
/// SOS
|
/// SOS
|
||||||
case sos = "sos"
|
case sos = "sos"
|
||||||
|
/// 求助列表
|
||||||
|
case sosList = "sosList"
|
||||||
|
/// 圈子消息
|
||||||
|
case mailbox = "mailbox"
|
||||||
/// 充值优惠弹窗
|
/// 充值优惠弹窗
|
||||||
case promotionalActivities = "promotionalActivities"
|
case promotionalActivities = "promotionalActivities"
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +308,12 @@ extension AppRouter: AppRouterProtocol {
|
||||||
AppRouter.register(Route.privacyPolicy) { url, parameters in
|
AppRouter.register(Route.privacyPolicy) { url, parameters in
|
||||||
PrivacyPolicyVC()
|
PrivacyPolicyVC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppRouter.register(Route.privacySettings) { _, _ in
|
||||||
|
let vc = PrivacySettingsViewController()
|
||||||
|
vc.isNeedLogin = true
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 权限检测
|
// MARK: - 权限检测
|
||||||
AppRouter.register(Route.checkPermission) { url, parameters in
|
AppRouter.register(Route.checkPermission) { url, parameters in
|
||||||
|
|
@ -451,6 +463,18 @@ extension AppRouter: AppRouterProtocol {
|
||||||
vc.isNeedLogin = true
|
vc.isNeedLogin = true
|
||||||
return vc
|
return vc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppRouter.register(Route.sosList) { _, _ in
|
||||||
|
let vc = SOSListViewController()
|
||||||
|
vc.isNeedLogin = true
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
|
AppRouter.register(Route.mailbox) { _, _ in
|
||||||
|
let vc = MailboxViewController()
|
||||||
|
vc.isNeedLogin = true
|
||||||
|
return vc
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 充值优惠弹窗
|
// MARK: - 充值优惠弹窗
|
||||||
AppRouter.register(Route.promotionalActivities) { url, parameters in
|
AppRouter.register(Route.promotionalActivities) { url, parameters in
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,35 @@ final class AppRestrictManager: NSObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 登录/恢复会话后立刻检查屏幕使用时间权限;已关闭则删除服务端配对。
|
||||||
|
func checkScreenTimePermissionAfterLogin() {
|
||||||
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.checkScreenTimePermissionAfterLogin()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentUserId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !currentUserId.isEmpty else { return }
|
||||||
|
|
||||||
|
if center.authorizationStatus == .approved {
|
||||||
|
var approvedUserIds = Set(Defaults[\.appRestrictApprovedUserIds])
|
||||||
|
approvedUserIds.insert(currentUserId)
|
||||||
|
Defaults[\.appRestrictApprovedUserIds] = Array(approvedUserIds)
|
||||||
|
if AppRestrictSharedStore.hasPairingData,
|
||||||
|
Defaults[\.appRestrictPairingOwnerUserId].isEmpty {
|
||||||
|
Defaults[\.appRestrictPairingOwnerUserId] = currentUserId
|
||||||
|
}
|
||||||
|
retryPendingDeleteIfNeeded(for: currentUserId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addPendingDeleteUserId(currentUserId)
|
||||||
|
clearLocalPairingData()
|
||||||
|
retryPendingDeleteIfNeeded(for: currentUserId)
|
||||||
|
}
|
||||||
|
|
||||||
func mergeSelection(_ incoming: FamilyActivitySelection) {
|
func mergeSelection(_ incoming: FamilyActivitySelection) {
|
||||||
var current = selection
|
var current = selection
|
||||||
current.applicationTokens.formUnion(incoming.applicationTokens)
|
current.applicationTokens.formUnion(incoming.applicationTokens)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,6 @@ import CocoaMQTT
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Network
|
import Network
|
||||||
import CoreTelephony
|
|
||||||
import AVFoundation
|
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
|
||||||
// MARK: - MQTT 模型
|
// MARK: - MQTT 模型
|
||||||
|
|
@ -33,6 +31,7 @@ enum MqttType: String, Codable {
|
||||||
case phoneUsage = "phoneUsage" // 当前 App 使用次数上报
|
case phoneUsage = "phoneUsage" // 当前 App 使用次数上报
|
||||||
case lockApp = "lockApp" // 远程锁 App
|
case lockApp = "lockApp" // 远程锁 App
|
||||||
case appUnlock = "appUnlock" // 远程解锁 App
|
case appUnlock = "appUnlock" // 远程解锁 App
|
||||||
|
case lockAppsChanged = "lockAppsChanged" // App 锁状态已同步
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 单点位置
|
/// 单点位置
|
||||||
|
|
@ -91,6 +90,22 @@ struct MqttLockAppBody: Decodable {
|
||||||
let token: MqttFlexibleStringArray?
|
let token: MqttFlexibleStringArray?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct MqttLockAppsChangedData: Codable, Equatable {
|
||||||
|
let userId: String
|
||||||
|
let groupKey: String
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case userId = "user_id"
|
||||||
|
case groupKey = "group_key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MqttLockAppsChangedPayload: Codable {
|
||||||
|
let type: String
|
||||||
|
let data: MqttLockAppsChangedData
|
||||||
|
let extra: String
|
||||||
|
}
|
||||||
|
|
||||||
enum MqttFlexibleStringArray: Decodable {
|
enum MqttFlexibleStringArray: Decodable {
|
||||||
case one(String)
|
case one(String)
|
||||||
case many([String])
|
case many([String])
|
||||||
|
|
@ -363,7 +378,6 @@ final class AppUnlockCoordinator {
|
||||||
performOnMain { [weak self] in
|
performOnMain { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
guard let body = message.data?.app_unlock else { return }
|
guard let body = message.data?.app_unlock else { return }
|
||||||
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
|
|
||||||
|
|
||||||
let currentUserId = AppContextManager.shared.userId.trimmed
|
let currentUserId = AppContextManager.shared.userId.trimmed
|
||||||
let topicUserId = topic.replacingOccurrences(of: "smartdrive/", with: "").trimmed
|
let topicUserId = topic.replacingOccurrences(of: "smartdrive/", with: "").trimmed
|
||||||
|
|
@ -518,10 +532,13 @@ final class AppUnlockCoordinator {
|
||||||
print("[AppUnlock] unlock confirmation failed: \(response.message ?? "unknown error")")
|
print("[AppUnlock] unlock confirmation failed: \(response.message ?? "unknown error")")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
MQTTService.shared.reportLockAppsChanged(
|
||||||
|
userId: confirmation.userId,
|
||||||
|
groupKey: confirmation.groupKey
|
||||||
|
)
|
||||||
self.pendingConfirmations.removeAll { $0.identifier == confirmation.identifier }
|
self.pendingConfirmations.removeAll { $0.identifier == confirmation.identifier }
|
||||||
self.savePendingConfirmations()
|
self.savePendingConfirmations()
|
||||||
self.retryPendingConfirmations()
|
self.retryPendingConfirmations()
|
||||||
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
|
|
||||||
}, onError: { [weak self] error in
|
}, onError: { [weak self] error in
|
||||||
guard let self,
|
guard let self,
|
||||||
self.inFlightConfirmation?.identifier == confirmation.identifier else { return }
|
self.inFlightConfirmation?.identifier == confirmation.identifier else { return }
|
||||||
|
|
@ -699,6 +716,8 @@ final class MQTTService: NSObject {
|
||||||
|
|
||||||
/// 按 topic 订阅的回调(优先匹配)
|
/// 按 topic 订阅的回调(优先匹配)
|
||||||
private var topicCallbacks: [String: (CocoaMQTT5Message) -> Void] = [:]
|
private var topicCallbacks: [String: (CocoaMQTT5Message) -> Void] = [:]
|
||||||
|
private var lockStateSubscriptionsByOwner: [UUID: Set<String>] = [:]
|
||||||
|
private var pendingLockAppsChangedEvents: [MqttLockAppsChangedData] = []
|
||||||
|
|
||||||
// MARK: - 配置
|
// MARK: - 配置
|
||||||
private var host: String { "emqx.batiao8.com" }
|
private var host: String { "emqx.batiao8.com" }
|
||||||
|
|
@ -709,6 +728,55 @@ final class MQTTService: NSObject {
|
||||||
private var password = "Batiao12B"
|
private var password = "Batiao12B"
|
||||||
private var topic = "smartdrive/"
|
private var topic = "smartdrive/"
|
||||||
|
|
||||||
|
private var lockStateSubscriptionTopics: Set<String> {
|
||||||
|
lockStateSubscriptionsByOwner.values.reduce(into: Set<String>()) { result, topics in
|
||||||
|
result.formUnion(topics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postLockDistractAppsChangeIfNeeded(topic: String, payload: String?) {
|
||||||
|
guard let payload,
|
||||||
|
let data = payload.data(using: .utf8),
|
||||||
|
let message = try? JSONDecoder().decode(MqttIncomingMessage.self, from: data),
|
||||||
|
let type = message.type else { return }
|
||||||
|
|
||||||
|
let topicUserId = topic.replacingOccurrences(of: self.topic, with: "").trimmed
|
||||||
|
let userId: String
|
||||||
|
let groupKey: String
|
||||||
|
switch type {
|
||||||
|
case MqttType.lockApp.rawValue:
|
||||||
|
userId = message.data?.lock_app?.user_id?.trimmed ?? topicUserId
|
||||||
|
groupKey = message.data?.lock_app?.group_key?.trimmed ?? ""
|
||||||
|
case MqttType.appUnlock.rawValue:
|
||||||
|
userId = message.data?.app_unlock?.user_id?.trimmed ?? topicUserId
|
||||||
|
groupKey = message.data?.app_unlock?.group_key?.trimmed ?? ""
|
||||||
|
case MqttType.lockAppsChanged.rawValue:
|
||||||
|
userId = message.data?.user_id?.trimmed ?? topicUserId
|
||||||
|
groupKey = message.data?.group_key?.trimmed ?? ""
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
postLockDistractAppsChange(userId: userId, groupKey: groupKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postLockDistractAppsChange(userId: String, groupKey: String) {
|
||||||
|
let post = {
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .lockDistractAppsDidChange,
|
||||||
|
object: nil,
|
||||||
|
userInfo: [
|
||||||
|
LockDistractAppsChangeUserInfoKey.userId: userId,
|
||||||
|
LockDistractAppsChangeUserInfoKey.groupKey: groupKey
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if Thread.isMainThread {
|
||||||
|
post()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async(execute: post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func postGroupDataChangeIfNeeded(payload: String?) {
|
private func postGroupDataChangeIfNeeded(payload: String?) {
|
||||||
guard let payload,
|
guard let payload,
|
||||||
let data = payload.data(using: .utf8),
|
let data = payload.data(using: .utf8),
|
||||||
|
|
@ -744,7 +812,6 @@ final class MQTTService: NSObject {
|
||||||
// MARK: - 手机信息上报
|
// MARK: - 手机信息上报
|
||||||
private let phoneReportInterval: TimeInterval = 30 * 60
|
private let phoneReportInterval: TimeInterval = 30 * 60
|
||||||
private let phoneReportRetryInterval: TimeInterval = 60
|
private let phoneReportRetryInterval: TimeInterval = 60
|
||||||
private let phoneNetworkMonitor = PhoneNetworkStatusMonitor()
|
|
||||||
private var phoneReportGroupKey = ""
|
private var phoneReportGroupKey = ""
|
||||||
private var phoneReportLocation: CLLocation?
|
private var phoneReportLocation: CLLocation?
|
||||||
private var phoneReportTimer: Timer?
|
private var phoneReportTimer: Timer?
|
||||||
|
|
@ -757,7 +824,7 @@ final class MQTTService: NSObject {
|
||||||
override private init() {
|
override private init() {
|
||||||
super.init()
|
super.init()
|
||||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
phoneNetworkMonitor.start()
|
PhoneStatusCollector.shared.start()
|
||||||
NotificationCenter.default.addObserver(
|
NotificationCenter.default.addObserver(
|
||||||
self,
|
self,
|
||||||
selector: #selector(applicationDidBecomeActive),
|
selector: #selector(applicationDidBecomeActive),
|
||||||
|
|
@ -842,6 +909,34 @@ final class MQTTService: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLockStateSubscriptions(owner: UUID, userIds: [String]) {
|
||||||
|
performOnMain { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let previousTopics = self.lockStateSubscriptionTopics
|
||||||
|
let nextOwnerTopics = Set(
|
||||||
|
userIds
|
||||||
|
.map(\.trimmed)
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.map { "\(self.topic)\($0)" }
|
||||||
|
)
|
||||||
|
if nextOwnerTopics.isEmpty {
|
||||||
|
self.lockStateSubscriptionsByOwner.removeValue(forKey: owner)
|
||||||
|
} else {
|
||||||
|
self.lockStateSubscriptionsByOwner[owner] = nextOwnerTopics
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextTopics = self.lockStateSubscriptionTopics
|
||||||
|
for subscriptionTopic in nextTopics.subtracting(previousTopics) {
|
||||||
|
self.subscribe(topic: subscriptionTopic)
|
||||||
|
}
|
||||||
|
for subscriptionTopic in previousTopics.subtracting(nextTopics)
|
||||||
|
where self.topicCallbacks[subscriptionTopic] == nil
|
||||||
|
&& !self.isRequiredBaseSubscription(subscriptionTopic) {
|
||||||
|
self.mqtt?.unsubscribe(subscriptionTopic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 取消订阅
|
// MARK: - 取消订阅
|
||||||
func unsubscribe(topic: String) {
|
func unsubscribe(topic: String) {
|
||||||
mqtt?.unsubscribe(topic)
|
mqtt?.unsubscribe(topic)
|
||||||
|
|
@ -858,10 +953,56 @@ final class MQTTService: NSObject {
|
||||||
/// 取消订阅上一批成员
|
/// 取消订阅上一批成员
|
||||||
func unsubscribeGroupMembers(_ memberIds: [String]) {
|
func unsubscribeGroupMembers(_ memberIds: [String]) {
|
||||||
for id in memberIds {
|
for id in memberIds {
|
||||||
mqtt?.unsubscribe("\(topic)\(id)")
|
let subscriptionTopic = "\(topic)\(id)"
|
||||||
|
topicCallbacks.removeValue(forKey: subscriptionTopic)
|
||||||
|
if !lockStateSubscriptionTopics.contains(subscriptionTopic),
|
||||||
|
!isRequiredBaseSubscription(subscriptionTopic) {
|
||||||
|
mqtt?.unsubscribe(subscriptionTopic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func reportLockAppsChanged(userId: String, groupKey: String) {
|
||||||
|
performOnMain { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
let event = MqttLockAppsChangedData(
|
||||||
|
userId: userId.trimmed,
|
||||||
|
groupKey: groupKey.trimmed
|
||||||
|
)
|
||||||
|
guard !event.userId.isEmpty else { return }
|
||||||
|
self.postLockDistractAppsChange(userId: event.userId, groupKey: event.groupKey)
|
||||||
|
if !self.pendingLockAppsChangedEvents.contains(event) {
|
||||||
|
self.pendingLockAppsChangedEvents.append(event)
|
||||||
|
}
|
||||||
|
self.flushPendingLockAppsChangedEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func flushPendingLockAppsChangedEvents() {
|
||||||
|
guard isConnected, !pendingLockAppsChangedEvents.isEmpty else { return }
|
||||||
|
var unsentEvents: [MqttLockAppsChangedData] = []
|
||||||
|
for event in pendingLockAppsChangedEvents {
|
||||||
|
let payload = MqttLockAppsChangedPayload(
|
||||||
|
type: MqttType.lockAppsChanged.rawValue,
|
||||||
|
data: event,
|
||||||
|
extra: ""
|
||||||
|
)
|
||||||
|
guard let data = try? JSONEncoder().encode(payload),
|
||||||
|
let message = String(data: data, encoding: .utf8),
|
||||||
|
publish(topic: "\(topic)\(event.userId)", message: message) >= 0 else {
|
||||||
|
unsentEvents.append(event)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingLockAppsChangedEvents = unsentEvents
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isRequiredBaseSubscription(_ subscriptionTopic: String) -> Bool {
|
||||||
|
if subscriptionTopic == topic { return true }
|
||||||
|
let currentUserId = AppContextManager.shared.userId.trimmed
|
||||||
|
return !currentUserId.isEmpty && subscriptionTopic == "\(topic)\(currentUserId)"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 发布消息
|
// MARK: - 发布消息
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func publish(topic: String, message: String, qos: CocoaMQTTQoS = .qos1) -> Int {
|
func publish(topic: String, message: String, qos: CocoaMQTTQoS = .qos1) -> Int {
|
||||||
|
|
@ -1078,16 +1219,16 @@ final class MQTTService: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func collectPhoneInfo(weather: String) -> MqttPhoneInfo {
|
private func collectPhoneInfo(weather: String) -> MqttPhoneInfo {
|
||||||
let storage = Self.storageInfo()
|
let snap = PhoneStatusCollector.shared.snapshot()
|
||||||
return MqttPhoneInfo(
|
return MqttPhoneInfo(
|
||||||
brand: "iphone",
|
brand: "iphone",
|
||||||
model: UIDevice.modelName,
|
model: snap.model,
|
||||||
memory_total: storage?.total ?? "未知",
|
memory_total: snap.memoryTotal ?? "未知",
|
||||||
memory_used: storage?.used ?? "未知",
|
memory_used: snap.memoryUsed ?? "未知",
|
||||||
battery: UIDevice.batteryPercent.map { "\($0)%" } ?? "未知",
|
battery: snap.batteryText,
|
||||||
network: phoneNetworkMonitor.statusText,
|
network: snap.network,
|
||||||
brightness: "\(Int((UIScreen.main.brightness * 100).rounded()))%",
|
brightness: snap.brightnessText,
|
||||||
volume: "\(Int((AVAudioSession.sharedInstance().outputVolume * 100).rounded()))%",
|
volume: snap.volumeText,
|
||||||
weather: weather,
|
weather: weather,
|
||||||
use_time: "\(max(0, UnlockCountManager.shared.todayScreenTimeSeconds))",
|
use_time: "\(max(0, UnlockCountManager.shared.todayScreenTimeSeconds))",
|
||||||
unlock_count: "\(UnlockCountManager.shared.todayCount)"
|
unlock_count: "\(UnlockCountManager.shared.todayCount)"
|
||||||
|
|
@ -1131,25 +1272,6 @@ final class MQTTService: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func storageInfo() -> (used: String, total: String)? {
|
|
||||||
let url = URL(fileURLWithPath: NSHomeDirectory())
|
|
||||||
let keys: Set<URLResourceKey> = [
|
|
||||||
.volumeTotalCapacityKey,
|
|
||||||
.volumeAvailableCapacityForImportantUsageKey
|
|
||||||
]
|
|
||||||
guard let values = try? url.resourceValues(forKeys: keys),
|
|
||||||
let total = values.volumeTotalCapacity,
|
|
||||||
let available = values.volumeAvailableCapacityForImportantUsage,
|
|
||||||
total > 0 else { return nil }
|
|
||||||
|
|
||||||
let totalBytes = Int64(total)
|
|
||||||
let usedBytes = max(0, totalBytes - available)
|
|
||||||
return (
|
|
||||||
used: String(format: "%.1fGB", Double(usedBytes) / 1_000_000_000),
|
|
||||||
total: String(format: "%.0fGB", Double(totalBytes) / 1_000_000_000)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func currentPhoneUsageSnapshot() -> MqttPhoneUsageSnapshot {
|
private static func currentPhoneUsageSnapshot() -> MqttPhoneUsageSnapshot {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.calendar = Calendar.current
|
formatter.calendar = Calendar.current
|
||||||
|
|
@ -1182,7 +1304,11 @@ extension MQTTService: CocoaMQTT5Delegate {
|
||||||
if !userId.isEmpty {
|
if !userId.isEmpty {
|
||||||
subscribe(topic: "\(topic)\(userId)")
|
subscribe(topic: "\(topic)\(userId)")
|
||||||
}
|
}
|
||||||
|
for subscriptionTopic in Set(topicCallbacks.keys).union(lockStateSubscriptionTopics) {
|
||||||
|
subscribe(topic: subscriptionTopic)
|
||||||
|
}
|
||||||
onConnected?()
|
onConnected?()
|
||||||
|
flushPendingLockAppsChangedEvents()
|
||||||
reportPhoneUsageIfNeeded()
|
reportPhoneUsageIfNeeded()
|
||||||
reportPhoneIfNeeded()
|
reportPhoneIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
@ -1199,6 +1325,7 @@ extension MQTTService: CocoaMQTT5Delegate {
|
||||||
|
|
||||||
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) {
|
func mqtt5(_ mqtt5: CocoaMQTT5, didReceiveMessage message: CocoaMQTT5Message, id: UInt16, publishData: MqttDecodePublish?) {
|
||||||
postGroupDataChangeIfNeeded(payload: message.string)
|
postGroupDataChangeIfNeeded(payload: message.string)
|
||||||
|
postLockDistractAppsChangeIfNeeded(topic: message.topic, payload: message.string)
|
||||||
if AppUnlockCoordinator.shared.handleIncoming(topic: message.topic, payload: message.string) {
|
if AppUnlockCoordinator.shared.handleIncoming(topic: message.topic, payload: message.string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -1238,63 +1365,4 @@ extension MQTTService: CocoaMQTT5Delegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class PhoneNetworkStatusMonitor {
|
|
||||||
|
|
||||||
private let monitor = NWPathMonitor()
|
|
||||||
private let queue = DispatchQueue(label: "com.quicklocation.mqtt.network")
|
|
||||||
private let lock = NSLock()
|
|
||||||
private let telephonyInfo = CTTelephonyNetworkInfo()
|
|
||||||
private var path: NWPath?
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
monitor.pathUpdateHandler = { [weak self] path in
|
|
||||||
self?.lock.lock()
|
|
||||||
self?.path = path
|
|
||||||
self?.lock.unlock()
|
|
||||||
}
|
|
||||||
monitor.start(queue: queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusText: String {
|
|
||||||
lock.lock()
|
|
||||||
let currentPath = path
|
|
||||||
lock.unlock()
|
|
||||||
|
|
||||||
guard let currentPath, currentPath.status == .satisfied else {
|
|
||||||
return "无网络"
|
|
||||||
}
|
|
||||||
if currentPath.usesInterfaceType(.wifi) {
|
|
||||||
return "Wi-Fi"
|
|
||||||
}
|
|
||||||
guard currentPath.usesInterfaceType(.cellular) else {
|
|
||||||
return "无网络"
|
|
||||||
}
|
|
||||||
return cellularGeneration
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cellularGeneration: String {
|
|
||||||
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
|
|
||||||
.map { Array($0.values) } ?? []
|
|
||||||
guard let technology = technologies.first else { return "蜂窝" }
|
|
||||||
switch technology {
|
|
||||||
case CTRadioAccessTechnologyNR, CTRadioAccessTechnologyNRNSA:
|
|
||||||
return "5G"
|
|
||||||
case CTRadioAccessTechnologyLTE:
|
|
||||||
return "4G"
|
|
||||||
case CTRadioAccessTechnologyWCDMA,
|
|
||||||
CTRadioAccessTechnologyHSDPA,
|
|
||||||
CTRadioAccessTechnologyHSUPA,
|
|
||||||
CTRadioAccessTechnologyCDMAEVDORev0,
|
|
||||||
CTRadioAccessTechnologyCDMAEVDORevA,
|
|
||||||
CTRadioAccessTechnologyCDMAEVDORevB,
|
|
||||||
CTRadioAccessTechnologyeHRPD:
|
|
||||||
return "3G"
|
|
||||||
case CTRadioAccessTechnologyGPRS,
|
|
||||||
CTRadioAccessTechnologyEdge,
|
|
||||||
CTRadioAccessTechnologyCDMA1x:
|
|
||||||
return "2G"
|
|
||||||
default:
|
|
||||||
return "蜂窝"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
//
|
||||||
|
// PhoneStatusCollector.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVFoundation
|
||||||
|
import CoreTelephony
|
||||||
|
import Foundation
|
||||||
|
import Network
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct PhoneStatusSnapshot: Equatable {
|
||||||
|
let model: String
|
||||||
|
let batteryPercent: Int?
|
||||||
|
let batteryText: String
|
||||||
|
let isCharging: Bool
|
||||||
|
let network: String
|
||||||
|
let brightnessPercent: Int
|
||||||
|
let brightnessText: String
|
||||||
|
let volumePercent: Int
|
||||||
|
let volumeText: String
|
||||||
|
let memoryUsed: String?
|
||||||
|
let memoryTotal: String?
|
||||||
|
let storageRatio: CGFloat
|
||||||
|
let storageText: String
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PhoneStatusCollector: NSObject {
|
||||||
|
|
||||||
|
static let shared = PhoneStatusCollector()
|
||||||
|
|
||||||
|
private let networkMonitor = PhoneNetworkStatusMonitor()
|
||||||
|
private var volumeObservation: NSKeyValueObservation?
|
||||||
|
private var pendingNotify: DispatchWorkItem?
|
||||||
|
private var started = false
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
guard !started else { return }
|
||||||
|
started = true
|
||||||
|
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||||
|
networkMonitor.start { [weak self] in
|
||||||
|
self?.emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = NotificationCenter.default
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleChange),
|
||||||
|
name: UIDevice.batteryLevelDidChangeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleChange),
|
||||||
|
name: UIDevice.batteryStateDidChangeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleChange),
|
||||||
|
name: UIScreen.brightnessDidChangeNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
center.addObserver(
|
||||||
|
self,
|
||||||
|
selector: #selector(handleChange),
|
||||||
|
name: UIApplication.didBecomeActiveNotification,
|
||||||
|
object: nil
|
||||||
|
)
|
||||||
|
volumeObservation = AVAudioSession.sharedInstance().observe(\.outputVolume, options: [.new]) { [weak self] _, _ in
|
||||||
|
self?.emitChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshot() -> PhoneStatusSnapshot {
|
||||||
|
start()
|
||||||
|
let batteryPercent = UIDevice.batteryPercent
|
||||||
|
let brightnessPercent = Int((UIScreen.main.brightness * 100).rounded())
|
||||||
|
let volumePercent = Int((AVAudioSession.sharedInstance().outputVolume * 100).rounded())
|
||||||
|
let storage = Self.storageInfo()
|
||||||
|
let used = storage?.used
|
||||||
|
let total = storage?.total
|
||||||
|
let ratio: CGFloat
|
||||||
|
if let usedValue = Self.firstNumber(in: used),
|
||||||
|
let totalValue = Self.firstNumber(in: total),
|
||||||
|
totalValue > 0 {
|
||||||
|
ratio = CGFloat(max(0, min(1, usedValue / totalValue)))
|
||||||
|
} else {
|
||||||
|
ratio = 0
|
||||||
|
}
|
||||||
|
let storageText: String
|
||||||
|
if let used, let total {
|
||||||
|
storageText = "\(used)/\(total)"
|
||||||
|
} else {
|
||||||
|
storageText = "**"
|
||||||
|
}
|
||||||
|
return PhoneStatusSnapshot(
|
||||||
|
model: UIDevice.modelName,
|
||||||
|
batteryPercent: batteryPercent,
|
||||||
|
batteryText: batteryPercent.map { "\($0)%" } ?? "未知",
|
||||||
|
isCharging: UIDevice.isBatteryCharging,
|
||||||
|
network: networkMonitor.statusText,
|
||||||
|
brightnessPercent: brightnessPercent,
|
||||||
|
brightnessText: "\(brightnessPercent)%",
|
||||||
|
volumePercent: volumePercent,
|
||||||
|
volumeText: "\(volumePercent)%",
|
||||||
|
memoryUsed: used,
|
||||||
|
memoryTotal: total,
|
||||||
|
storageRatio: ratio,
|
||||||
|
storageText: storageText
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleChange() {
|
||||||
|
emitChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func emitChange() {
|
||||||
|
let work = {
|
||||||
|
self.pendingNotify?.cancel()
|
||||||
|
let item = DispatchWorkItem {
|
||||||
|
NotificationCenter.default.post(name: .phoneStatusDidChange, object: nil)
|
||||||
|
}
|
||||||
|
self.pendingNotify = item
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: item)
|
||||||
|
}
|
||||||
|
if Thread.isMainThread {
|
||||||
|
work()
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.async(execute: work)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func storageInfo() -> (used: String, total: String)? {
|
||||||
|
let url = URL(fileURLWithPath: NSHomeDirectory())
|
||||||
|
let keys: Set<URLResourceKey> = [
|
||||||
|
.volumeTotalCapacityKey,
|
||||||
|
.volumeAvailableCapacityForImportantUsageKey
|
||||||
|
]
|
||||||
|
guard let values = try? url.resourceValues(forKeys: keys),
|
||||||
|
let total = values.volumeTotalCapacity,
|
||||||
|
let available = values.volumeAvailableCapacityForImportantUsage,
|
||||||
|
total > 0 else { return nil }
|
||||||
|
|
||||||
|
let totalBytes = Int64(total)
|
||||||
|
let usedBytes = max(0, totalBytes - available)
|
||||||
|
return (
|
||||||
|
used: String(format: "%.1fGB", Double(usedBytes) / 1_000_000_000),
|
||||||
|
total: String(format: "%.0fGB", Double(totalBytes) / 1_000_000_000)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func firstNumber(in value: String?) -> Double? {
|
||||||
|
guard let value,
|
||||||
|
let range = value.range(of: #"\d+(?:\.\d+)?"#, options: .regularExpression) else { return nil }
|
||||||
|
return Double(value[range])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PhoneNetworkStatusMonitor {
|
||||||
|
|
||||||
|
private let monitor = NWPathMonitor()
|
||||||
|
private let queue = DispatchQueue(label: "com.quicklocation.phone.network")
|
||||||
|
private let lock = NSLock()
|
||||||
|
private let telephonyInfo = CTTelephonyNetworkInfo()
|
||||||
|
private var path: NWPath?
|
||||||
|
private var onChange: (() -> Void)?
|
||||||
|
|
||||||
|
func start(onChange: @escaping () -> Void) {
|
||||||
|
self.onChange = onChange
|
||||||
|
monitor.pathUpdateHandler = { [weak self] path in
|
||||||
|
self?.lock.lock()
|
||||||
|
self?.path = path
|
||||||
|
self?.lock.unlock()
|
||||||
|
onChange()
|
||||||
|
}
|
||||||
|
monitor.start(queue: queue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusText: String {
|
||||||
|
lock.lock()
|
||||||
|
let currentPath = path
|
||||||
|
lock.unlock()
|
||||||
|
|
||||||
|
guard let currentPath, currentPath.status == .satisfied else {
|
||||||
|
return "无网络"
|
||||||
|
}
|
||||||
|
if currentPath.usesInterfaceType(.wifi) {
|
||||||
|
return "Wi-Fi"
|
||||||
|
}
|
||||||
|
guard currentPath.usesInterfaceType(.cellular) else {
|
||||||
|
return "无网络"
|
||||||
|
}
|
||||||
|
return cellularGeneration
|
||||||
|
}
|
||||||
|
|
||||||
|
private var cellularGeneration: String {
|
||||||
|
let technologies = telephonyInfo.serviceCurrentRadioAccessTechnology
|
||||||
|
.map { Array($0.values) } ?? []
|
||||||
|
guard let technology = technologies.first else { return "蜂窝" }
|
||||||
|
switch technology {
|
||||||
|
case CTRadioAccessTechnologyNR, CTRadioAccessTechnologyNRNSA:
|
||||||
|
return "5G"
|
||||||
|
case CTRadioAccessTechnologyLTE:
|
||||||
|
return "4G"
|
||||||
|
case CTRadioAccessTechnologyWCDMA,
|
||||||
|
CTRadioAccessTechnologyHSDPA,
|
||||||
|
CTRadioAccessTechnologyHSUPA,
|
||||||
|
CTRadioAccessTechnologyCDMAEVDORev0,
|
||||||
|
CTRadioAccessTechnologyCDMAEVDORevA,
|
||||||
|
CTRadioAccessTechnologyCDMAEVDORevB,
|
||||||
|
CTRadioAccessTechnologyeHRPD:
|
||||||
|
return "3G"
|
||||||
|
case CTRadioAccessTechnologyGPRS,
|
||||||
|
CTRadioAccessTechnologyEdge,
|
||||||
|
CTRadioAccessTechnologyCDMA1x:
|
||||||
|
return "2G"
|
||||||
|
default:
|
||||||
|
return "蜂窝"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -86,12 +86,12 @@ 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.21:9243/"
|
return "http://172.16.10.23:9243/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let presetServers: [(title: String, url: String, env: Int)] = [
|
static let presetServers: [(title: String, url: String, env: Int)] = [
|
||||||
("开发", "http://172.16.10.21:9243/", 0),
|
("开发", "http://172.16.10.23:9243/", 0),
|
||||||
("正式", "https://jsapi.zuom8.cn/", 1)
|
("正式", "https://jsapi.zuom8.cn/", 1)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -228,6 +228,8 @@ struct GroupInfoModel: Mappable, Equatable {
|
||||||
var level: String = ""
|
var level: String = ""
|
||||||
/// 邀请码
|
/// 邀请码
|
||||||
var share_code: String = ""
|
var share_code: String = ""
|
||||||
|
/// 当前用户在该圈的隐私权限
|
||||||
|
var privilege = GroupPrivilege()
|
||||||
|
|
||||||
init?(map: Map) {
|
init?(map: Map) {
|
||||||
|
|
||||||
|
|
@ -246,9 +248,57 @@ struct GroupInfoModel: Mappable, Equatable {
|
||||||
description <- map["description"]
|
description <- map["description"]
|
||||||
share_code <- map["share_code"]
|
share_code <- map["share_code"]
|
||||||
people_no <- (map["people_no"], kStrTransformInt)
|
people_no <- (map["people_no"], kStrTransformInt)
|
||||||
|
privilege <- map["privilege"]
|
||||||
|
if map["app_usage_record"].isKeyPresent {
|
||||||
|
privilege.app_usage_record <- (map["app_usage_record"], kFlexibleBool)
|
||||||
|
}
|
||||||
|
if map["phone_report"].isKeyPresent {
|
||||||
|
privilege.phone_report <- (map["phone_report"], kFlexibleBool)
|
||||||
|
}
|
||||||
|
if map["app_lock"].isKeyPresent {
|
||||||
|
privilege.app_lock <- (map["app_lock"], kFlexibleBool)
|
||||||
|
}
|
||||||
|
if map["pigeon_mail"].isKeyPresent {
|
||||||
|
privilege.pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct GroupPrivilege: Mappable, Equatable {
|
||||||
|
var app_usage_record = false
|
||||||
|
var phone_report = false
|
||||||
|
var app_lock = false
|
||||||
|
var pigeon_mail = false
|
||||||
|
|
||||||
|
init() {}
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
app_usage_record <- (map["app_usage_record"], kFlexibleBool)
|
||||||
|
phone_report <- (map["phone_report"], kFlexibleBool)
|
||||||
|
app_lock <- (map["app_lock"], kFlexibleBool)
|
||||||
|
pigeon_mail <- (map["pigeon_mail"], kFlexibleBool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let kFlexibleBool = TransformOf<Bool, Any>(fromJSON: { value in
|
||||||
|
switch value {
|
||||||
|
case let value as Bool:
|
||||||
|
return value
|
||||||
|
case let value as NSNumber:
|
||||||
|
return value.boolValue
|
||||||
|
case let value as Int:
|
||||||
|
return value != 0
|
||||||
|
case let value as String:
|
||||||
|
let text = value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||||
|
return text == "1" || text == "true" || text == "yes"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, toJSON: { value in
|
||||||
|
value
|
||||||
|
})
|
||||||
|
|
||||||
extension UIImageView {
|
extension UIImageView {
|
||||||
@MainActor
|
@MainActor
|
||||||
func setGroupIcon(_ model: GroupInfoModel) {
|
func setGroupIcon(_ model: GroupInfoModel) {
|
||||||
|
|
@ -279,17 +329,58 @@ extension GroupInfoModel: IdentifiableType {
|
||||||
struct CommonGroupModel: Mappable, Equatable {
|
struct CommonGroupModel: Mappable, Equatable {
|
||||||
var group_key: String = ""
|
var group_key: String = ""
|
||||||
var group_name: String = ""
|
var group_name: String = ""
|
||||||
|
var icon: String = ""
|
||||||
|
var icon_index: Int = 1
|
||||||
|
var group_template: GroupTemplateModel?
|
||||||
|
|
||||||
|
var groupIconURL: String {
|
||||||
|
let templateIcon = group_template?.icon.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
if !templateIcon.isEmpty { return templateIcon }
|
||||||
|
return icon.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupIcon: UIImage {
|
||||||
|
UIImage(named: "GroupIcon/\(icon_index)")
|
||||||
|
?? UIImage(named: "GroupIcon/1")
|
||||||
|
?? UIImage()
|
||||||
|
}
|
||||||
|
|
||||||
init(groupKey: String, groupName: String) {
|
init(groupKey: String, groupName: String) {
|
||||||
group_key = groupKey
|
group_key = groupKey
|
||||||
group_name = groupName
|
group_name = groupName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(info: GroupInfoModel) {
|
||||||
|
group_key = info.group_key
|
||||||
|
group_name = info.name
|
||||||
|
icon = info.groupIconURL
|
||||||
|
icon_index = info.icon_index
|
||||||
|
group_template = info.group_template
|
||||||
|
}
|
||||||
|
|
||||||
init?(map: Map) {}
|
init?(map: Map) {}
|
||||||
|
|
||||||
mutating func mapping(map: Map) {
|
mutating func mapping(map: Map) {
|
||||||
group_key <- map["group_key"]
|
group_key <- map["group_key"]
|
||||||
group_name <- map["group_name"]
|
group_name <- map["group_name"]
|
||||||
|
if group_name.isEmpty {
|
||||||
|
group_name <- map["name"]
|
||||||
|
}
|
||||||
|
icon <- map["icon"]
|
||||||
|
icon_index <- (map["icon_index"], kStrTransformInt)
|
||||||
|
group_template <- map["group_template"]
|
||||||
|
if icon.isEmpty {
|
||||||
|
icon = group_template?.icon ?? ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func apply(_ info: GroupInfoModel) {
|
||||||
|
if group_name.isEmpty || group_name == "未知圈子" {
|
||||||
|
group_name = info.name
|
||||||
|
}
|
||||||
|
if icon.isEmpty { icon = info.groupIconURL }
|
||||||
|
if icon_index <= 1 { icon_index = info.icon_index }
|
||||||
|
if group_template == nil { group_template = info.group_template }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] {
|
static func deduplicated(_ groups: [CommonGroupModel]) -> [CommonGroupModel] {
|
||||||
|
|
@ -415,12 +506,14 @@ struct GroupMemberModel: Mappable, Equatable {
|
||||||
|
|
||||||
struct MemberExtra: Mappable, Equatable {
|
struct MemberExtra: Mappable, Equatable {
|
||||||
var relation_idx: String = ""
|
var relation_idx: String = ""
|
||||||
|
var privilege = GroupPrivilege()
|
||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
init?(map: Map) {}
|
init?(map: Map) {}
|
||||||
|
|
||||||
mutating func mapping(map: Map) {
|
mutating func mapping(map: Map) {
|
||||||
relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform)
|
relation_idx <- (map["relation_idx"], kMemberRelationIndexTransform)
|
||||||
|
privilege <- map["privilege"]
|
||||||
}
|
}
|
||||||
|
|
||||||
static func decode(_ value: Any?) -> MemberExtra {
|
static func decode(_ value: Any?) -> MemberExtra {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// MailboxModel.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import ObjectMapper
|
||||||
|
|
||||||
|
enum MailboxCategory: Int, CaseIterable {
|
||||||
|
case system = 1
|
||||||
|
case notice = 2
|
||||||
|
case interaction = 3
|
||||||
|
|
||||||
|
static let displayOrder: [MailboxCategory] = [.interaction, .notice, .system]
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .interaction:
|
||||||
|
return "互动"
|
||||||
|
case .notice:
|
||||||
|
return "通知"
|
||||||
|
case .system:
|
||||||
|
return "系统"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var detailTitle: String { "\(title)消息" }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MailboxMessage: Mappable, Equatable {
|
||||||
|
var id = 0
|
||||||
|
var title = ""
|
||||||
|
var content = ""
|
||||||
|
var isRead = false
|
||||||
|
var type = MailboxCategory.system.rawValue
|
||||||
|
var senderId = ""
|
||||||
|
var senderIcon = ""
|
||||||
|
var modifyTime = ""
|
||||||
|
|
||||||
|
var category: MailboxCategory {
|
||||||
|
MailboxCategory(rawValue: type) ?? .system
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
id <- (map["id"], kStrTransformInt)
|
||||||
|
title <- (map["title"], kFlexString)
|
||||||
|
content <- (map["content"], kFlexString)
|
||||||
|
isRead <- map["is_read"]
|
||||||
|
type <- (map["type"], kStrTransformInt)
|
||||||
|
senderId <- (map["sender_id"], kFlexString)
|
||||||
|
senderIcon <- (map["sender_icon"], kFlexString)
|
||||||
|
modifyTime <- (map["mtime"], kFlexString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MailboxListData: Mappable {
|
||||||
|
var page = 1
|
||||||
|
var size = 10
|
||||||
|
var total = 0
|
||||||
|
var messages: [MailboxMessage] = []
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
page <- (map["page"], kStrTransformInt)
|
||||||
|
size <- (map["size"], kStrTransformInt)
|
||||||
|
total <- (map["total"], kStrTransformInt)
|
||||||
|
messages <- map["datas"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MailboxListResponse: BaseModelProtocol {
|
||||||
|
var code: String?
|
||||||
|
var message: String?
|
||||||
|
var data: MailboxListData?
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
code <- (map["code"], kIntTransformStr)
|
||||||
|
message <- map["message"]
|
||||||
|
data <- map["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MailboxDetailResponse: BaseModelProtocol {
|
||||||
|
var code: String?
|
||||||
|
var message: String?
|
||||||
|
var data: MailboxMessage?
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
code <- (map["code"], kIntTransformStr)
|
||||||
|
message <- map["message"]
|
||||||
|
data <- map["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ final class AppRestrictCell: UITableViewCell {
|
||||||
private let systemIconHost = UIView()
|
private let systemIconHost = UIView()
|
||||||
private let linkIconView = UIImageView()
|
private let linkIconView = UIImageView()
|
||||||
private let catalogIconView = UIImageView()
|
private let catalogIconView = UIImageView()
|
||||||
private let placeholderView = UIView()
|
private let placeholderView = UIButton(type: .custom)
|
||||||
private let placeholderLab = UILabel()
|
private let placeholderLab = UILabel()
|
||||||
|
|
||||||
private let divider = UIView()
|
private let divider = UIView()
|
||||||
|
|
@ -87,6 +87,7 @@ final class AppRestrictCell: UITableViewCell {
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(Metric.appIcon)
|
.width(Metric.appIcon)
|
||||||
.height(Metric.appIcon)
|
.height(Metric.appIcon)
|
||||||
|
placeholderView.addTarget(self, action: #selector(tapPair), for: .touchUpInside)
|
||||||
placeholderView.addSubview(placeholderLab)
|
placeholderView.addSubview(placeholderLab)
|
||||||
placeholderLab.text = "?"
|
placeholderLab.text = "?"
|
||||||
placeholderLab.font = .systemFont(ofSize: 18, weight: .bold)
|
placeholderLab.font = .systemFont(ofSize: 18, weight: .bold)
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
|
viewModel.markAsRead()
|
||||||
VoicePlayerManager.shared.stop()
|
VoicePlayerManager.shared.stop()
|
||||||
if isVoiceRecording {
|
if isVoiceRecording {
|
||||||
finishVoiceRecording(action: .discard)
|
finishVoiceRecording(action: .discard)
|
||||||
|
|
@ -156,6 +157,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
guard self.rootView.voiceRecordView.isHidden else { return }
|
guard self.rootView.voiceRecordView.isHidden else { return }
|
||||||
guard self.rootView.emojiPanelView.isHidden else { return }
|
guard self.rootView.emojiPanelView.isHidden else { return }
|
||||||
let duration = (noti.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
let duration = (noti.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double) ?? 0.25
|
||||||
|
self.rootView.refreshComposer()
|
||||||
UIView.animate(withDuration: duration) {
|
UIView.animate(withDuration: duration) {
|
||||||
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin)
|
self.rootView.bottomBar.layoutChain.bottom(kSafeBottomMargin)
|
||||||
self.rootView.layoutIfNeeded()
|
self.rootView.layoutIfNeeded()
|
||||||
|
|
@ -652,6 +654,7 @@ final class GroupChatVC: BaseViewController {
|
||||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||||
self.viewModel.input.sendMessage.onNext(text)
|
self.viewModel.input.sendMessage.onNext(text)
|
||||||
self.rootView.textField.text = ""
|
self.rootView.textField.text = ""
|
||||||
|
self.rootView.refreshComposer()
|
||||||
self.hideMentionPicker()
|
self.hideMentionPicker()
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
@ -1095,6 +1098,7 @@ extension GroupChatVC: UITextFieldDelegate, UITableViewDataSource {
|
||||||
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
|
guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return true }
|
||||||
viewModel.input.sendMessage.onNext(text)
|
viewModel.input.sendMessage.onNext(text)
|
||||||
textField.text = ""
|
textField.text = ""
|
||||||
|
rootView.refreshComposer()
|
||||||
hideMentionPicker()
|
hideMentionPicker()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1750,9 +1750,29 @@ extension GroupChatView: UICollectionViewDelegate {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private enum VoiceBubbleLayout {
|
||||||
|
static let minWidth: CGFloat = 80
|
||||||
|
static let maxWidthCap: CGFloat = 220
|
||||||
|
static let maxWidthRatio: CGFloat = 0.55
|
||||||
|
static let maxDurationSeconds = 60
|
||||||
|
|
||||||
|
static func durationSeconds(from content: String) -> Int {
|
||||||
|
let raw = content.int
|
||||||
|
let seconds = raw >= 1000 ? raw / 1000 : raw
|
||||||
|
return min(Int(max(seconds, 1)), maxDurationSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func bubbleWidth(durationSeconds: Int) -> CGFloat {
|
||||||
|
let maxW = min(UIScreen.main.bounds.width * maxWidthRatio, maxWidthCap)
|
||||||
|
let t = CGFloat(durationSeconds - 1) / CGFloat(maxDurationSeconds - 1)
|
||||||
|
return (minWidth + (maxW - minWidth) * t).rounded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 发送的语音消息
|
// MARK: - 发送的语音消息
|
||||||
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
var menuAnchorView: UIView { bubbleView }
|
var menuAnchorView: UIView { bubbleView }
|
||||||
|
private var bubbleWidthConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
private let timeLabel: UILabel = {
|
private let timeLabel: UILabel = {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
|
|
@ -1828,10 +1848,12 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
bubbleView.layoutChain
|
bubbleView.layoutChain
|
||||||
.topToBottomOfView(senderNameView, offset: 5)
|
.topToBottomOfView(senderNameView, offset: 5)
|
||||||
.rightToView(senderNameView)
|
.rightToView(senderNameView)
|
||||||
.width(105).height(39).bottom(10)
|
.height(39).bottom(10)
|
||||||
|
bubbleWidthConstraint = bubbleView.widthAnchor.constraint(equalToConstant: VoiceBubbleLayout.minWidth)
|
||||||
|
bubbleWidthConstraint.isActive = true
|
||||||
|
|
||||||
playAnimation.layoutChain
|
playAnimation.layoutChain
|
||||||
.right(36)
|
.right(10)
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(20).height(20)
|
.width(20).height(20)
|
||||||
|
|
||||||
|
|
@ -1849,8 +1871,9 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
||||||
avatarView.setHeadPic(msg.headPic)
|
avatarView.setHeadPic(msg.headPic)
|
||||||
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
|
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
|
||||||
let dur = msg.content.int / 1000
|
let dur = VoiceBubbleLayout.durationSeconds(from: msg.content)
|
||||||
durationLabel.text = dur > 0 ? "\(dur)''" : ""
|
durationLabel.text = "\(dur)''"
|
||||||
|
bubbleWidthConstraint.constant = VoiceBubbleLayout.bubbleWidth(durationSeconds: dur)
|
||||||
voiceUrl = msg.voiceUrl
|
voiceUrl = msg.voiceUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1887,14 +1910,16 @@ final class VoiceSendMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
// MARK: - 收到的语音消息
|
// MARK: - 收到的语音消息
|
||||||
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
var menuAnchorView: UIView { bubbleView }
|
var menuAnchorView: UIView { bubbleView }
|
||||||
|
private var bubbleWidthConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
func configure(_ msg: ChatMessage) {
|
func configure(_ msg: ChatMessage) {
|
||||||
timeLabel.isHidden = !msg.showTime
|
timeLabel.isHidden = !msg.showTime
|
||||||
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
timeLabel.text = msg.showTime ? chatFormatTime(msg.timestamp) : nil
|
||||||
avatarView.setHeadPic(msg.headPic)
|
avatarView.setHeadPic(msg.headPic)
|
||||||
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
|
senderNameView.configure(name: msg.senderName, isOwner: msg.isCircleOwner, relationIdx: msg.relationIdx)
|
||||||
let dur = msg.content.int / 1000
|
let dur = VoiceBubbleLayout.durationSeconds(from: msg.content)
|
||||||
durationLabel.text = dur > 0 ? "\(dur)''" : ""
|
durationLabel.text = "\(dur)''"
|
||||||
|
bubbleWidthConstraint.constant = VoiceBubbleLayout.bubbleWidth(durationSeconds: dur)
|
||||||
voiceUrl = msg.voiceUrl
|
voiceUrl = msg.voiceUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1992,12 +2017,13 @@ final class VoiceReceivedMsgCell: UITableViewCell, VoicePlaybackView {
|
||||||
bubbleView.layoutChain
|
bubbleView.layoutChain
|
||||||
.topToBottomOfView(senderNameView, offset: 5)
|
.topToBottomOfView(senderNameView, offset: 5)
|
||||||
.leftToView(senderNameView)
|
.leftToView(senderNameView)
|
||||||
.width(105)
|
|
||||||
.height(39)
|
.height(39)
|
||||||
.bottom(10)
|
.bottom(10)
|
||||||
|
bubbleWidthConstraint = bubbleView.widthAnchor.constraint(equalToConstant: VoiceBubbleLayout.minWidth)
|
||||||
|
bubbleWidthConstraint.isActive = true
|
||||||
|
|
||||||
playAnimation.layoutChain
|
playAnimation.layoutChain
|
||||||
.left(36)
|
.left(10)
|
||||||
.centerY()
|
.centerY()
|
||||||
.width(20).height(20)
|
.width(20).height(20)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -692,7 +692,8 @@ final class GroupChatViewModel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func markAsRead() {
|
func markAsRead() {
|
||||||
|
guard !conversationID.isEmpty else { return }
|
||||||
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
OIMManager.manager.markConversationMessage(asRead: conversationID,
|
||||||
onSuccess: nil,
|
onSuccess: nil,
|
||||||
onFailure: nil)
|
onFailure: nil)
|
||||||
|
|
@ -716,6 +717,9 @@ final class GroupChatViewModel {
|
||||||
// MARK: - Receive
|
// MARK: - Receive
|
||||||
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
func onReceiveMessage(_ msg: OIMMessageInfo) {
|
||||||
cacheMessage(msg)
|
cacheMessage(msg)
|
||||||
|
if msg.sendID != AppContextManager.shared.userId {
|
||||||
|
markAsRead()
|
||||||
|
}
|
||||||
guard let item = toSectionItem(msg) else { return }
|
guard let item = toSectionItem(msg) else { return }
|
||||||
let ts = timestampFrom(item: item)
|
let ts = timestampFrom(item: item)
|
||||||
var items = messagesSubject.value
|
var items = messagesSubject.value
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,12 @@ final class GroupViewController: BaseViewController {
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.messageBtn.rx.tap
|
||||||
|
.subscribe(onNext: {
|
||||||
|
AppRouter.push(Route.mailbox)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
// 子 Tab:全部圈子(0=joined) / 我的圈子(1=created)
|
// 子 Tab:全部圈子(0=joined) / 我的圈子(1=created)
|
||||||
rootView.joinedTabLabel.rx.tapGesture
|
rootView.joinedTabLabel.rx.tapGesture
|
||||||
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
|
.subscribe(onNext: { [weak self] _ in self?.switchToSegment(0) })
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,392 @@
|
||||||
|
//
|
||||||
|
// MailboxDetailViewController.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import Lottie
|
||||||
|
|
||||||
|
final class MailboxDetailViewController: UIViewController {
|
||||||
|
private var messages: [MailboxMessage]
|
||||||
|
private var currentIndex: Int
|
||||||
|
private var currentMessage: MailboxMessage
|
||||||
|
private let disposeBag = DisposeBag()
|
||||||
|
private let detailView = MailboxDetailView()
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
var onMessageRead: ((MailboxMessage) -> Void)?
|
||||||
|
|
||||||
|
init(messages: [MailboxMessage], initialMessage: MailboxMessage) {
|
||||||
|
self.messages = messages
|
||||||
|
self.currentMessage = initialMessage
|
||||||
|
self.currentIndex = messages.firstIndex(where: { $0.id == initialMessage.id }) ?? 0
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
modalPresentationStyle = .overFullScreen
|
||||||
|
modalTransitionStyle = .crossDissolve
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
view = detailView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
detailView.closeButton.addTarget(self, action: #selector(closeAction), for: .touchUpInside)
|
||||||
|
detailView.previousButton.addTarget(self, action: #selector(previousAction), for: .touchUpInside)
|
||||||
|
detailView.nextButton.addTarget(self, action: #selector(nextAction), for: .touchUpInside)
|
||||||
|
renderCurrentMessage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func renderCurrentMessage() {
|
||||||
|
detailView.configure(
|
||||||
|
message: currentMessage,
|
||||||
|
canGoPrevious: currentIndex > 0,
|
||||||
|
canGoNext: currentIndex + 1 < messages.count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadMessage(at index: Int) {
|
||||||
|
guard messages.indices.contains(index), !isLoading else { return }
|
||||||
|
let cachedMessage = messages[index]
|
||||||
|
isLoading = true
|
||||||
|
detailView.setControlsEnabled(false)
|
||||||
|
DLToast.showLoading()
|
||||||
|
|
||||||
|
UserService.mailboxDetail(id: cachedMessage.id)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.isLoading = false
|
||||||
|
guard let responseMessage = response.data else {
|
||||||
|
self.detailView.setControlsEnabled(true)
|
||||||
|
DLToast.showError(text: response.message ?? "消息详情加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var detail = self.mergeDetail(responseMessage, fallback: cachedMessage)
|
||||||
|
detail.isRead = true
|
||||||
|
self.messages[index] = detail
|
||||||
|
self.currentIndex = index
|
||||||
|
self.currentMessage = detail
|
||||||
|
self.onMessageRead?(detail)
|
||||||
|
self.renderCurrentMessage()
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
self?.isLoading = false
|
||||||
|
self?.detailView.setControlsEnabled(true)
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage {
|
||||||
|
var merged = detail
|
||||||
|
if merged.id == 0 { merged.id = fallback.id }
|
||||||
|
if merged.title.isEmpty { merged.title = fallback.title }
|
||||||
|
if merged.content.isEmpty { merged.content = fallback.content }
|
||||||
|
if merged.senderId.isEmpty { merged.senderId = fallback.senderId }
|
||||||
|
if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon }
|
||||||
|
if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime }
|
||||||
|
if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type }
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func closeAction() {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func previousAction() {
|
||||||
|
loadMessage(at: currentIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func nextAction() {
|
||||||
|
loadMessage(at: currentIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MailboxDetailView: UIView {
|
||||||
|
let previousButton = UIButton(type: .custom)
|
||||||
|
let closeButton = UIButton(type: .custom)
|
||||||
|
let nextButton = UIButton(type: .custom)
|
||||||
|
|
||||||
|
private let cardView = UIView()
|
||||||
|
private let cardBackgroundView = UIImageView()
|
||||||
|
private let birdImageView = UIImageView(image: UIImage(named: "Mailbox/detail_bird"))
|
||||||
|
private let typeLabel = UILabel()
|
||||||
|
private let dividerView = UIView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let scrollView = UIScrollView()
|
||||||
|
private let contentContainerView = UIView()
|
||||||
|
private let contentLabel = UILabel()
|
||||||
|
private let lottieView = LottieAnimationView()
|
||||||
|
private let quickMessageLabel = UILabel()
|
||||||
|
private var animationName: String?
|
||||||
|
private var contentBottomConstraint: NSLayoutConstraint?
|
||||||
|
private var cardHeightConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(message: MailboxMessage, canGoPrevious: Bool, canGoNext: Bool) {
|
||||||
|
typeLabel.text = message.category.detailTitle
|
||||||
|
titleLabel.text = message.title
|
||||||
|
configureContent(message: message)
|
||||||
|
previousButton.isEnabled = canGoPrevious
|
||||||
|
nextButton.isEnabled = canGoNext
|
||||||
|
updateButtonAppearance()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setControlsEnabled(_ enabled: Bool) {
|
||||||
|
previousButton.isUserInteractionEnabled = enabled
|
||||||
|
nextButton.isUserInteractionEnabled = enabled
|
||||||
|
closeButton.isUserInteractionEnabled = enabled
|
||||||
|
previousButton.alpha = enabled ? (previousButton.isEnabled ? 1 : 0.35) : 0.35
|
||||||
|
nextButton.alpha = enabled ? (nextButton.isEnabled ? 1 : 0.35) : 0.35
|
||||||
|
closeButton.alpha = enabled ? 1 : 0.55
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = UIColor.black.withAlphaComponent(0.68)
|
||||||
|
|
||||||
|
cardView.layer.cornerRadius = 31
|
||||||
|
cardView.clipsToBounds = true
|
||||||
|
cardBackgroundView.image = UIImage(named: "Mailbox/detail_card")?.resizableImage(
|
||||||
|
withCapInsets: UIEdgeInsets(top: 70, left: 70, bottom: 70, right: 70),
|
||||||
|
resizingMode: .stretch
|
||||||
|
)
|
||||||
|
cardBackgroundView.contentMode = .scaleToFill
|
||||||
|
|
||||||
|
birdImageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
typeLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
||||||
|
typeLabel.textColor = UIColor(hexStr: "#14324E")
|
||||||
|
typeLabel.textAlignment = .center
|
||||||
|
|
||||||
|
dividerView.backgroundColor = UIColor(hexStr: "#80D8FF", alpha: 0.55)
|
||||||
|
|
||||||
|
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#333333")
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
titleLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
|
||||||
|
contentLabel.font = .systemFont(ofSize: 15)
|
||||||
|
contentLabel.textColor = UIColor(hexStr: "#858585")
|
||||||
|
contentLabel.textAlignment = .center
|
||||||
|
contentLabel.numberOfLines = 0
|
||||||
|
|
||||||
|
lottieView.contentMode = .scaleAspectFit
|
||||||
|
lottieView.loopMode = .loop
|
||||||
|
lottieView.backgroundBehavior = .pauseAndRestore
|
||||||
|
|
||||||
|
quickMessageLabel.font = .systemFont(ofSize: 15)
|
||||||
|
quickMessageLabel.textColor = UIColor(hexStr: "#52606D")
|
||||||
|
quickMessageLabel.textAlignment = .center
|
||||||
|
quickMessageLabel.numberOfLines = 0
|
||||||
|
quickMessageLabel.backgroundColor = UIColor(hexStr: "#DFF5FF")
|
||||||
|
quickMessageLabel.layer.cornerRadius = 9
|
||||||
|
quickMessageLabel.clipsToBounds = true
|
||||||
|
|
||||||
|
configureControlButton(previousButton, title: "上一条")
|
||||||
|
configureControlButton(nextButton, title: "下一条")
|
||||||
|
closeButton.setImage(UIImage(named: "Mailbox/detail_close"), for: .normal)
|
||||||
|
closeButton.accessibilityLabel = "关闭"
|
||||||
|
|
||||||
|
addSubview(cardView)
|
||||||
|
cardView.addSubview(cardBackgroundView)
|
||||||
|
addSubview(birdImageView)
|
||||||
|
cardView.addSubview(typeLabel)
|
||||||
|
cardView.addSubview(dividerView)
|
||||||
|
cardView.addSubview(titleLabel)
|
||||||
|
cardView.addSubview(scrollView)
|
||||||
|
scrollView.addSubview(contentContainerView)
|
||||||
|
contentContainerView.addSubview(contentLabel)
|
||||||
|
contentContainerView.addSubview(lottieView)
|
||||||
|
contentContainerView.addSubview(quickMessageLabel)
|
||||||
|
addSubview(previousButton)
|
||||||
|
addSubview(closeButton)
|
||||||
|
addSubview(nextButton)
|
||||||
|
|
||||||
|
[cardView, cardBackgroundView, birdImageView, typeLabel, dividerView, titleLabel, scrollView,
|
||||||
|
contentContainerView, contentLabel, lottieView, quickMessageLabel, previousButton, closeButton,
|
||||||
|
nextButton].forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
|
||||||
|
|
||||||
|
let cardWidth = min(kScreenWidth - 48, 360)
|
||||||
|
let cardHeightConstraint = cardView.heightAnchor.constraint(equalToConstant: 245)
|
||||||
|
self.cardHeightConstraint = cardHeightConstraint
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
cardView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
cardView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -32),
|
||||||
|
cardView.widthAnchor.constraint(equalToConstant: cardWidth),
|
||||||
|
cardHeightConstraint,
|
||||||
|
cardBackgroundView.topAnchor.constraint(equalTo: cardView.topAnchor),
|
||||||
|
cardBackgroundView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor),
|
||||||
|
cardBackgroundView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor),
|
||||||
|
cardBackgroundView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor),
|
||||||
|
birdImageView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 36),
|
||||||
|
birdImageView.bottomAnchor.constraint(equalTo: cardView.topAnchor, constant: 35),
|
||||||
|
birdImageView.widthAnchor.constraint(equalToConstant: 100),
|
||||||
|
birdImageView.heightAnchor.constraint(equalToConstant: 74),
|
||||||
|
typeLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 49),
|
||||||
|
typeLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 26),
|
||||||
|
typeLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -26),
|
||||||
|
dividerView.topAnchor.constraint(equalTo: typeLabel.bottomAnchor, constant: 15),
|
||||||
|
dividerView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 23),
|
||||||
|
dividerView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -23),
|
||||||
|
dividerView.heightAnchor.constraint(equalToConstant: 0.5),
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: dividerView.bottomAnchor, constant: 20),
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 30),
|
||||||
|
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -30),
|
||||||
|
scrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 13),
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 28),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -28),
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -25),
|
||||||
|
contentContainerView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
||||||
|
contentContainerView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
||||||
|
contentContainerView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
||||||
|
contentContainerView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
|
||||||
|
contentContainerView.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor),
|
||||||
|
contentLabel.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
|
||||||
|
contentLabel.leadingAnchor.constraint(equalTo: contentContainerView.leadingAnchor),
|
||||||
|
contentLabel.trailingAnchor.constraint(equalTo: contentContainerView.trailingAnchor),
|
||||||
|
lottieView.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 10),
|
||||||
|
lottieView.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
|
||||||
|
lottieView.widthAnchor.constraint(equalToConstant: 116),
|
||||||
|
lottieView.heightAnchor.constraint(equalToConstant: 116),
|
||||||
|
quickMessageLabel.topAnchor.constraint(equalTo: contentLabel.bottomAnchor, constant: 14),
|
||||||
|
quickMessageLabel.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
|
||||||
|
quickMessageLabel.leadingAnchor.constraint(greaterThanOrEqualTo: contentContainerView.leadingAnchor, constant: 20),
|
||||||
|
quickMessageLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentContainerView.trailingAnchor, constant: -20),
|
||||||
|
quickMessageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
|
||||||
|
previousButton.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 20),
|
||||||
|
previousButton.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 34),
|
||||||
|
previousButton.heightAnchor.constraint(equalToConstant: 40),
|
||||||
|
nextButton.topAnchor.constraint(equalTo: previousButton.topAnchor),
|
||||||
|
nextButton.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -34),
|
||||||
|
nextButton.heightAnchor.constraint(equalTo: previousButton.heightAnchor),
|
||||||
|
closeButton.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
closeButton.centerYAnchor.constraint(equalTo: previousButton.centerYAnchor),
|
||||||
|
closeButton.widthAnchor.constraint(equalToConstant: 24),
|
||||||
|
closeButton.heightAnchor.constraint(equalToConstant: 24)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureControlButton(_ button: UIButton, title: String) {
|
||||||
|
button.setTitle(title, for: .normal)
|
||||||
|
button.setTitleColor(.white, for: .normal)
|
||||||
|
button.setTitleColor(UIColor.white.withAlphaComponent(0.45), for: .disabled)
|
||||||
|
button.titleLabel?.font = .systemFont(ofSize: 16)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureContent(message: MailboxMessage) {
|
||||||
|
animationName = nil
|
||||||
|
lottieView.stop()
|
||||||
|
lottieView.animation = nil
|
||||||
|
lottieView.isHidden = true
|
||||||
|
quickMessageLabel.isHidden = true
|
||||||
|
contentLabel.text = message.content
|
||||||
|
cardHeightConstraint?.constant = message.content.count > 80 ? 300 : 245
|
||||||
|
|
||||||
|
guard message.category == .interaction,
|
||||||
|
let payload = parsePayload(message.content) else {
|
||||||
|
updateContentBottomAnchor(visibleView: contentLabel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLabel.text = payload.prefix
|
||||||
|
switch payload.kind {
|
||||||
|
case let .animation(name):
|
||||||
|
cardHeightConstraint?.constant = 330
|
||||||
|
animationName = name
|
||||||
|
lottieView.isHidden = false
|
||||||
|
updateContentBottomAnchor(visibleView: lottieView)
|
||||||
|
EmojiPanelCell.loadAnimation(for: name) { [weak self] animation in
|
||||||
|
guard let self, self.animationName == name else { return }
|
||||||
|
guard let animation else {
|
||||||
|
self.showFallbackContent(message.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.lottieView.animation = animation
|
||||||
|
self.lottieView.currentProgress = 0
|
||||||
|
self.lottieView.play()
|
||||||
|
}
|
||||||
|
case let .quickMessage(text):
|
||||||
|
cardHeightConstraint?.constant = 265
|
||||||
|
quickMessageLabel.text = " \(text) "
|
||||||
|
quickMessageLabel.isHidden = false
|
||||||
|
updateContentBottomAnchor(visibleView: quickMessageLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateContentBottomAnchor(visibleView: UIView) {
|
||||||
|
contentBottomConstraint?.isActive = false
|
||||||
|
contentBottomConstraint = visibleView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor)
|
||||||
|
contentBottomConstraint?.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showFallbackContent(_ content: String) {
|
||||||
|
animationName = nil
|
||||||
|
lottieView.stop()
|
||||||
|
lottieView.animation = nil
|
||||||
|
lottieView.isHidden = true
|
||||||
|
quickMessageLabel.isHidden = true
|
||||||
|
contentLabel.text = content
|
||||||
|
updateContentBottomAnchor(visibleView: contentLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parsePayload(_ content: String) -> MailboxPayload? {
|
||||||
|
guard let opening = content.firstIndex(of: "["),
|
||||||
|
let closing = content[opening...].firstIndex(of: "]"),
|
||||||
|
let lastOpening = content.lastIndex(of: "[") else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let codeStart = content.index(after: opening)
|
||||||
|
let code = String(content[codeStart..<closing])
|
||||||
|
guard code.count > 1, let prefixCode = code.first else { return nil }
|
||||||
|
let value = String(code.dropFirst())
|
||||||
|
let prefix = String(content[..<lastOpening]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
|
switch prefixCode {
|
||||||
|
case "1":
|
||||||
|
guard Int(value) != nil else { return nil }
|
||||||
|
return MailboxPayload(prefix: prefix, kind: .animation(name: "normal_\(value)"))
|
||||||
|
case "2":
|
||||||
|
guard Int(value) != nil else { return nil }
|
||||||
|
return MailboxPayload(prefix: prefix, kind: .animation(name: "fun_\(value)"))
|
||||||
|
case "3":
|
||||||
|
guard let index = Int(value), QuickMessageView.messageList.indices.contains(index) else { return nil }
|
||||||
|
return MailboxPayload(prefix: prefix, kind: .quickMessage(QuickMessageView.messageList[index]))
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateButtonAppearance() {
|
||||||
|
previousButton.alpha = previousButton.isEnabled ? 1 : 0.35
|
||||||
|
nextButton.alpha = nextButton.isEnabled ? 1 : 0.35
|
||||||
|
closeButton.alpha = 1
|
||||||
|
setControlsEnabled(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct MailboxPayload {
|
||||||
|
enum Kind {
|
||||||
|
case animation(name: String)
|
||||||
|
case quickMessage(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
let prefix: String
|
||||||
|
let kind: Kind
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,494 @@
|
||||||
|
//
|
||||||
|
// MailboxView.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import MJRefresh
|
||||||
|
|
||||||
|
extension MailboxCategory {
|
||||||
|
var tintColor: UIColor {
|
||||||
|
switch self {
|
||||||
|
case .interaction:
|
||||||
|
return UIColor(hexStr: "#3ED989")
|
||||||
|
case .notice:
|
||||||
|
return UIColor(hexStr: "#C274F7")
|
||||||
|
case .system:
|
||||||
|
return UIColor(hexStr: "#55B8F7")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var backgroundColor: UIColor {
|
||||||
|
switch self {
|
||||||
|
case .interaction:
|
||||||
|
return UIColor(hexStr: "#E7FFF2")
|
||||||
|
case .notice:
|
||||||
|
return UIColor(hexStr: "#F9E9FF")
|
||||||
|
case .system:
|
||||||
|
return UIColor(hexStr: "#E8F8FF")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var iconAssetName: String {
|
||||||
|
switch self {
|
||||||
|
case .interaction:
|
||||||
|
return "Mailbox/interaction"
|
||||||
|
case .notice:
|
||||||
|
return "Mailbox/notice"
|
||||||
|
case .system:
|
||||||
|
return "Mailbox/system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var listIconAssetName: String {
|
||||||
|
switch self {
|
||||||
|
case .system:
|
||||||
|
return "Mailbox/system_message"
|
||||||
|
default:
|
||||||
|
return iconAssetName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MailboxView: UIView {
|
||||||
|
let backButton = UIButton(type: .custom)
|
||||||
|
let clearButton = UIButton(type: .custom)
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
|
||||||
|
private let headerGradientLayer = CAGradientLayer()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let categoryStackView = UIStackView()
|
||||||
|
private let contentTopSeparator = UIView()
|
||||||
|
private let emptyView = UIView()
|
||||||
|
private let emptyImageView = UIImageView(image: UIImage(named: "empty_data"))
|
||||||
|
private let emptyLabel = UILabel()
|
||||||
|
private let loadingIndicator = UIActivityIndicatorView(style: .medium)
|
||||||
|
private var categoryViews: [MailboxCategory: MailboxCategoryView] = [:]
|
||||||
|
|
||||||
|
var onSelectCategory: ((MailboxCategory) -> Void)?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
headerGradientLayer.frame = CGRect(x: 0, y: 0, width: bounds.width, height: kNaviHeight + 132)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectCategory(_ category: MailboxCategory) {
|
||||||
|
categoryViews.forEach { itemCategory, categoryView in
|
||||||
|
categoryView.setSelected(itemCategory == category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUnreadCounts(_ counts: [MailboxCategory: Int]) {
|
||||||
|
categoryViews.forEach { category, categoryView in
|
||||||
|
categoryView.setUnreadCount(counts[category] ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateState(hasMessages: Bool, isLoading: Bool, reachedEnd: Bool) {
|
||||||
|
loadingIndicator.isHidden = !isLoading
|
||||||
|
if isLoading {
|
||||||
|
loadingIndicator.startAnimating()
|
||||||
|
} else {
|
||||||
|
loadingIndicator.stopAnimating()
|
||||||
|
}
|
||||||
|
emptyImageView.isHidden = isLoading || hasMessages || !reachedEnd
|
||||||
|
emptyLabel.isHidden = emptyImageView.isHidden
|
||||||
|
tableView.mj_footer?.isHidden = !hasMessages || reachedEnd
|
||||||
|
if reachedEnd {
|
||||||
|
tableView.mj_footer?.endRefreshingWithNoMoreData()
|
||||||
|
} else {
|
||||||
|
tableView.mj_footer?.resetNoMoreData()
|
||||||
|
tableView.mj_footer?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = .white
|
||||||
|
headerGradientLayer.colors = [
|
||||||
|
UIColor(hexStr: "#DFF9FF").cgColor,
|
||||||
|
UIColor(hexStr: "#F3FFE9").cgColor,
|
||||||
|
UIColor.white.cgColor
|
||||||
|
]
|
||||||
|
headerGradientLayer.locations = [0, 0.58, 1]
|
||||||
|
headerGradientLayer.startPoint = CGPoint(x: 0, y: 0)
|
||||||
|
headerGradientLayer.endPoint = CGPoint(x: 1, y: 1)
|
||||||
|
layer.insertSublayer(headerGradientLayer, at: 0)
|
||||||
|
|
||||||
|
setupNavigationBar()
|
||||||
|
setupCategories()
|
||||||
|
setupTableView()
|
||||||
|
setupEmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNavigationBar() {
|
||||||
|
titleLabel.text = "消息"
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#18304B")
|
||||||
|
titleLabel.font = .systemFont(ofSize: 18, weight: .medium)
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
|
||||||
|
configureNavigationButton(backButton, symbolName: "chevron.left")
|
||||||
|
configureNavigationButton(clearButton, symbolName: "trash.fill")
|
||||||
|
backButton.accessibilityLabel = "返回"
|
||||||
|
clearButton.accessibilityLabel = "清空全部消息"
|
||||||
|
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(backButton)
|
||||||
|
addSubview(clearButton)
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
clearButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 18),
|
||||||
|
backButton.topAnchor.constraint(equalTo: topAnchor, constant: kStatusBarHeight + 8),
|
||||||
|
backButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
|
backButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
|
clearButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -18),
|
||||||
|
clearButton.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
||||||
|
clearButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
|
clearButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
|
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureNavigationButton(_ button: UIButton, symbolName: String) {
|
||||||
|
let configuration = UIImage.SymbolConfiguration(pointSize: 18, weight: .bold)
|
||||||
|
button.setImage(UIImage(systemName: symbolName, withConfiguration: configuration), for: .normal)
|
||||||
|
button.tintColor = UIColor(hexStr: "#213148")
|
||||||
|
button.backgroundColor = UIColor.white.withAlphaComponent(0.88)
|
||||||
|
button.layer.cornerRadius = 10
|
||||||
|
button.layer.shadowColor = UIColor.black.withAlphaComponent(0.08).cgColor
|
||||||
|
button.layer.shadowOpacity = 1
|
||||||
|
button.layer.shadowRadius = 8
|
||||||
|
button.layer.shadowOffset = CGSize(width: 0, height: 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupCategories() {
|
||||||
|
categoryStackView.axis = .horizontal
|
||||||
|
categoryStackView.distribution = .fillEqually
|
||||||
|
categoryStackView.alignment = .fill
|
||||||
|
|
||||||
|
MailboxCategory.displayOrder.forEach { category in
|
||||||
|
let categoryView = MailboxCategoryView(category: category)
|
||||||
|
categoryView.addTarget(self, action: #selector(categoryTapped(_:)), for: .touchUpInside)
|
||||||
|
categoryStackView.addArrangedSubview(categoryView)
|
||||||
|
categoryViews[category] = categoryView
|
||||||
|
}
|
||||||
|
|
||||||
|
contentTopSeparator.backgroundColor = UIColor(hexStr: "#E8E8E8")
|
||||||
|
addSubview(categoryStackView)
|
||||||
|
addSubview(contentTopSeparator)
|
||||||
|
categoryStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentTopSeparator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
categoryStackView.topAnchor.constraint(equalTo: topAnchor, constant: kNaviHeight + 14),
|
||||||
|
categoryStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
categoryStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
categoryStackView.heightAnchor.constraint(equalToConstant: 112),
|
||||||
|
contentTopSeparator.topAnchor.constraint(equalTo: categoryStackView.bottomAnchor),
|
||||||
|
contentTopSeparator.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
contentTopSeparator.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
contentTopSeparator.heightAnchor.constraint(equalToConstant: 0.5)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupTableView() {
|
||||||
|
tableView.backgroundColor = .white
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.rowHeight = 66
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.tableFooterView = UIView()
|
||||||
|
tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: kSafeBottomMargin + 18, right: 0)
|
||||||
|
tableView.register(MailboxMessageCell.self, forCellReuseIdentifier: MailboxMessageCell.reuseIdentifier)
|
||||||
|
|
||||||
|
addSubview(tableView)
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
tableView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupEmptyView() {
|
||||||
|
emptyView.isUserInteractionEnabled = false
|
||||||
|
emptyImageView.contentMode = .scaleAspectFit
|
||||||
|
emptyLabel.text = "这里怎么什么都没有~"
|
||||||
|
emptyLabel.textColor = UIColor(hexStr: "#B1B1B1")
|
||||||
|
emptyLabel.font = .systemFont(ofSize: 14)
|
||||||
|
emptyLabel.textAlignment = .center
|
||||||
|
loadingIndicator.color = UIColor(hexStr: "#16B3FF")
|
||||||
|
|
||||||
|
addSubview(emptyView)
|
||||||
|
emptyView.addSubview(emptyImageView)
|
||||||
|
emptyView.addSubview(emptyLabel)
|
||||||
|
emptyView.addSubview(loadingIndicator)
|
||||||
|
emptyView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
emptyImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
emptyView.topAnchor.constraint(equalTo: contentTopSeparator.bottomAnchor),
|
||||||
|
emptyView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
emptyView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
emptyView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
emptyImageView.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
|
||||||
|
emptyImageView.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -56),
|
||||||
|
emptyImageView.widthAnchor.constraint(equalToConstant: 140),
|
||||||
|
emptyImageView.heightAnchor.constraint(equalToConstant: 120),
|
||||||
|
emptyLabel.topAnchor.constraint(equalTo: emptyImageView.bottomAnchor, constant: 8),
|
||||||
|
emptyLabel.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
|
||||||
|
loadingIndicator.centerXAnchor.constraint(equalTo: emptyView.centerXAnchor),
|
||||||
|
loadingIndicator.centerYAnchor.constraint(equalTo: emptyView.centerYAnchor, constant: -40)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func categoryTapped(_ sender: MailboxCategoryView) {
|
||||||
|
onSelectCategory?(sender.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MailboxCategoryView: UIControl {
|
||||||
|
let category: MailboxCategory
|
||||||
|
|
||||||
|
private let iconBackgroundView = UIView()
|
||||||
|
private let iconImageView = UIImageView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let underlineView = UIView()
|
||||||
|
private let badgeLabel = UILabel()
|
||||||
|
|
||||||
|
init(category: MailboxCategory) {
|
||||||
|
self.category = category
|
||||||
|
super.init(frame: .zero)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSelected(_ selected: Bool) {
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#26364A")
|
||||||
|
underlineView.isHidden = !selected
|
||||||
|
}
|
||||||
|
|
||||||
|
func setUnreadCount(_ count: Int) {
|
||||||
|
badgeLabel.isHidden = count <= 0
|
||||||
|
badgeLabel.text = count > 99 ? "99+" : "\(count)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
accessibilityLabel = category.title
|
||||||
|
iconBackgroundView.isUserInteractionEnabled = false
|
||||||
|
iconBackgroundView.backgroundColor = .clear
|
||||||
|
iconImageView.image = UIImage(named: category.iconAssetName)
|
||||||
|
iconImageView.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
titleLabel.text = category.title
|
||||||
|
titleLabel.textAlignment = .center
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#26364A")
|
||||||
|
titleLabel.font = .systemFont(ofSize: 15)
|
||||||
|
|
||||||
|
underlineView.backgroundColor = UIColor(hexStr: "#13B6FB")
|
||||||
|
underlineView.layer.cornerRadius = 1.5
|
||||||
|
underlineView.isHidden = true
|
||||||
|
|
||||||
|
badgeLabel.backgroundColor = UIColor(hexStr: "#FF5260")
|
||||||
|
badgeLabel.textColor = .white
|
||||||
|
badgeLabel.font = .systemFont(ofSize: 10, weight: .medium)
|
||||||
|
badgeLabel.textAlignment = .center
|
||||||
|
badgeLabel.layer.cornerRadius = 9
|
||||||
|
badgeLabel.clipsToBounds = true
|
||||||
|
badgeLabel.isHidden = true
|
||||||
|
|
||||||
|
addSubview(iconBackgroundView)
|
||||||
|
iconBackgroundView.addSubview(iconImageView)
|
||||||
|
addSubview(titleLabel)
|
||||||
|
addSubview(underlineView)
|
||||||
|
addSubview(badgeLabel)
|
||||||
|
[iconBackgroundView, iconImageView, titleLabel, underlineView, badgeLabel].forEach {
|
||||||
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
iconBackgroundView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
|
||||||
|
iconBackgroundView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
iconBackgroundView.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
iconBackgroundView.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
iconImageView.centerXAnchor.constraint(equalTo: iconBackgroundView.centerXAnchor),
|
||||||
|
iconImageView.centerYAnchor.constraint(equalTo: iconBackgroundView.centerYAnchor),
|
||||||
|
iconImageView.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
iconImageView.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: iconBackgroundView.bottomAnchor, constant: 7),
|
||||||
|
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
underlineView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
underlineView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
underlineView.widthAnchor.constraint(equalToConstant: 18),
|
||||||
|
underlineView.heightAnchor.constraint(equalToConstant: 3),
|
||||||
|
badgeLabel.centerXAnchor.constraint(equalTo: iconBackgroundView.trailingAnchor, constant: -2),
|
||||||
|
badgeLabel.centerYAnchor.constraint(equalTo: iconBackgroundView.topAnchor, constant: 2),
|
||||||
|
badgeLabel.heightAnchor.constraint(equalToConstant: 18),
|
||||||
|
badgeLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 18)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MailboxMessageCell: UITableViewCell {
|
||||||
|
static let reuseIdentifier = "MailboxMessageCell"
|
||||||
|
|
||||||
|
private let avatarView = UIImageView()
|
||||||
|
private let defaultIconView = MailboxTypeIconView()
|
||||||
|
private let unreadDotView = UIView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let contentLabel = UILabel()
|
||||||
|
private let timeLabel = UILabel()
|
||||||
|
private let separatorView = UIView()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatarView.setHeadPic(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(message: MailboxMessage) {
|
||||||
|
titleLabel.text = message.title
|
||||||
|
contentLabel.text = Self.previewText(for: message)
|
||||||
|
timeLabel.text = Self.timeText(from: message.modifyTime)
|
||||||
|
unreadDotView.isHidden = message.isRead
|
||||||
|
|
||||||
|
let senderIcon = message.senderIcon.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let shouldShowAvatar = message.category != .system && !senderIcon.isEmpty && senderIcon != "0"
|
||||||
|
avatarView.isHidden = !shouldShowAvatar
|
||||||
|
defaultIconView.isHidden = shouldShowAvatar
|
||||||
|
if shouldShowAvatar {
|
||||||
|
avatarView.setHeadPic(senderIcon)
|
||||||
|
} else {
|
||||||
|
defaultIconView.configure(category: message.category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
selectionStyle = .none
|
||||||
|
backgroundColor = .white
|
||||||
|
|
||||||
|
avatarView.contentMode = .scaleAspectFill
|
||||||
|
avatarView.layer.cornerRadius = 11
|
||||||
|
avatarView.clipsToBounds = true
|
||||||
|
|
||||||
|
unreadDotView.backgroundColor = UIColor(hexStr: "#FF5260")
|
||||||
|
unreadDotView.layer.cornerRadius = 4
|
||||||
|
|
||||||
|
titleLabel.font = .systemFont(ofSize: 15, weight: .medium)
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#26364A")
|
||||||
|
titleLabel.numberOfLines = 1
|
||||||
|
|
||||||
|
contentLabel.font = .systemFont(ofSize: 13)
|
||||||
|
contentLabel.textColor = UIColor(hexStr: "#AAAAAA")
|
||||||
|
contentLabel.numberOfLines = 1
|
||||||
|
|
||||||
|
timeLabel.font = .systemFont(ofSize: 12)
|
||||||
|
timeLabel.textColor = UIColor(hexStr: "#BBBBBB")
|
||||||
|
timeLabel.textAlignment = .right
|
||||||
|
|
||||||
|
separatorView.backgroundColor = UIColor(hexStr: "#EEEEEE")
|
||||||
|
|
||||||
|
contentView.addSubview(avatarView)
|
||||||
|
contentView.addSubview(defaultIconView)
|
||||||
|
contentView.addSubview(unreadDotView)
|
||||||
|
contentView.addSubview(titleLabel)
|
||||||
|
contentView.addSubview(contentLabel)
|
||||||
|
contentView.addSubview(timeLabel)
|
||||||
|
contentView.addSubview(separatorView)
|
||||||
|
[avatarView, defaultIconView, unreadDotView, titleLabel, contentLabel, timeLabel, separatorView].forEach {
|
||||||
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 26),
|
||||||
|
avatarView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
avatarView.widthAnchor.constraint(equalToConstant: 42),
|
||||||
|
avatarView.heightAnchor.constraint(equalToConstant: 42),
|
||||||
|
defaultIconView.leadingAnchor.constraint(equalTo: avatarView.leadingAnchor),
|
||||||
|
defaultIconView.centerYAnchor.constraint(equalTo: avatarView.centerYAnchor),
|
||||||
|
defaultIconView.widthAnchor.constraint(equalTo: avatarView.widthAnchor),
|
||||||
|
defaultIconView.heightAnchor.constraint(equalTo: avatarView.heightAnchor),
|
||||||
|
unreadDotView.centerXAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: -2),
|
||||||
|
unreadDotView.centerYAnchor.constraint(equalTo: avatarView.topAnchor, constant: 2),
|
||||||
|
unreadDotView.widthAnchor.constraint(equalToConstant: 8),
|
||||||
|
unreadDotView.heightAnchor.constraint(equalToConstant: 8),
|
||||||
|
timeLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15),
|
||||||
|
timeLabel.widthAnchor.constraint(equalToConstant: 46),
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
|
||||||
|
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: timeLabel.leadingAnchor, constant: -8),
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 11),
|
||||||
|
contentLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||||
|
contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5),
|
||||||
|
separatorView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||||
|
separatorView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16),
|
||||||
|
separatorView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
separatorView.heightAnchor.constraint(equalToConstant: 0.5)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func previewText(for message: MailboxMessage) -> String {
|
||||||
|
guard message.category == .interaction,
|
||||||
|
message.content.contains("]"),
|
||||||
|
let bracketIndex = message.content.lastIndex(of: "[") else {
|
||||||
|
return message.content
|
||||||
|
}
|
||||||
|
return String(message.content[..<bracketIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func timeText(from value: String) -> String {
|
||||||
|
guard value.count >= 16 else { return value }
|
||||||
|
let startIndex = value.index(value.startIndex, offsetBy: 11)
|
||||||
|
let endIndex = value.index(startIndex, offsetBy: 5, limitedBy: value.endIndex) ?? value.endIndex
|
||||||
|
return String(value[startIndex..<endIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MailboxTypeIconView: UIView {
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
layer.cornerRadius = 11
|
||||||
|
clipsToBounds = true
|
||||||
|
imageView.contentMode = .center
|
||||||
|
addSubview(imageView)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
imageView.widthAnchor.constraint(equalTo: widthAnchor),
|
||||||
|
imageView.heightAnchor.constraint(equalTo: heightAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(category: MailboxCategory) {
|
||||||
|
backgroundColor = .clear
|
||||||
|
imageView.image = UIImage(named: category.listIconAssetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,372 @@
|
||||||
|
//
|
||||||
|
// MailboxViewController.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import MJRefresh
|
||||||
|
|
||||||
|
final class MailboxViewController: BaseViewController {
|
||||||
|
override var isNavigationBarHidden: Bool { true }
|
||||||
|
|
||||||
|
private enum LoadKind {
|
||||||
|
case initial
|
||||||
|
case refresh
|
||||||
|
case more
|
||||||
|
case category
|
||||||
|
}
|
||||||
|
|
||||||
|
private var rootView: MailboxView!
|
||||||
|
private var messages: [MailboxMessage] = []
|
||||||
|
private var selectedCategory: MailboxCategory = .interaction
|
||||||
|
private var currentPage = 0
|
||||||
|
private var total = 0
|
||||||
|
private var reachedEnd = false
|
||||||
|
private var isLoading = false
|
||||||
|
private var pendingCategory: MailboxCategory?
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = MailboxView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
setupTableView()
|
||||||
|
bindActions()
|
||||||
|
rootView.selectCategory(selectedCategory)
|
||||||
|
updateContentState(showLoading: true)
|
||||||
|
loadPage(
|
||||||
|
1,
|
||||||
|
kind: .initial,
|
||||||
|
replacing: true,
|
||||||
|
category: selectedCategory,
|
||||||
|
baselineCount: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var visibleMessages: [MailboxMessage] {
|
||||||
|
messages.filter { $0.category == selectedCategory }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupTableView() {
|
||||||
|
rootView.tableView.dataSource = self
|
||||||
|
rootView.tableView.delegate = self
|
||||||
|
|
||||||
|
let header = MJRefreshNormalHeader { [weak self] in
|
||||||
|
self?.refreshMessages()
|
||||||
|
}
|
||||||
|
header.addFeedbackGenerator()
|
||||||
|
header.stateLabel?.isHidden = true
|
||||||
|
rootView.tableView.mj_header = header
|
||||||
|
|
||||||
|
let footer = MJRefreshAutoNormalFooter { [weak self] in
|
||||||
|
self?.loadMoreMessages()
|
||||||
|
}
|
||||||
|
footer.stateLabel?.isHidden = true
|
||||||
|
footer.isHidden = true
|
||||||
|
rootView.tableView.mj_footer = footer
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindActions() {
|
||||||
|
rootView.backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
|
||||||
|
rootView.clearButton.addTarget(self, action: #selector(clearAction), for: .touchUpInside)
|
||||||
|
rootView.onSelectCategory = { [weak self] category in
|
||||||
|
self?.selectCategory(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectCategory(_ category: MailboxCategory) {
|
||||||
|
guard selectedCategory != category else { return }
|
||||||
|
selectedCategory = category
|
||||||
|
rootView.selectCategory(category)
|
||||||
|
rootView.tableView.reloadData()
|
||||||
|
rootView.tableView.setContentOffset(
|
||||||
|
CGPoint(x: 0, y: -rootView.tableView.adjustedContentInset.top),
|
||||||
|
animated: false
|
||||||
|
)
|
||||||
|
updateContentState(showLoading: isLoading)
|
||||||
|
ensureMessagesLoaded(for: category)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refreshMessages() {
|
||||||
|
guard !isLoading else {
|
||||||
|
rootView.tableView.mj_header?.endRefreshing()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadPage(
|
||||||
|
1,
|
||||||
|
kind: .refresh,
|
||||||
|
replacing: true,
|
||||||
|
category: selectedCategory,
|
||||||
|
baselineCount: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadMoreMessages() {
|
||||||
|
guard !isLoading, !reachedEnd else {
|
||||||
|
finishRefreshControls()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let category = selectedCategory
|
||||||
|
loadPage(
|
||||||
|
currentPage + 1,
|
||||||
|
kind: .more,
|
||||||
|
replacing: false,
|
||||||
|
category: category,
|
||||||
|
baselineCount: messages(for: category).count
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ensureMessagesLoaded(for category: MailboxCategory) {
|
||||||
|
guard messages(for: category).isEmpty, !reachedEnd else {
|
||||||
|
updateContentState(showLoading: false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !isLoading else {
|
||||||
|
pendingCategory = category
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadPage(
|
||||||
|
currentPage + 1,
|
||||||
|
kind: .category,
|
||||||
|
replacing: false,
|
||||||
|
category: category,
|
||||||
|
baselineCount: 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPage(
|
||||||
|
_ page: Int,
|
||||||
|
kind: LoadKind,
|
||||||
|
replacing: Bool,
|
||||||
|
category: MailboxCategory,
|
||||||
|
baselineCount: Int
|
||||||
|
) {
|
||||||
|
guard !isLoading else { return }
|
||||||
|
isLoading = true
|
||||||
|
pendingCategory = nil
|
||||||
|
updateContentState(showLoading: visibleMessages.isEmpty)
|
||||||
|
|
||||||
|
UserService.mailboxList(page: page)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
guard let data = response.data else {
|
||||||
|
self.finishLoadingWithError(message: response.message ?? "消息加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if replacing {
|
||||||
|
self.messages = data.messages
|
||||||
|
} else {
|
||||||
|
self.mergeMessages(data.messages)
|
||||||
|
}
|
||||||
|
self.currentPage = max(data.page, page)
|
||||||
|
self.total = max(data.total, self.messages.count)
|
||||||
|
self.reachedEnd = self.calculateReachedEnd(data: data)
|
||||||
|
self.rootView.tableView.reloadData()
|
||||||
|
self.updateUnreadCounts()
|
||||||
|
|
||||||
|
let categoryCount = self.messages(for: category).count
|
||||||
|
if self.selectedCategory == category,
|
||||||
|
categoryCount <= baselineCount,
|
||||||
|
!self.reachedEnd {
|
||||||
|
self.isLoading = false
|
||||||
|
self.loadPage(
|
||||||
|
self.currentPage + 1,
|
||||||
|
kind: kind,
|
||||||
|
replacing: false,
|
||||||
|
category: category,
|
||||||
|
baselineCount: baselineCount
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.isLoading = false
|
||||||
|
self.finishRefreshControls()
|
||||||
|
self.updateContentState(showLoading: false)
|
||||||
|
self.loadPendingCategoryIfNeeded()
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
self?.finishLoadingWithError(message: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergeMessages(_ newMessages: [MailboxMessage]) {
|
||||||
|
var indices = Dictionary(uniqueKeysWithValues: messages.enumerated().map { ($0.element.id, $0.offset) })
|
||||||
|
for message in newMessages {
|
||||||
|
if let index = indices[message.id] {
|
||||||
|
messages[index] = message
|
||||||
|
} else {
|
||||||
|
indices[message.id] = messages.count
|
||||||
|
messages.append(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func calculateReachedEnd(data: MailboxListData) -> Bool {
|
||||||
|
if data.total == 0 || data.messages.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if messages.count >= data.total {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
let pageSize = max(data.size, 1)
|
||||||
|
return max(data.page, currentPage) * pageSize >= data.total
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPendingCategoryIfNeeded() {
|
||||||
|
guard let category = pendingCategory ?? (visibleMessages.isEmpty ? selectedCategory : nil) else { return }
|
||||||
|
pendingCategory = nil
|
||||||
|
ensureMessagesLoaded(for: category)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishLoadingWithError(message: String) {
|
||||||
|
isLoading = false
|
||||||
|
finishRefreshControls()
|
||||||
|
updateContentState(showLoading: false)
|
||||||
|
DLToast.showError(text: message)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishRefreshControls() {
|
||||||
|
rootView.tableView.mj_header?.endRefreshing()
|
||||||
|
if reachedEnd {
|
||||||
|
rootView.tableView.mj_footer?.endRefreshingWithNoMoreData()
|
||||||
|
} else {
|
||||||
|
rootView.tableView.mj_footer?.endRefreshing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func messages(for category: MailboxCategory) -> [MailboxMessage] {
|
||||||
|
messages.filter { $0.category == category }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateUnreadCounts() {
|
||||||
|
var counts: [MailboxCategory: Int] = [:]
|
||||||
|
MailboxCategory.displayOrder.forEach { category in
|
||||||
|
counts[category] = messages(for: category).filter { !$0.isRead }.count
|
||||||
|
}
|
||||||
|
rootView.updateUnreadCounts(counts)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateContentState(showLoading: Bool) {
|
||||||
|
rootView.updateState(
|
||||||
|
hasMessages: !visibleMessages.isEmpty,
|
||||||
|
isLoading: showLoading,
|
||||||
|
reachedEnd: reachedEnd
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openMessage(_ message: MailboxMessage) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
UserService.mailboxDetail(id: message.id)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
DLToast.dismiss()
|
||||||
|
guard let responseMessage = response.data else {
|
||||||
|
DLToast.showError(text: response.message ?? "消息详情加载失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var detail = self.mergeDetail(responseMessage, fallback: message)
|
||||||
|
detail.isRead = true
|
||||||
|
self.updateMessage(detail)
|
||||||
|
let categoryMessages = self.messages(for: detail.category)
|
||||||
|
let detailController = MailboxDetailViewController(
|
||||||
|
messages: categoryMessages,
|
||||||
|
initialMessage: detail
|
||||||
|
)
|
||||||
|
detailController.onMessageRead = { [weak self] updatedMessage in
|
||||||
|
self?.updateMessage(updatedMessage)
|
||||||
|
}
|
||||||
|
self.present(detailController, animated: true)
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateMessage(_ updatedMessage: MailboxMessage) {
|
||||||
|
guard let index = messages.firstIndex(where: { $0.id == updatedMessage.id }) else { return }
|
||||||
|
messages[index] = updatedMessage
|
||||||
|
rootView.tableView.reloadData()
|
||||||
|
updateUnreadCounts()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func mergeDetail(_ detail: MailboxMessage, fallback: MailboxMessage) -> MailboxMessage {
|
||||||
|
var merged = detail
|
||||||
|
if merged.id == 0 { merged.id = fallback.id }
|
||||||
|
if merged.title.isEmpty { merged.title = fallback.title }
|
||||||
|
if merged.content.isEmpty { merged.content = fallback.content }
|
||||||
|
if merged.senderId.isEmpty { merged.senderId = fallback.senderId }
|
||||||
|
if merged.senderIcon.isEmpty { merged.senderIcon = fallback.senderIcon }
|
||||||
|
if merged.modifyTime.isEmpty { merged.modifyTime = fallback.modifyTime }
|
||||||
|
if MailboxCategory(rawValue: merged.type) == nil { merged.type = fallback.type }
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func backAction() {
|
||||||
|
navigationController?.popViewController(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func clearAction() {
|
||||||
|
guard !messages.isEmpty else {
|
||||||
|
DLToast.show(text: "暂无消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showConfirmPop(
|
||||||
|
title: "清空全部消息",
|
||||||
|
message: "清空后无法恢复,是否继续?",
|
||||||
|
confirmText: "清空",
|
||||||
|
confirmBlock: { [weak self] in
|
||||||
|
self?.requestClearAll()
|
||||||
|
},
|
||||||
|
cancelText: "取消"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestClearAll() {
|
||||||
|
DLToast.showLoading()
|
||||||
|
UserService.clearMailbox()
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
DLToast.dismiss()
|
||||||
|
self.messages = []
|
||||||
|
self.currentPage = 0
|
||||||
|
self.total = 0
|
||||||
|
self.reachedEnd = true
|
||||||
|
self.pendingCategory = nil
|
||||||
|
self.rootView.tableView.reloadData()
|
||||||
|
self.updateUnreadCounts()
|
||||||
|
self.finishRefreshControls()
|
||||||
|
self.updateContentState(showLoading: false)
|
||||||
|
DLToast.showSuccess(text: "已清空")
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MailboxViewController: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
visibleMessages.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
guard let cell = tableView.dequeueReusableCell(
|
||||||
|
withIdentifier: MailboxMessageCell.reuseIdentifier,
|
||||||
|
for: indexPath
|
||||||
|
) as? MailboxMessageCell else {
|
||||||
|
return UITableViewCell()
|
||||||
|
}
|
||||||
|
cell.configure(message: visibleMessages[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
openMessage(visibleMessages[indexPath.row])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,12 +13,26 @@ final class MemberInfoVC: BaseViewController {
|
||||||
private var members: [GroupMemberModel]
|
private var members: [GroupMemberModel]
|
||||||
private var selectedUserId: String
|
private var selectedUserId: String
|
||||||
private var groupModel: GroupModel?
|
private var groupModel: GroupModel?
|
||||||
|
private var routeGroupKey: String
|
||||||
private var isLoadingRelations = false
|
private var isLoadingRelations = false
|
||||||
|
|
||||||
init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?) {
|
private var resolvedGroupKey: String {
|
||||||
|
let fromModel = groupModel?.default_group_key.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||||
|
if !fromModel.isEmpty { return fromModel }
|
||||||
|
let fromRoute = routeGroupKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !fromRoute.isEmpty { return fromRoute }
|
||||||
|
let fromGroups = groupModel?.groups.first(where: { !$0.group_key.isEmpty })?.group_key
|
||||||
|
?? groupModel?.groups.first?.group_key
|
||||||
|
?? ""
|
||||||
|
if !fromGroups.isEmpty { return fromGroups }
|
||||||
|
return AppContextManager.shared.defaultGroupKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(members: [GroupMemberModel], selectedUserId: String, groupModel: GroupModel?, groupKey: String = "") {
|
||||||
self.members = members
|
self.members = members
|
||||||
self.selectedUserId = selectedUserId
|
self.selectedUserId = selectedUserId
|
||||||
self.groupModel = groupModel
|
self.groupModel = groupModel
|
||||||
|
self.routeGroupKey = groupKey
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +49,8 @@ final class MemberInfoVC: BaseViewController {
|
||||||
let members = membersJson.compactMap { GroupMemberModel(JSON: $0) }
|
let members = membersJson.compactMap { GroupMemberModel(JSON: $0) }
|
||||||
let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? ""
|
let selected = (userInfo["userId"] as? String) ?? members.first?.user_id ?? ""
|
||||||
let group = (userInfo["groupJson"] as? [String: Any]).flatMap { GroupModel(JSON: $0) }
|
let group = (userInfo["groupJson"] as? [String: Any]).flatMap { GroupModel(JSON: $0) }
|
||||||
self.init(members: members, selectedUserId: selected, groupModel: group)
|
let groupKey = (userInfo["groupKey"] as? String) ?? ""
|
||||||
|
self.init(members: members, selectedUserId: selected, groupModel: group, groupKey: groupKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||||
|
|
@ -129,26 +144,23 @@ final class MemberInfoVC: BaseViewController {
|
||||||
var groups = CommonGroupModel.deduplicated(member?.common_groups ?? [])
|
var groups = CommonGroupModel.deduplicated(member?.common_groups ?? [])
|
||||||
let groupInfos = groupModel?.groups ?? []
|
let groupInfos = groupModel?.groups ?? []
|
||||||
|
|
||||||
for index in groups.indices where groups[index].group_name.isEmpty {
|
for index in groups.indices {
|
||||||
groups[index].group_name = groupInfos.first {
|
if let info = groupInfos.first(where: { $0.group_key == groups[index].group_key }) {
|
||||||
$0.group_key == groups[index].group_key
|
groups[index].apply(info)
|
||||||
}?.name ?? "未知圈子"
|
} else if groups[index].group_name.isEmpty {
|
||||||
|
groups[index].group_name = "未知圈子"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentGroup = groupInfos.first { $0.group_key == groupModel?.default_group_key }
|
let currentGroup = groupInfos.first { $0.group_key == resolvedGroupKey }
|
||||||
?? groupInfos.first
|
?? groupInfos.first
|
||||||
if let currentGroup, !currentGroup.group_key.isEmpty {
|
if let currentGroup, !currentGroup.group_key.isEmpty {
|
||||||
if let index = groups.firstIndex(where: { $0.group_key == currentGroup.group_key }) {
|
if let index = groups.firstIndex(where: { $0.group_key == currentGroup.group_key }) {
|
||||||
var group = groups.remove(at: index)
|
var group = groups.remove(at: index)
|
||||||
if group.group_name.isEmpty || group.group_name == "未知圈子" {
|
group.apply(currentGroup)
|
||||||
group.group_name = currentGroup.name
|
|
||||||
}
|
|
||||||
groups.insert(group, at: 0)
|
groups.insert(group, at: 0)
|
||||||
} else {
|
} else {
|
||||||
groups.insert(
|
groups.insert(CommonGroupModel(info: currentGroup), at: 0)
|
||||||
CommonGroupModel(groupKey: currentGroup.group_key, groupName: currentGroup.name),
|
|
||||||
at: 0
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return CommonGroupModel.deduplicated(groups)
|
return CommonGroupModel.deduplicated(groups)
|
||||||
|
|
@ -195,7 +207,7 @@ final class MemberInfoVC: BaseViewController {
|
||||||
let trimmed = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = remark.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !trimmed.isEmpty,
|
guard !trimmed.isEmpty,
|
||||||
let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
|
let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
|
||||||
let groupKey = groupModel?.default_group_key ?? ""
|
let groupKey = resolvedGroupKey
|
||||||
guard !groupKey.isEmpty else {
|
guard !groupKey.isEmpty else {
|
||||||
DLToast.show(text: "圈子信息缺失")
|
DLToast.show(text: "圈子信息缺失")
|
||||||
return
|
return
|
||||||
|
|
@ -256,7 +268,7 @@ final class MemberInfoVC: BaseViewController {
|
||||||
|
|
||||||
private func requestEditRelation(_ idx: String) {
|
private func requestEditRelation(_ idx: String) {
|
||||||
guard let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
|
guard let member = members.first(where: { $0.user_id == selectedUserId }) else { return }
|
||||||
let groupKey = groupModel?.default_group_key ?? ""
|
let groupKey = resolvedGroupKey
|
||||||
guard !groupKey.isEmpty else {
|
guard !groupKey.isEmpty else {
|
||||||
DLToast.show(text: "圈子信息缺失")
|
DLToast.show(text: "圈子信息缺失")
|
||||||
return
|
return
|
||||||
|
|
@ -367,8 +379,28 @@ final class MemberInfoVC: BaseViewController {
|
||||||
|
|
||||||
private func showCommonGroups(_ groups: [CommonGroupModel]) {
|
private func showCommonGroups(_ groups: [CommonGroupModel]) {
|
||||||
guard groups.count > 1 else { return }
|
guard groups.count > 1 else { return }
|
||||||
let popup = CommonGroupsPopVC(groups: groups, groupInfos: groupModel?.groups ?? [])
|
let presentPopup: ([CommonGroupModel], [GroupInfoModel]) -> Void = { [weak self] groups, infos in
|
||||||
present(popup, animated: true)
|
let popup = CommonGroupsPopVC(groups: groups, groupInfos: infos)
|
||||||
|
self?.present(popup, animated: true)
|
||||||
|
}
|
||||||
|
let infos = groupModel?.groups ?? []
|
||||||
|
if !infos.isEmpty {
|
||||||
|
presentPopup(groups, infos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DLToast.showLoading()
|
||||||
|
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
||||||
|
DLToast.dismiss()
|
||||||
|
if let model = response.model {
|
||||||
|
self?.groupModel = model
|
||||||
|
}
|
||||||
|
presentPopup(self?.resolvedCommonGroups(
|
||||||
|
for: self?.members.first { $0.user_id == self?.selectedUserId }
|
||||||
|
) ?? groups, response.model?.groups ?? [])
|
||||||
|
}, onError: { _ in
|
||||||
|
DLToast.dismiss()
|
||||||
|
presentPopup(groups, [])
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -177,19 +177,6 @@ final class MemberInfoRowView: UIControl {
|
||||||
stack.isUserInteractionEnabled = false
|
stack.isUserInteractionEnabled = false
|
||||||
addSubview(stack)
|
addSubview(stack)
|
||||||
|
|
||||||
if row.showsMoreTags {
|
|
||||||
let moreLabel = UILabel()
|
|
||||||
moreLabel.text = "..."
|
|
||||||
moreLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
|
||||||
moreLabel.textColor = UIColor(hexStr: "#D5923F")
|
|
||||||
moreLabel.backgroundColor = UIColor(hexStr: "#FFF9DD")
|
|
||||||
moreLabel.textAlignment = .center
|
|
||||||
moreLabel.layer.cornerRadius = 7
|
|
||||||
moreLabel.clipsToBounds = true
|
|
||||||
stack.addArrangedSubview(moreLabel)
|
|
||||||
moreLabel.layoutChain.width(26).height(26)
|
|
||||||
}
|
|
||||||
|
|
||||||
let nameLabel = MemberInfoTagLabel()
|
let nameLabel = MemberInfoTagLabel()
|
||||||
nameLabel.text = row.value
|
nameLabel.text = row.value
|
||||||
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
nameLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
|
@ -206,6 +193,17 @@ final class MemberInfoRowView: UIControl {
|
||||||
nameLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 160).isActive = true
|
nameLabel.widthAnchor.constraint(lessThanOrEqualToConstant: 160).isActive = true
|
||||||
|
|
||||||
if row.showsMoreTags {
|
if row.showsMoreTags {
|
||||||
|
let moreLabel = UILabel()
|
||||||
|
moreLabel.text = "..."
|
||||||
|
moreLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
moreLabel.textColor = UIColor(hexStr: "#D5923F")
|
||||||
|
moreLabel.backgroundColor = UIColor(hexStr: "#FFF9DD")
|
||||||
|
moreLabel.textAlignment = .center
|
||||||
|
moreLabel.layer.cornerRadius = 7
|
||||||
|
moreLabel.clipsToBounds = true
|
||||||
|
stack.addArrangedSubview(moreLabel)
|
||||||
|
moreLabel.layoutChain.width(26).height(26)
|
||||||
|
|
||||||
arrow.contentMode = .scaleAspectFit
|
arrow.contentMode = .scaleAspectFit
|
||||||
stack.addArrangedSubview(arrow)
|
stack.addArrangedSubview(arrow)
|
||||||
arrow.layoutChain.width(12).height(12)
|
arrow.layoutChain.width(12).height(12)
|
||||||
|
|
@ -368,10 +366,11 @@ private final class CommonGroupReadonlyCell: UITableViewCell {
|
||||||
|
|
||||||
func configure(group: CommonGroupModel, info: GroupInfoModel?) {
|
func configure(group: CommonGroupModel, info: GroupInfoModel?) {
|
||||||
nameLabel.text = group.group_name.isEmpty ? "未知圈子" : group.group_name
|
nameLabel.text = group.group_name.isEmpty ? "未知圈子" : group.group_name
|
||||||
if let info {
|
let url = info?.groupIconURL ?? group.groupIconURL
|
||||||
iconView.setGroupIcon(info)
|
let fallback = info?.groupIcon ?? group.groupIcon
|
||||||
} else {
|
iconView.setGroupIcon(
|
||||||
iconView.setGroupIcon(url: "", fallback: UIImage(named: "GroupIcon/1"))
|
url: url,
|
||||||
}
|
fallback: fallback.size.width > 0 ? fallback : UIImage(named: "GroupIcon/1")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ class GroupMemberView2: UIView {
|
||||||
private var currentMemberModel: GroupMemberModel?
|
private var currentMemberModel: GroupMemberModel?
|
||||||
private var emojiEchoByUserId: [String: String] = [:]
|
private var emojiEchoByUserId: [String: String] = [:]
|
||||||
private var messageEchoByUserId: [String: String] = [:]
|
private var messageEchoByUserId: [String: String] = [:]
|
||||||
|
private var phoneStatusObserver: NSObjectProtocol?
|
||||||
|
private var weatherRequestID = UUID()
|
||||||
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
|
var onCoupleMemberTap: ((GroupMemberModel) -> Void)?
|
||||||
|
|
||||||
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
|
private static let batteryNormalColor = UIColor(hexStr: "#47DD00")
|
||||||
|
|
@ -45,12 +47,20 @@ class GroupMemberView2: UIView {
|
||||||
updateMoodBadge(model.mood)
|
updateMoodBadge(model.mood)
|
||||||
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
|
statusDotView.backgroundColor = UIColor(hexStr: model.is_online ? "#67EA76" : "#D8D8D8")
|
||||||
statusLab.text = model.is_online ? "在线" : "离线"
|
statusLab.text = model.is_online ? "在线" : "离线"
|
||||||
locationLab.text = model.lastLocation
|
|
||||||
|
|
||||||
let isCurrentUser = model.user_id == AppContextManager.shared.userId
|
let isCurrentUser = model.user_id == AppContextManager.shared.userId
|
||||||
updateDistance(for: model, isCurrentUser: isCurrentUser)
|
if !model.is_online, !isCurrentUser {
|
||||||
|
locationLab.text = "**"
|
||||||
|
distanceView.isHidden = true
|
||||||
|
} else {
|
||||||
|
locationLab.text = model.lastLocation
|
||||||
|
updateDistance(for: model, isCurrentUser: isCurrentUser)
|
||||||
|
}
|
||||||
|
|
||||||
applyPhoneUsage(phoneUsage)
|
applyPhoneUsage(phoneUsage)
|
||||||
|
if isCurrentUser {
|
||||||
|
requestLocalWeather()
|
||||||
|
}
|
||||||
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
|
updateRealtimeInteractionVisibility(isCurrentUser: isCurrentUser)
|
||||||
refreshRealtimeInteraction()
|
refreshRealtimeInteraction()
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +75,7 @@ class GroupMemberView2: UIView {
|
||||||
statusLab.text = ""
|
statusLab.text = ""
|
||||||
locationLab.text = ""
|
locationLab.text = ""
|
||||||
distanceView.isHidden = true
|
distanceView.isHidden = true
|
||||||
|
stopLocalPhoneStatusObserving()
|
||||||
applyPhoneUsage(nil)
|
applyPhoneUsage(nil)
|
||||||
updateRealtimeInteractionVisibility(isCurrentUser: true)
|
updateRealtimeInteractionVisibility(isCurrentUser: true)
|
||||||
refreshRealtimeInteraction()
|
refreshRealtimeInteraction()
|
||||||
|
|
@ -159,14 +170,148 @@ class GroupMemberView2: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) {
|
private func applyPhoneUsage(_ usage: PhoneUsageTodayModel?) {
|
||||||
applyDeviceInfo(usage?.phoneInfo)
|
if isShowingCurrentUser {
|
||||||
|
applyLocalDeviceInfo()
|
||||||
|
startLocalPhoneStatusObserving()
|
||||||
|
} else {
|
||||||
|
stopLocalPhoneStatusObserving()
|
||||||
|
if isShowingOfflineOther {
|
||||||
|
applyOfflineDeviceInfo()
|
||||||
|
} else {
|
||||||
|
applyDeviceInfo(usage?.phoneInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isShowingOfflineOther {
|
||||||
|
phoneReportView.configure(with: nil)
|
||||||
|
appUsageView.configure(with: nil)
|
||||||
|
applyPrivilegeMasks(appUsagePreview: nil)
|
||||||
|
applyOfflineStayPoints()
|
||||||
|
return
|
||||||
|
}
|
||||||
phoneReportView.configure(with: Self.phoneReportPreview(from: usage))
|
phoneReportView.configure(with: Self.phoneReportPreview(from: usage))
|
||||||
let appUsagePreview = MemberAppUsagePreview(topApps: usage?.topApps ?? [])
|
let appUsagePreview = MemberAppUsagePreview(topApps: usage?.topApps ?? [])
|
||||||
appUsageView.configure(with: appUsagePreview)
|
appUsageView.configure(with: appUsagePreview)
|
||||||
updateAppUsageVisibility(appUsagePreview != nil)
|
applyPrivilegeMasks(appUsagePreview: appUsagePreview)
|
||||||
applyStayPoints(usage?.stayPoints ?? [])
|
applyStayPoints(usage?.stayPoints ?? [])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPhoneReportDenied: Bool { phoneReportView.isDenied }
|
||||||
|
var isAppUsageDenied: Bool { appUsageView.isDenied }
|
||||||
|
var onPhoneReportRemind: (() -> Void)?
|
||||||
|
var onAppUsageRemind: (() -> Void)?
|
||||||
|
|
||||||
|
private func applyPrivilegeMasks(appUsagePreview: MemberAppUsagePreview? = nil) {
|
||||||
|
guard currentMemberModel != nil else {
|
||||||
|
phoneReportView.setDenied(false)
|
||||||
|
appUsageView.setDenied(false)
|
||||||
|
updateAppUsageVisibility(appUsagePreview != nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let isSelf = isShowingCurrentUser
|
||||||
|
let privilege = currentMemberModel?.extra.privilege ?? GroupPrivilege()
|
||||||
|
let phoneDenied = !isSelf && !privilege.phone_report
|
||||||
|
let usageDenied = !isSelf && !privilege.app_usage_record
|
||||||
|
phoneReportView.setDenied(phoneDenied)
|
||||||
|
phoneReportView.onDeniedAction = { [weak self] in self?.onPhoneReportRemind?() }
|
||||||
|
appUsageView.setDenied(usageDenied)
|
||||||
|
appUsageView.onDeniedAction = { [weak self] in self?.onAppUsageRemind?() }
|
||||||
|
let showUsage = appUsagePreview != nil || usageDenied
|
||||||
|
if appUsagePreview == nil, usageDenied {
|
||||||
|
appUsageView.configure(with: nil)
|
||||||
|
}
|
||||||
|
updateAppUsageVisibility(showUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isShowingCurrentUser: Bool {
|
||||||
|
guard let userId = currentMemberModel?.user_id, !userId.isEmpty else { return false }
|
||||||
|
return userId == AppContextManager.shared.userId
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isShowingOfflineOther: Bool {
|
||||||
|
guard let model = currentMemberModel, !isShowingCurrentUser else { return false }
|
||||||
|
return !model.is_online
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyOfflineDeviceInfo() {
|
||||||
|
phoneModelIcon.image = UIImage(named: "Home/member_phone_unknown")
|
||||||
|
phoneModelLab.text = "**"
|
||||||
|
phoneModelLab.textColor = Self.batteryDefaultTextColor
|
||||||
|
networkIcon.image = UIImage(named: "Home/member_network_unknown")
|
||||||
|
networkLab.text = "**"
|
||||||
|
networkLab.textColor = Self.batteryDefaultTextColor
|
||||||
|
applyBatteryUnknown(placeholder: "**")
|
||||||
|
applyUnknownWeather(text: "**")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyOfflineStayPoints() {
|
||||||
|
stayNumLab.text = "**"
|
||||||
|
stayLocationView.isHidden = true
|
||||||
|
stayNodataView.isHidden = false
|
||||||
|
stayLocationLab.text = ""
|
||||||
|
stayStatusLab.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startLocalPhoneStatusObserving() {
|
||||||
|
guard phoneStatusObserver == nil else { return }
|
||||||
|
PhoneStatusCollector.shared.start()
|
||||||
|
phoneStatusObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: .phoneStatusDidChange,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
guard let self, self.isShowingCurrentUser else { return }
|
||||||
|
self.applyLocalDeviceInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLocalPhoneStatusObserving() {
|
||||||
|
if let phoneStatusObserver {
|
||||||
|
NotificationCenter.default.removeObserver(phoneStatusObserver)
|
||||||
|
self.phoneStatusObserver = nil
|
||||||
|
}
|
||||||
|
weatherRequestID = UUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyLocalDeviceInfo() {
|
||||||
|
let snap = PhoneStatusCollector.shared.snapshot()
|
||||||
|
phoneModelIcon.image = UIImage(named: "Home/member_phone")
|
||||||
|
phoneModelLab.text = snap.model
|
||||||
|
phoneModelLab.textColor = Self.batteryDefaultTextColor
|
||||||
|
|
||||||
|
let hasNetwork = snap.network != "无网络"
|
||||||
|
networkIcon.image = UIImage(named: hasNetwork ? "Home/member_wifi" : "Home/member_network_unknown")
|
||||||
|
networkLab.text = snap.network
|
||||||
|
networkLab.textColor = Self.batteryDefaultTextColor
|
||||||
|
|
||||||
|
applyBatteryDisplay(percent: snap.batteryPercent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestLocalWeather() {
|
||||||
|
let requestID = UUID()
|
||||||
|
weatherRequestID = requestID
|
||||||
|
applyUnknownWeather(text: "***")
|
||||||
|
guard let latitude = Defaults[\.currentLatitude],
|
||||||
|
let longitude = Defaults[\.currentLongitude] else {
|
||||||
|
applyUnknownWeather(text: "***")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
|
guard CLLocationCoordinate2DIsValid(location.coordinate) else {
|
||||||
|
applyUnknownWeather(text: "***")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
MemberWeatherService.shared.weather(for: location) { [weak self] result in
|
||||||
|
guard let self, self.weatherRequestID == requestID, self.isShowingCurrentUser else { return }
|
||||||
|
switch result {
|
||||||
|
case .success(let snapshot):
|
||||||
|
self.weatherIcon.image = UIImage(named: snapshot.assetName)
|
||||||
|
self.weatherLab.text = snapshot.text
|
||||||
|
case .failure:
|
||||||
|
self.applyUnknownWeather(text: "***")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func updateAppUsageVisibility(_ visible: Bool) {
|
private func updateAppUsageVisibility(_ visible: Bool) {
|
||||||
if visible {
|
if visible {
|
||||||
phoneReportBottomConstraint?.isActive = false
|
phoneReportBottomConstraint?.isActive = false
|
||||||
|
|
@ -209,11 +354,11 @@ class GroupMemberView2: UIView {
|
||||||
weatherLab.text = text
|
weatherLab.text = text
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyBatteryUnknown() {
|
private func applyBatteryUnknown(placeholder: String = "***") {
|
||||||
batteryIcon.image = UIImage(named: "Home/member_battery_unknown")
|
batteryIcon.image = UIImage(named: "Home/member_battery_unknown")
|
||||||
batteryFillView.isHidden = true
|
batteryFillView.isHidden = true
|
||||||
batteryFillView.layoutChain.width(0)
|
batteryFillView.layoutChain.width(0)
|
||||||
batteryLab.text = "***"
|
batteryLab.text = placeholder
|
||||||
batteryLab.textColor = Self.batteryDefaultTextColor
|
batteryLab.textColor = Self.batteryDefaultTextColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1283,6 +1428,7 @@ class GroupMemberView2: UIView {
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
stopLocalPhoneStatusObserving()
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -505,8 +505,36 @@ extension HomeView2: UIScrollViewDelegate {
|
||||||
|
|
||||||
// MARK: - UIGestureRecognizerDelegate
|
// MARK: - UIGestureRecognizerDelegate
|
||||||
extension HomeView2: UIGestureRecognizerDelegate {
|
extension HomeView2: UIGestureRecognizerDelegate {
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
guard let pan = gestureRecognizer as? UIPanGestureRecognizer,
|
||||||
|
pan.view === groupMemberView else { return true }
|
||||||
|
let memberCV = groupMemberView.memberCV
|
||||||
|
guard !memberCV.isHidden, isTouch(pan, inside: memberCV) else { return true }
|
||||||
|
if memberCV.isDragging || memberCV.isDecelerating {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let velocity = pan.velocity(in: groupMemberView)
|
||||||
|
let translation = pan.translation(in: groupMemberView)
|
||||||
|
let dx = max(abs(velocity.x), abs(translation.x) * 60)
|
||||||
|
let dy = max(abs(velocity.y), abs(translation.y) * 60)
|
||||||
|
return dy > dx * 1.8
|
||||||
|
}
|
||||||
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||||
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer) -> Bool {
|
||||||
|
if isMemberListPan(other) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isMemberListPan(_ gesture: UIGestureRecognizer) -> Bool {
|
||||||
|
let memberCV = groupMemberView.memberCV
|
||||||
|
if gesture.view === memberCV { return true }
|
||||||
|
return gesture.view?.isDescendant(of: memberCV) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTouch(_ pan: UIPanGestureRecognizer, inside view: UIView) -> Bool {
|
||||||
|
view.bounds.contains(pan.location(in: view))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -343,13 +343,22 @@ class HomeViewController: BaseViewController {
|
||||||
|
|
||||||
// 手机报告二级页
|
// 手机报告二级页
|
||||||
rootView.groupMemberView.phoneReportView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
|
rootView.groupMemberView.phoneReportView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
|
||||||
self?.openPhoneReportDetail()
|
guard let self, !self.rootView.groupMemberView.isPhoneReportDenied else { return }
|
||||||
|
self.openPhoneReportDetail()
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
// APP 报告二级页
|
// APP 报告二级页
|
||||||
rootView.groupMemberView.appUsageView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
|
rootView.groupMemberView.appUsageView.rx.tapGesture.subscribe(onNext: { [weak self] _ in
|
||||||
self?.openAppUsageReport()
|
guard let self, !self.rootView.groupMemberView.isAppUsageDenied else { return }
|
||||||
|
self.openAppUsageReport()
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.groupMemberView.onPhoneReportRemind = {
|
||||||
|
DLToast.show(text: "已提醒")
|
||||||
|
}
|
||||||
|
rootView.groupMemberView.onAppUsageRemind = {
|
||||||
|
DLToast.show(text: "已提醒")
|
||||||
|
}
|
||||||
|
|
||||||
// 地图回到自己并重新开启自动跟随
|
// 地图回到自己并重新开启自动跟随
|
||||||
rootView.locationView.rx.tapGesture.subscribe { _ in
|
rootView.locationView.rx.tapGesture.subscribe { _ in
|
||||||
|
|
@ -1493,7 +1502,6 @@ extension HomeViewController {
|
||||||
let isSelf = (!record.userId.isEmpty && record.userId == selfId)
|
let isSelf = (!record.userId.isEmpty && record.userId == selfId)
|
||||||
|| userId == selfId
|
|| userId == selfId
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
NotificationCenter.default.post(name: .lockDistractAppsDidChange, object: nil)
|
|
||||||
guard isSelf else { return }
|
guard isSelf else { return }
|
||||||
AppUnlockCoordinator.shared.registerIncomingLock(tokens: record.tokens)
|
AppUnlockCoordinator.shared.registerIncomingLock(tokens: record.tokens)
|
||||||
self?.applyIncomingLocks([record])
|
self?.applyIncomingLocks([record])
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,9 @@ final class MemberAppUsageView: UIView {
|
||||||
private let usageTitleLab = UILabel()
|
private let usageTitleLab = UILabel()
|
||||||
private let topTitleLab = UILabel()
|
private let topTitleLab = UILabel()
|
||||||
private let topStack = UIStackView()
|
private let topStack = UIStackView()
|
||||||
|
private let deniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
private(set) var isDenied = false
|
||||||
|
var onDeniedAction: (() -> Void)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -85,6 +88,19 @@ final class MemberAppUsageView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDenied(_ denied: Bool) {
|
||||||
|
isDenied = denied
|
||||||
|
deniedOverlay.configure(
|
||||||
|
title: "TA未开启设置",
|
||||||
|
message: "无法查看app使用记录",
|
||||||
|
actionTitle: "提醒TA"
|
||||||
|
)
|
||||||
|
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
||||||
|
deniedOverlay.setVisible(denied)
|
||||||
|
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
|
||||||
|
gestureRecognizers?.forEach { $0.isEnabled = !denied }
|
||||||
|
}
|
||||||
|
|
||||||
func configure(with preview: MemberAppUsagePreview?) {
|
func configure(with preview: MemberAppUsagePreview?) {
|
||||||
topStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
topStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
|
|
||||||
|
|
@ -134,6 +150,9 @@ final class MemberAppUsageView: UIView {
|
||||||
.height(176)
|
.height(176)
|
||||||
.bottom()
|
.bottom()
|
||||||
|
|
||||||
|
cardView.addSubview(deniedOverlay)
|
||||||
|
deniedOverlay.layoutChain.edges()
|
||||||
|
|
||||||
usageTitleLab.attributedText = Self.cardTitle("使用时长")
|
usageTitleLab.attributedText = Self.cardTitle("使用时长")
|
||||||
cardView.addSubview(usageTitleLab)
|
cardView.addSubview(usageTitleLab)
|
||||||
usageTitleLab.layoutChain
|
usageTitleLab.layoutChain
|
||||||
|
|
@ -179,6 +198,8 @@ final class MemberAppUsageView: UIView {
|
||||||
.top(43)
|
.top(43)
|
||||||
.left(160)
|
.left(160)
|
||||||
.right(14)
|
.right(14)
|
||||||
|
|
||||||
|
cardView.bringSubviewToFront(deniedOverlay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func cardTitle(_ title: String) -> NSAttributedString {
|
private static func cardTitle(_ title: String) -> NSAttributedString {
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ final class MemberPhoneReportView: UIView {
|
||||||
private let unlockIcon = UIImageView()
|
private let unlockIcon = UIImageView()
|
||||||
private let unlockTitleLab = UILabel()
|
private let unlockTitleLab = UILabel()
|
||||||
private let unlockValueLab = UILabel()
|
private let unlockValueLab = UILabel()
|
||||||
|
private let deniedOverlay = PrivilegeDeniedOverlay()
|
||||||
|
private(set) var isDenied = false
|
||||||
|
var onDeniedAction: (() -> Void)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
@ -63,6 +66,19 @@ final class MemberPhoneReportView: UIView {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDenied(_ denied: Bool) {
|
||||||
|
isDenied = denied
|
||||||
|
deniedOverlay.configure(
|
||||||
|
title: "TA未开启设置",
|
||||||
|
message: "无法查看手机报告",
|
||||||
|
actionTitle: "提醒TA"
|
||||||
|
)
|
||||||
|
deniedOverlay.onAction = { [weak self] in self?.onDeniedAction?() }
|
||||||
|
deniedOverlay.setVisible(denied)
|
||||||
|
deniedOverlay.layer.cornerRadius = cardView.layer.cornerRadius
|
||||||
|
gestureRecognizers?.forEach { $0.isEnabled = !denied }
|
||||||
|
}
|
||||||
|
|
||||||
func configure(with preview: MemberPhoneReportPreview?) {
|
func configure(with preview: MemberPhoneReportPreview?) {
|
||||||
if let preview {
|
if let preview {
|
||||||
screenTimeBadge.configure(seconds: preview.screenTimeSeconds)
|
screenTimeBadge.configure(seconds: preview.screenTimeSeconds)
|
||||||
|
|
@ -103,6 +119,9 @@ final class MemberPhoneReportView: UIView {
|
||||||
.edgesHorzontal(15)
|
.edgesHorzontal(15)
|
||||||
.bottom()
|
.bottom()
|
||||||
|
|
||||||
|
cardView.addSubview(deniedOverlay)
|
||||||
|
deniedOverlay.layoutChain.edges()
|
||||||
|
|
||||||
cardView.addSubview(leftPanel)
|
cardView.addSubview(leftPanel)
|
||||||
cardView.addSubview(rightPanel)
|
cardView.addSubview(rightPanel)
|
||||||
leftPanel.backgroundColor = UIColor(hexStr: "#FFFCEA")
|
leftPanel.backgroundColor = UIColor(hexStr: "#FFFCEA")
|
||||||
|
|
@ -238,6 +257,8 @@ final class MemberPhoneReportView: UIView {
|
||||||
unlockValueUnitLab.layoutChain
|
unlockValueUnitLab.layoutChain
|
||||||
.leftToRightOfView(unlockValueLab, offset: 3)
|
.leftToRightOfView(unlockValueLab, offset: 3)
|
||||||
.bottomToView(unlockValueLab, offset: -4)
|
.bottomToView(unlockValueLab, offset: -4)
|
||||||
|
|
||||||
|
cardView.bringSubviewToFront(deniedOverlay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -339,7 +339,7 @@ final class PhoneBatteryRingView: UIView {
|
||||||
|
|
||||||
func configure(percent: Int?, text: String, isCharging: Bool = false) {
|
func configure(percent: Int?, text: String, isCharging: Bool = false) {
|
||||||
valueLab.text = text
|
valueLab.text = text
|
||||||
bolt.isHidden = false
|
bolt.isHidden = !isCharging
|
||||||
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
|
progress.strokeEnd = CGFloat(max(0, min(100, percent ?? 0))) / 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ final class PhoneReportDetailViewModel {
|
||||||
|
|
||||||
private let groupKey: String
|
private let groupKey: String
|
||||||
private let disposeBag = DisposeBag()
|
private let disposeBag = DisposeBag()
|
||||||
|
private var statusObserveBag = DisposeBag()
|
||||||
private let placeholder = "**"
|
private let placeholder = "**"
|
||||||
private var reportCache: [String: PhoneUsageReportModel] = [:]
|
private var reportCache: [String: PhoneUsageReportModel] = [:]
|
||||||
private var requestIDs: [String: UUID] = [:]
|
private var requestIDs: [String: UUID] = [:]
|
||||||
|
|
@ -59,15 +60,30 @@ final class PhoneReportDetailViewModel {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func isCurrentUser(_ userId: String) -> Bool {
|
||||||
|
!userId.isEmpty && userId == AppContextManager.shared.userId
|
||||||
|
}
|
||||||
|
|
||||||
private func refresh(userId: String) {
|
private func refresh(userId: String) {
|
||||||
guard !userId.isEmpty, !groupKey.isEmpty else {
|
let isSelf = isCurrentUser(userId)
|
||||||
|
updateStatusObserving(isSelf: isSelf)
|
||||||
|
|
||||||
|
guard !userId.isEmpty else {
|
||||||
snapshot.accept(Self.emptySnapshot())
|
snapshot.accept(Self.emptySnapshot())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if groupKey.isEmpty {
|
||||||
|
snapshot.accept(isSelf ? makeLocalStatusSnapshot(report: nil) : Self.emptySnapshot())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let cacheKey = "\(groupKey)|\(userId)"
|
let cacheKey = "\(groupKey)|\(userId)"
|
||||||
if let cached = reportCache[cacheKey] {
|
let cached = reportCache[cacheKey]
|
||||||
snapshot.accept(makeSnapshot(report: cached))
|
if isSelf {
|
||||||
|
snapshot.accept(makeLocalStatusSnapshot(report: cached))
|
||||||
|
} else if let cached {
|
||||||
|
snapshot.accept(makeRemoteSnapshot(report: cached))
|
||||||
} else {
|
} else {
|
||||||
snapshot.accept(Self.emptySnapshot())
|
snapshot.accept(Self.emptySnapshot())
|
||||||
}
|
}
|
||||||
|
|
@ -80,35 +96,72 @@ final class PhoneReportDetailViewModel {
|
||||||
.subscribe(onNext: { [weak self] response in
|
.subscribe(onNext: { [weak self] response in
|
||||||
guard let self, self.requestIDs[cacheKey] == requestID else { return }
|
guard let self, self.requestIDs[cacheKey] == requestID else { return }
|
||||||
self.requestIDs.removeValue(forKey: cacheKey)
|
self.requestIDs.removeValue(forKey: cacheKey)
|
||||||
guard response.code == "0", let report = response.model else { return }
|
guard response.code == "0", let report = response.model else {
|
||||||
|
guard self.selectedMemberId.value == userId, self.isCurrentUser(userId) else { return }
|
||||||
|
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.reportCache[cacheKey] = report
|
self.reportCache[cacheKey] = report
|
||||||
guard self.selectedMemberId.value == userId else { return }
|
guard self.selectedMemberId.value == userId else { return }
|
||||||
self.snapshot.accept(self.makeSnapshot(report: report))
|
self.snapshot.accept(self.makeSnapshot(report: report, userId: userId))
|
||||||
}, onError: { [weak self] _ in
|
}, onError: { [weak self] _ in
|
||||||
guard let self, self.requestIDs[cacheKey] == requestID else { return }
|
guard let self, self.requestIDs[cacheKey] == requestID else { return }
|
||||||
self.requestIDs.removeValue(forKey: cacheKey)
|
self.requestIDs.removeValue(forKey: cacheKey)
|
||||||
|
guard self.selectedMemberId.value == userId, self.isCurrentUser(userId) else { return }
|
||||||
|
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
|
||||||
})
|
})
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makeSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
|
private func updateStatusObserving(isSelf: Bool) {
|
||||||
let info = report.phoneInfo
|
statusObserveBag = DisposeBag()
|
||||||
let storageRatio = Self.storageRatio(used: info?.memoryUsed, total: info?.memoryTotal)
|
guard isSelf else { return }
|
||||||
let screenBars = Self.makeBars(from: report.screenUseTimes, value: { $0.useTime })
|
PhoneStatusCollector.shared.start()
|
||||||
let appUsageBars = Self.makeBars(from: report.appUseCounts, value: { $0.count })
|
NotificationCenter.default.rx.notification(.phoneStatusDidChange)
|
||||||
|
.observe(on: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
let userId = self.selectedMemberId.value
|
||||||
|
guard self.isCurrentUser(userId) else { return }
|
||||||
|
let cacheKey = "\(self.groupKey)|\(userId)"
|
||||||
|
self.snapshot.accept(self.makeLocalStatusSnapshot(report: self.reportCache[cacheKey]))
|
||||||
|
})
|
||||||
|
.disposed(by: statusObserveBag)
|
||||||
|
}
|
||||||
|
|
||||||
let todayScreenSeconds = Self.todayValue(in: report.screenUseTimes, value: { $0.useTime })
|
private func makeSnapshot(report: PhoneUsageReportModel, userId: String) -> StatusSnapshot {
|
||||||
?? Self.nonnegativeInt(from: info?.useTime)
|
if isCurrentUser(userId) {
|
||||||
let todayAppUsageCount = Self.todayValue(in: report.appUseCounts, value: { $0.count })
|
return makeLocalStatusSnapshot(report: report)
|
||||||
let screenTodayText: String
|
|
||||||
if let todayScreenSeconds {
|
|
||||||
screenTodayText = Self.formatDuration(todayScreenSeconds)
|
|
||||||
} else {
|
|
||||||
screenTodayText = placeholder
|
|
||||||
}
|
}
|
||||||
let appUsageTodayText = todayAppUsageCount.map { "\($0)次" } ?? placeholder
|
return makeRemoteSnapshot(report: report)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeLocalStatusSnapshot(report: PhoneUsageReportModel?) -> StatusSnapshot {
|
||||||
|
let local = PhoneStatusCollector.shared.snapshot()
|
||||||
|
let charts = chartFields(from: report)
|
||||||
|
return StatusSnapshot(
|
||||||
|
batteryText: local.batteryPercent == nil ? placeholder : local.batteryText,
|
||||||
|
batteryPercent: local.batteryPercent,
|
||||||
|
isCharging: local.isCharging,
|
||||||
|
estimateText: placeholder,
|
||||||
|
brightnessText: local.brightnessText,
|
||||||
|
volumeText: local.volumeText,
|
||||||
|
wifiText: local.network,
|
||||||
|
deviceText: local.model,
|
||||||
|
storageText: local.memoryUsed == nil ? placeholder : local.storageText,
|
||||||
|
storageRatio: local.storageRatio,
|
||||||
|
brandIconURL: Self.validHTTPURLString(report?.brandIcon ?? "") ?? "",
|
||||||
|
screenTodayText: charts.screenTodayText,
|
||||||
|
appUsageTodayText: charts.appUsageTodayText,
|
||||||
|
screenBars: charts.screenBars,
|
||||||
|
appUsageBars: charts.appUsageBars
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeRemoteSnapshot(report: PhoneUsageReportModel) -> StatusSnapshot {
|
||||||
|
let info = report.phoneInfo
|
||||||
|
let charts = chartFields(from: report)
|
||||||
return StatusSnapshot(
|
return StatusSnapshot(
|
||||||
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
|
batteryText: Self.displayText(info?.battery, placeholder: placeholder),
|
||||||
batteryPercent: Self.percent(from: info?.battery),
|
batteryPercent: Self.percent(from: info?.battery),
|
||||||
|
|
@ -119,15 +172,35 @@ final class PhoneReportDetailViewModel {
|
||||||
wifiText: Self.displayText(info?.network, placeholder: placeholder),
|
wifiText: Self.displayText(info?.network, placeholder: placeholder),
|
||||||
deviceText: Self.displayText(info?.model, placeholder: placeholder),
|
deviceText: Self.displayText(info?.model, placeholder: placeholder),
|
||||||
storageText: Self.storageText(used: info?.memoryUsed, total: info?.memoryTotal, placeholder: placeholder),
|
storageText: Self.storageText(used: info?.memoryUsed, total: info?.memoryTotal, placeholder: placeholder),
|
||||||
storageRatio: storageRatio,
|
storageRatio: Self.storageRatio(used: info?.memoryUsed, total: info?.memoryTotal),
|
||||||
brandIconURL: Self.validHTTPURLString(report.brandIcon) ?? "",
|
brandIconURL: Self.validHTTPURLString(report.brandIcon) ?? "",
|
||||||
screenTodayText: screenTodayText,
|
screenTodayText: charts.screenTodayText,
|
||||||
appUsageTodayText: appUsageTodayText,
|
appUsageTodayText: charts.appUsageTodayText,
|
||||||
screenBars: screenBars,
|
screenBars: charts.screenBars,
|
||||||
appUsageBars: appUsageBars
|
appUsageBars: charts.appUsageBars
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func chartFields(from report: PhoneUsageReportModel?) -> (
|
||||||
|
screenTodayText: String,
|
||||||
|
appUsageTodayText: String,
|
||||||
|
screenBars: [ChartBar],
|
||||||
|
appUsageBars: [ChartBar]
|
||||||
|
) {
|
||||||
|
guard let report else {
|
||||||
|
return (placeholder, placeholder, [], [])
|
||||||
|
}
|
||||||
|
let info = report.phoneInfo
|
||||||
|
let screenBars = Self.makeBars(from: report.screenUseTimes, value: { $0.useTime })
|
||||||
|
let appUsageBars = Self.makeBars(from: report.appUseCounts, value: { $0.count })
|
||||||
|
let todayScreenSeconds = Self.todayValue(in: report.screenUseTimes, value: { $0.useTime })
|
||||||
|
?? Self.nonnegativeInt(from: info?.useTime)
|
||||||
|
let todayAppUsageCount = Self.todayValue(in: report.appUseCounts, value: { $0.count })
|
||||||
|
let screenTodayText = todayScreenSeconds.map { Self.formatDuration($0) } ?? placeholder
|
||||||
|
let appUsageTodayText = todayAppUsageCount.map { "\($0)次" } ?? placeholder
|
||||||
|
return (screenTodayText, appUsageTodayText, screenBars, appUsageBars)
|
||||||
|
}
|
||||||
|
|
||||||
private static func emptySnapshot() -> StatusSnapshot {
|
private static func emptySnapshot() -> StatusSnapshot {
|
||||||
StatusSnapshot(
|
StatusSnapshot(
|
||||||
batteryText: "**",
|
batteryText: "**",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,19 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
|
enum SOSConfiguration {
|
||||||
|
static let countdownDuration: TimeInterval = 5
|
||||||
|
static let slideCancelCompletionRatio: CGFloat = 0.9
|
||||||
|
|
||||||
|
static var countdownSeconds: Int {
|
||||||
|
Int(ceil(countdownDuration))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var countdownDescription: String {
|
||||||
|
"\(countdownSeconds)秒后,会将你的SOS和位置发送到你的圈子和紧急联系人。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class SOSPulseButtonView: UIView {
|
final class SOSPulseButtonView: UIView {
|
||||||
|
|
||||||
var onTap: (() -> Void)?
|
var onTap: (() -> Void)?
|
||||||
|
|
@ -194,7 +207,7 @@ final class SOSCountdownRingView: UIView {
|
||||||
|
|
||||||
private var countdownTimer: Timer?
|
private var countdownTimer: Timer?
|
||||||
private var deadline: Date?
|
private var deadline: Date?
|
||||||
private var duration: TimeInterval = 10
|
private var duration = SOSConfiguration.countdownDuration
|
||||||
private var lastDisplayedValue = -1
|
private var lastDisplayedValue = -1
|
||||||
private var completionDelivered = false
|
private var completionDelivered = false
|
||||||
private var progress: CGFloat = 1
|
private var progress: CGFloat = 1
|
||||||
|
|
@ -241,7 +254,7 @@ final class SOSCountdownRingView: UIView {
|
||||||
alarmContainer.addSubview(alarmRaysView)
|
alarmContainer.addSubview(alarmRaysView)
|
||||||
addSubview(alarmContainer)
|
addSubview(alarmContainer)
|
||||||
|
|
||||||
render(progress: 1, value: 10)
|
render(progress: 1, value: SOSConfiguration.countdownSeconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
|
@ -301,7 +314,7 @@ final class SOSCountdownRingView: UIView {
|
||||||
updateAlarmPosition(progress: progress)
|
updateAlarmPosition(progress: progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
func start(duration: TimeInterval = 10) {
|
func start(duration: TimeInterval = SOSConfiguration.countdownDuration) {
|
||||||
stop(reset: false)
|
stop(reset: false)
|
||||||
self.duration = max(0.1, duration)
|
self.duration = max(0.1, duration)
|
||||||
deadline = Date().addingTimeInterval(self.duration)
|
deadline = Date().addingTimeInterval(self.duration)
|
||||||
|
|
@ -327,7 +340,7 @@ final class SOSCountdownRingView: UIView {
|
||||||
if reset {
|
if reset {
|
||||||
completionDelivered = false
|
completionDelivered = false
|
||||||
lastDisplayedValue = -1
|
lastDisplayedValue = -1
|
||||||
render(progress: 1, value: 10)
|
render(progress: 1, value: SOSConfiguration.countdownSeconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +348,7 @@ final class SOSCountdownRingView: UIView {
|
||||||
guard let deadline else { return }
|
guard let deadline else { return }
|
||||||
let remaining = max(0, deadline.timeIntervalSinceNow)
|
let remaining = max(0, deadline.timeIntervalSinceNow)
|
||||||
let progress = CGFloat(min(1, remaining / duration))
|
let progress = CGFloat(min(1, remaining / duration))
|
||||||
let value = max(0, min(10, Int(ceil(remaining))))
|
let value = max(0, min(Int(ceil(duration)), Int(ceil(remaining))))
|
||||||
render(progress: progress, value: value)
|
render(progress: progress, value: value)
|
||||||
|
|
||||||
guard remaining <= 0, !completionDelivered else { return }
|
guard remaining <= 0, !completionDelivered else { return }
|
||||||
|
|
@ -564,7 +577,8 @@ final class SOSSlideCancelView: UIView {
|
||||||
|
|
||||||
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
||||||
guard !isLocked else { return }
|
guard !isLocked else { return }
|
||||||
let maxX = max(2, bounds.width - thumbView.bounds.width - 2)
|
let minimumX: CGFloat = 2
|
||||||
|
let maxX = max(minimumX, bounds.width - thumbView.bounds.width - minimumX)
|
||||||
|
|
||||||
switch gesture.state {
|
switch gesture.state {
|
||||||
case .began:
|
case .began:
|
||||||
|
|
@ -572,10 +586,11 @@ final class SOSSlideCancelView: UIView {
|
||||||
startX = thumbView.frame.minX
|
startX = thumbView.frame.minX
|
||||||
case .changed:
|
case .changed:
|
||||||
let translation = gesture.translation(in: self)
|
let translation = gesture.translation(in: self)
|
||||||
thumbView.frame.origin.x = min(maxX, max(2, startX + translation.x))
|
thumbView.frame.origin.x = min(maxX, max(minimumX, startX + translation.x))
|
||||||
case .ended, .cancelled, .failed:
|
case .ended, .cancelled, .failed:
|
||||||
isDragging = false
|
isDragging = false
|
||||||
if thumbView.frame.minX >= maxX - 4 {
|
let completionX = minimumX + (maxX - minimumX) * SOSConfiguration.slideCancelCompletionRatio
|
||||||
|
if thumbView.frame.minX >= completionX {
|
||||||
isLocked = true
|
isLocked = true
|
||||||
thumbView.isUserInteractionEnabled = false
|
thumbView.isUserInteractionEnabled = false
|
||||||
onCompleted?()
|
onCompleted?()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,354 @@
|
||||||
|
import UIKit
|
||||||
|
import ObjectMapper
|
||||||
|
import RxSwift
|
||||||
|
|
||||||
|
struct SOSListResponse: BaseModelProtocol {
|
||||||
|
var code: String?
|
||||||
|
var message: String?
|
||||||
|
var list: [SOSListItem] = []
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
code <- (map["code"], kIntTransformStr)
|
||||||
|
message <- map["message"]
|
||||||
|
if message == nil {
|
||||||
|
message <- map["msg"]
|
||||||
|
}
|
||||||
|
list <- map["data"]
|
||||||
|
if list.isEmpty {
|
||||||
|
list <- map["data.list"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SOSListItem: Mappable {
|
||||||
|
var userId = ""
|
||||||
|
var nickName = ""
|
||||||
|
var headPic = ""
|
||||||
|
var lastPosition = ""
|
||||||
|
var groupKey = ""
|
||||||
|
var groupName = ""
|
||||||
|
var sosTime = ""
|
||||||
|
|
||||||
|
init?(map: Map) {}
|
||||||
|
|
||||||
|
mutating func mapping(map: Map) {
|
||||||
|
userId <- (map["user_id"], kIntTransformStr)
|
||||||
|
nickName <- (map["nick_name"], kIntTransformStr)
|
||||||
|
headPic <- (map["head_pic"], kIntTransformStr)
|
||||||
|
lastPosition <- (map["last_position"], kIntTransformStr)
|
||||||
|
groupKey <- (map["group_key"], kIntTransformStr)
|
||||||
|
groupName <- (map["group_name"], kIntTransformStr)
|
||||||
|
sosTime <- (map["sos_time"], kIntTransformStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayAddress: String {
|
||||||
|
let value = lastPosition.trimmed
|
||||||
|
let parts = value.split(separator: ":", maxSplits: 2, omittingEmptySubsequences: false)
|
||||||
|
guard parts.count == 3,
|
||||||
|
Double(parts[0]) != nil,
|
||||||
|
Double(parts[1]) != nil else { return value }
|
||||||
|
let address = String(parts[2]).trimmed
|
||||||
|
return address.isEmpty ? value : address
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayTime: String {
|
||||||
|
guard let date = parsedSOSDate else { return sosTime }
|
||||||
|
return Self.displayDateFormatter.string(from: date)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var parsedSOSDate: Date? {
|
||||||
|
let value = sosTime.trimmed
|
||||||
|
if let timestamp = TimeInterval(value) {
|
||||||
|
return Date(timeIntervalSince1970: timestamp > 10_000_000_000 ? timestamp / 1000 : timestamp)
|
||||||
|
}
|
||||||
|
for formatter in Self.apiDateFormatters {
|
||||||
|
if let date = formatter.date(from: value) {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ISO8601DateFormatter().date(from: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let apiDateFormatters: [DateFormatter] = [
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
"yyyy-MM-dd HH:mm",
|
||||||
|
"yyyy/MM/dd HH:mm:ss",
|
||||||
|
"yyyy/MM/dd HH:mm"
|
||||||
|
].map { format in
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
formatter.timeZone = .current
|
||||||
|
formatter.dateFormat = format
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let displayDateFormatter: DateFormatter = {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.locale = Locale(identifier: "zh_CN")
|
||||||
|
formatter.timeZone = .current
|
||||||
|
formatter.dateFormat = "M月d日 HH:mm"
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SOSListViewController: BaseViewController {
|
||||||
|
private var rootView: SOSListView!
|
||||||
|
private var items: [SOSListItem] = []
|
||||||
|
private var isLoading = false
|
||||||
|
|
||||||
|
override var isNavigationBarHidden: Bool { true }
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = SOSListView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
rootView.tableView.dataSource = self
|
||||||
|
rootView.tableView.delegate = self
|
||||||
|
rootView.refreshControl.addTarget(self, action: #selector(refresh), for: .valueChanged)
|
||||||
|
loadSOSList(showsLoading: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func refresh() {
|
||||||
|
loadSOSList(showsLoading: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadSOSList(showsLoading: Bool) {
|
||||||
|
guard !isLoading else { return }
|
||||||
|
isLoading = true
|
||||||
|
if showsLoading {
|
||||||
|
DLToast.showLoading()
|
||||||
|
}
|
||||||
|
SystemService.sosList()
|
||||||
|
.observe(on: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.finishLoading()
|
||||||
|
guard response.code == "0" else {
|
||||||
|
DLToast.showError(text: response.message ?? "获取求助列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.items = response.list
|
||||||
|
self.rootView.showEmpty(self.items.isEmpty)
|
||||||
|
self.rootView.tableView.reloadData()
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
self?.finishLoading()
|
||||||
|
DLToast.showError(text: error.gatewayMessage ?? error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func finishLoading() {
|
||||||
|
isLoading = false
|
||||||
|
DLToast.dismiss()
|
||||||
|
rootView.refreshControl.endRefreshing()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showLocation(for item: SOSListItem) {
|
||||||
|
guard !item.groupKey.isEmpty, !item.lastPosition.isEmpty else { return }
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: .ShowMemberLocationNotification,
|
||||||
|
object: nil,
|
||||||
|
userInfo: [
|
||||||
|
"last_position": item.lastPosition,
|
||||||
|
"group_key": item.groupKey,
|
||||||
|
"user_id": item.userId
|
||||||
|
]
|
||||||
|
)
|
||||||
|
let mainTabBarController = tabBarController as? MainTabBarController
|
||||||
|
navigationController?.popToRootViewController(animated: true)
|
||||||
|
mainTabBarController?.selectTab(at: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SOSListViewController: UITableViewDataSource, UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||||
|
items.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||||
|
let cell: SOSListCell = tableView.dequeueReusableCell(for: indexPath)
|
||||||
|
cell.configure(with: items[indexPath.row])
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
|
showLocation(for: items[indexPath.row])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SOSListView: UIView {
|
||||||
|
let tableView = UITableView(frame: .zero, style: .plain)
|
||||||
|
let refreshControl = UIRefreshControl()
|
||||||
|
private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
|
||||||
|
private let navView = BaseNavigationView(title: "紧急求助")
|
||||||
|
private let emptyLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func showEmpty(_ isEmpty: Bool) {
|
||||||
|
emptyLabel.isHidden = !isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
|
navBackgroundView.contentMode = .scaleAspectFill
|
||||||
|
navBackgroundView.clipsToBounds = true
|
||||||
|
addSubview(navBackgroundView)
|
||||||
|
addSubview(navView)
|
||||||
|
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.showsVerticalScrollIndicator = false
|
||||||
|
tableView.estimatedRowHeight = 126
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.contentInset = UIEdgeInsets(top: 14, left: 0, bottom: kSafeBottomMargin + 16, right: 0)
|
||||||
|
tableView.register(SOSListCell.self)
|
||||||
|
tableView.refreshControl = refreshControl
|
||||||
|
addSubview(tableView)
|
||||||
|
|
||||||
|
emptyLabel.text = "暂无紧急求助"
|
||||||
|
emptyLabel.textColor = UIColor(hexStr: "#A7ABB2")
|
||||||
|
emptyLabel.font = .systemFont(ofSize: 15)
|
||||||
|
emptyLabel.textAlignment = .center
|
||||||
|
emptyLabel.isHidden = true
|
||||||
|
addSubview(emptyLabel)
|
||||||
|
|
||||||
|
navBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
navView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
emptyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
navBackgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
navBackgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
navBackgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
navBackgroundView.heightAnchor.constraint(equalToConstant: max(kNaviHeight + 70, 150)),
|
||||||
|
navView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
navView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
navView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
navView.heightAnchor.constraint(equalToConstant: kNaviHeight),
|
||||||
|
tableView.topAnchor.constraint(equalTo: navView.bottomAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
emptyLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
emptyLabel.centerYAnchor.constraint(equalTo: centerYAnchor, constant: -20)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SOSListCell: UITableViewCell {
|
||||||
|
private let cardView = UIView()
|
||||||
|
private let avatarView = UIImageView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let addressLabel = UILabel()
|
||||||
|
private let timeLabel = UILabel()
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
avatarView.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(with item: SOSListItem) {
|
||||||
|
avatarView.setHeadPic(item.headPic)
|
||||||
|
titleLabel.attributedText = makeTitle(item)
|
||||||
|
addressLabel.text = item.displayAddress.isEmpty ? "位置暂未获取" : item.displayAddress
|
||||||
|
timeLabel.text = item.displayTime
|
||||||
|
accessibilityLabel = [item.groupName, item.nickName, "紧急求助中", addressLabel.text, timeLabel.text]
|
||||||
|
.compactMap { $0 }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
.joined(separator: ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTitle(_ item: SOSListItem) -> NSAttributedString {
|
||||||
|
let groupName = item.groupName.isEmpty ? "当前" : item.groupName
|
||||||
|
let nickName = item.nickName.isEmpty ? "圈子成员" : item.nickName
|
||||||
|
let text = "\(groupName)圈子的 \(nickName) 紧急求助中"
|
||||||
|
let result = NSMutableAttributedString(
|
||||||
|
string: text,
|
||||||
|
attributes: [
|
||||||
|
.font: UIFont.systemFont(ofSize: 16, weight: .regular),
|
||||||
|
.foregroundColor: UIColor(hexStr: "#273246")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
let highlightColor = UIColor(hexStr: "#04AFFF")
|
||||||
|
result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: groupName))
|
||||||
|
result.addAttribute(.foregroundColor, value: highlightColor, range: (text as NSString).range(of: nickName))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
selectionStyle = .none
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
|
|
||||||
|
cardView.backgroundColor = .white
|
||||||
|
cardView.layer.cornerRadius = 22
|
||||||
|
contentView.addSubview(cardView)
|
||||||
|
|
||||||
|
avatarView.contentMode = .scaleAspectFill
|
||||||
|
avatarView.clipsToBounds = true
|
||||||
|
avatarView.layer.cornerRadius = 25
|
||||||
|
cardView.addSubview(avatarView)
|
||||||
|
|
||||||
|
titleLabel.numberOfLines = 2
|
||||||
|
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
|
cardView.addSubview(titleLabel)
|
||||||
|
|
||||||
|
addressLabel.font = .systemFont(ofSize: 14)
|
||||||
|
addressLabel.textColor = UIColor(hexStr: "#8D9198")
|
||||||
|
addressLabel.numberOfLines = 2
|
||||||
|
cardView.addSubview(addressLabel)
|
||||||
|
|
||||||
|
timeLabel.font = .systemFont(ofSize: 13)
|
||||||
|
timeLabel.textColor = UIColor(hexStr: "#B6B9BE")
|
||||||
|
cardView.addSubview(timeLabel)
|
||||||
|
|
||||||
|
cardView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
avatarView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addressLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
cardView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
cardView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15),
|
||||||
|
cardView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15),
|
||||||
|
cardView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -14),
|
||||||
|
avatarView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 15),
|
||||||
|
avatarView.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 27),
|
||||||
|
avatarView.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
avatarView.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
titleLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 22),
|
||||||
|
titleLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 15),
|
||||||
|
titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -15),
|
||||||
|
addressLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6),
|
||||||
|
addressLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||||
|
addressLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
|
||||||
|
timeLabel.topAnchor.constraint(equalTo: addressLabel.bottomAnchor, constant: 7),
|
||||||
|
timeLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
|
||||||
|
timeLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
|
||||||
|
timeLabel.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -16)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -258,7 +258,7 @@ final class SOSPracticeView: UIView, UIScrollViewDelegate {
|
||||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
let bodyLabel = UILabel()
|
let bodyLabel = UILabel()
|
||||||
bodyLabel.text = "10秒后,会将你的SOS和位置发送到你的圈子和紧急联系人。"
|
bodyLabel.text = SOSConfiguration.countdownDescription
|
||||||
bodyLabel.textColor = UIColor(hexStr: "#273246")
|
bodyLabel.textColor = UIColor(hexStr: "#273246")
|
||||||
bodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
bodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
bodyLabel.textAlignment = .center
|
bodyLabel.textAlignment = .center
|
||||||
|
|
@ -432,7 +432,7 @@ final class SOSPracticeView: UIView, UIScrollViewDelegate {
|
||||||
practicePulseView.setAnimationEnabled(false)
|
practicePulseView.setAnimationEnabled(false)
|
||||||
practiceSlider.configure(style: .practiceCountdown)
|
practiceSlider.configure(style: .practiceCountdown)
|
||||||
practiceSlider.setInteractionEnabled(true)
|
practiceSlider.setInteractionEnabled(true)
|
||||||
practiceCountdownRing.start(duration: 10)
|
practiceCountdownRing.start()
|
||||||
case .success:
|
case .success:
|
||||||
practiceCountdownRing.stop(reset: false)
|
practiceCountdownRing.stop(reset: false)
|
||||||
practicePulseView.setAnimationEnabled(false)
|
practicePulseView.setAnimationEnabled(false)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ final class SOSView: UIView {
|
||||||
var onCountdownCompleted: (() -> Void)?
|
var onCountdownCompleted: (() -> Void)?
|
||||||
var onCancelActive: (() -> Void)?
|
var onCancelActive: (() -> Void)?
|
||||||
var onManageContacts: (() -> Void)?
|
var onManageContacts: (() -> Void)?
|
||||||
|
var onShowSOSList: (() -> Void)?
|
||||||
|
|
||||||
private(set) var state: SOSPageState = .idle
|
private(set) var state: SOSPageState = .idle
|
||||||
let sosPracticeView = SOSPracticeView()
|
let sosPracticeView = SOSPracticeView()
|
||||||
|
|
@ -83,10 +84,10 @@ final class SOSView: UIView {
|
||||||
idlePulseView.setAnimationEnabled(false)
|
idlePulseView.setAnimationEnabled(false)
|
||||||
activePulseView.setAnimationEnabled(false)
|
activePulseView.setAnimationEnabled(false)
|
||||||
countdownTitleLabel.attributedText = Self.cancelTitle
|
countdownTitleLabel.attributedText = Self.cancelTitle
|
||||||
countdownBodyLabel.text = "10秒后,会将你的SOS和位置发送到你的圈子和紧急联系人。"
|
countdownBodyLabel.text = SOSConfiguration.countdownDescription
|
||||||
countdownSlider.configure(style: .countdown)
|
countdownSlider.configure(style: .countdown)
|
||||||
countdownSlider.setInteractionEnabled(true)
|
countdownSlider.setInteractionEnabled(true)
|
||||||
countdownRingView.start(duration: 10)
|
countdownRingView.start()
|
||||||
case .sending:
|
case .sending:
|
||||||
countdownRingView.stop(reset: false)
|
countdownRingView.stop(reset: false)
|
||||||
countdownTitleLabel.text = "正在发送"
|
countdownTitleLabel.text = "正在发送"
|
||||||
|
|
@ -194,10 +195,15 @@ final class SOSView: UIView {
|
||||||
moreButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
|
moreButton.widthAnchor.constraint(equalToConstant: 24).isActive = true
|
||||||
moreButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
moreButton.heightAnchor.constraint(equalToConstant: 24).isActive = true
|
||||||
moreButton.transform = CGAffineTransform(a: 1.5, b: 0, c: 0, d: 1.5, tx: -7, ty: -7)
|
moreButton.transform = CGAffineTransform(a: 1.5, b: 0, c: 0, d: 1.5, tx: -7, ty: -7)
|
||||||
moreButton.accessibilityLabel = "更多"
|
moreButton.accessibilityLabel = "求助列表"
|
||||||
|
moreButton.addTarget(self, action: #selector(showSOSList), for: .touchUpInside)
|
||||||
navView.addRightButton(moreButton)
|
navView.addRightButton(moreButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func showSOSList() {
|
||||||
|
onShowSOSList?()
|
||||||
|
}
|
||||||
|
|
||||||
private func setupInfoCard() {
|
private func setupInfoCard() {
|
||||||
infoCard.backgroundColor = UIColor.white.withAlphaComponent(0.93)
|
infoCard.backgroundColor = UIColor.white.withAlphaComponent(0.93)
|
||||||
infoCard.layer.cornerRadius = 14
|
infoCard.layer.cornerRadius = 14
|
||||||
|
|
@ -417,7 +423,7 @@ final class SOSView: UIView {
|
||||||
contentView.addSubview(countdownTitleLabel)
|
contentView.addSubview(countdownTitleLabel)
|
||||||
countdownTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
countdownTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
countdownBodyLabel.text = "10秒后,会将你的SOS和位置发送到你的圈子和紧急联系人。"
|
countdownBodyLabel.text = SOSConfiguration.countdownDescription
|
||||||
countdownBodyLabel.textColor = UIColor(hexStr: "#273246")
|
countdownBodyLabel.textColor = UIColor(hexStr: "#273246")
|
||||||
countdownBodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
countdownBodyLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
countdownBodyLabel.textAlignment = .center
|
countdownBodyLabel.textAlignment = .center
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,9 @@ final class SOSViewController: BaseViewController {
|
||||||
rootView.onManageContacts = {
|
rootView.onManageContacts = {
|
||||||
AppRouter.push(Route.emergencyContact)
|
AppRouter.push(Route.emergencyContact)
|
||||||
}
|
}
|
||||||
|
rootView.onShowSOSList = {
|
||||||
|
AppRouter.push(Route.sosList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func requestEmergencyContacts() {
|
private func requestEmergencyContacts() {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,9 @@ class LaunchViewController: BaseViewController {
|
||||||
MQTTService.shared.connect()
|
MQTTService.shared.connect()
|
||||||
Defaults[\.loginToken] = model.token
|
Defaults[\.loginToken] = model.token
|
||||||
RelationStore.shared.preload()
|
RelationStore.shared.preload()
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
AppRestrictManager.shared.checkScreenTimePermissionAfterLogin()
|
||||||
|
}
|
||||||
|
|
||||||
AppContextManager.shared.systemConfig = model.config
|
AppContextManager.shared.systemConfig = model.config
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ final class LockDistractVC: BaseViewController {
|
||||||
private var members: [GroupMemberModel] = []
|
private var members: [GroupMemberModel] = []
|
||||||
private var selectedIndex = 0
|
private var selectedIndex = 0
|
||||||
private var lockAppsDisposable: Disposable?
|
private var lockAppsDisposable: Disposable?
|
||||||
|
private let lockStateSubscriptionOwner = UUID()
|
||||||
|
|
||||||
override var isNavigationBarHidden: Bool { true }
|
override var isNavigationBarHidden: Bool { true }
|
||||||
|
|
||||||
|
|
@ -31,6 +32,13 @@ final class LockDistractVC: BaseViewController {
|
||||||
loadGroupMembers()
|
loadGroupMembers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor deinit {
|
||||||
|
MQTTService.shared.updateLockStateSubscriptions(
|
||||||
|
owner: lockStateSubscriptionOwner,
|
||||||
|
userIds: []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private func bind() {
|
private func bind() {
|
||||||
rootView.navView.onBackTapped = { [weak self] in
|
rootView.navView.onBackTapped = { [weak self] in
|
||||||
self?.navigationController?.popViewController(animated: true)
|
self?.navigationController?.popViewController(animated: true)
|
||||||
|
|
@ -59,6 +67,15 @@ final class LockDistractVC: BaseViewController {
|
||||||
LockDistractPermissionPopView.show()
|
LockDistractPermissionPopView.show()
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
|
|
||||||
|
rootView.onPrivilegeAction = { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
if self.selectedMember?.user_id == AppContextManager.shared.userId {
|
||||||
|
AppRouter.push(Route.privacySettings)
|
||||||
|
} else {
|
||||||
|
DLToast.show(text: "已提醒")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rootView.lockBtn.rx.tap
|
rootView.lockBtn.rx.tap
|
||||||
.subscribe(onNext: { [weak self] in
|
.subscribe(onNext: { [weak self] in
|
||||||
self?.performPrimaryAction()
|
self?.performPrimaryAction()
|
||||||
|
|
@ -66,6 +83,18 @@ final class LockDistractVC: BaseViewController {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
NotificationCenter.default.rx.notification(.lockDistractAppsDidChange)
|
NotificationCenter.default.rx.notification(.lockDistractAppsDidChange)
|
||||||
|
.observe(on: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] notification in
|
||||||
|
guard let self else { return }
|
||||||
|
let groupKey = notification.userInfo?[LockDistractAppsChangeUserInfoKey.groupKey] as? String ?? ""
|
||||||
|
let userId = notification.userInfo?[LockDistractAppsChangeUserInfoKey.userId] as? String ?? ""
|
||||||
|
guard groupKey.isEmpty || groupKey == self.activeGroupKey,
|
||||||
|
userId.isEmpty || userId == self.selectedMember?.user_id else { return }
|
||||||
|
self.loadLockableApps()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
|
||||||
|
NotificationCenter.default.rx.notification(UIApplication.didBecomeActiveNotification)
|
||||||
.observe(on: MainScheduler.instance)
|
.observe(on: MainScheduler.instance)
|
||||||
.subscribe(onNext: { [weak self] _ in
|
.subscribe(onNext: { [weak self] _ in
|
||||||
self?.loadLockableApps()
|
self?.loadLockableApps()
|
||||||
|
|
@ -83,6 +112,10 @@ final class LockDistractVC: BaseViewController {
|
||||||
private func applyOwnedGroups(from model: GroupModel) {
|
private func applyOwnedGroups(from model: GroupModel) {
|
||||||
let owned = model.groups.filter(\.is_owner)
|
let owned = model.groups.filter(\.is_owner)
|
||||||
guard !owned.isEmpty else {
|
guard !owned.isEmpty else {
|
||||||
|
MQTTService.shared.updateLockStateSubscriptions(
|
||||||
|
owner: lockStateSubscriptionOwner,
|
||||||
|
userIds: []
|
||||||
|
)
|
||||||
DLToast.show(text: "只有圈主才能使用此功能") { [weak self] in
|
DLToast.show(text: "只有圈主才能使用此功能") { [weak self] in
|
||||||
self?.navigationController?.popViewController(animated: true)
|
self?.navigationController?.popViewController(animated: true)
|
||||||
}
|
}
|
||||||
|
|
@ -126,6 +159,10 @@ final class LockDistractVC: BaseViewController {
|
||||||
|
|
||||||
private func applyMembers(_ members: [GroupMemberModel]) {
|
private func applyMembers(_ members: [GroupMemberModel]) {
|
||||||
self.members = members
|
self.members = members
|
||||||
|
MQTTService.shared.updateLockStateSubscriptions(
|
||||||
|
owner: lockStateSubscriptionOwner,
|
||||||
|
userIds: members.map(\.user_id)
|
||||||
|
)
|
||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
refreshMember()
|
refreshMember()
|
||||||
rootView.setTodayLockCount(0)
|
rootView.setTodayLockCount(0)
|
||||||
|
|
@ -144,16 +181,19 @@ final class LockDistractVC: BaseViewController {
|
||||||
guard members.indices.contains(selectedIndex) else {
|
guard members.indices.contains(selectedIndex) else {
|
||||||
rootView.configureMember(name: " ")
|
rootView.configureMember(name: " ")
|
||||||
rootView.configureLocked(false)
|
rootView.configureLocked(false)
|
||||||
|
rootView.setPrivilegeDenied(false, isCurrentUser: false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let m = members[selectedIndex]
|
let m = members[selectedIndex]
|
||||||
let name = m.showName
|
let name = m.showName
|
||||||
|
let isSelf = m.user_id.trimmed == AppContextManager.shared.userId.trimmed
|
||||||
rootView.configureMember(
|
rootView.configureMember(
|
||||||
name: name,
|
name: name,
|
||||||
headPic: m.displayHeadPic,
|
headPic: m.displayHeadPic,
|
||||||
isCurrentUser: m.user_id.trimmed == AppContextManager.shared.userId.trimmed
|
isCurrentUser: isSelf
|
||||||
)
|
)
|
||||||
rootView.configureLocked(false)
|
rootView.configureLocked(false)
|
||||||
|
rootView.setPrivilegeDenied(!m.extra.privilege.app_lock, isCurrentUser: isSelf)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateGroupName() {
|
private func updateGroupName() {
|
||||||
|
|
@ -186,6 +226,10 @@ final class LockDistractVC: BaseViewController {
|
||||||
lockAppsDisposable?.dispose()
|
lockAppsDisposable?.dispose()
|
||||||
rootView.configureLockableApps([])
|
rootView.configureLockableApps([])
|
||||||
guard let member = selectedMember, !activeGroupKey.isEmpty else { return }
|
guard let member = selectedMember, !activeGroupKey.isEmpty else { return }
|
||||||
|
if !member.extra.privilege.app_lock {
|
||||||
|
rootView.setTodayLockCount(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
let targetUserId = member.user_id
|
let targetUserId = member.user_id
|
||||||
let requestGroupKey = activeGroupKey
|
let requestGroupKey = activeGroupKey
|
||||||
lockAppsDisposable = UserService.phoneLockApps(userId: targetUserId, groupKey: requestGroupKey)
|
lockAppsDisposable = UserService.phoneLockApps(userId: targetUserId, groupKey: requestGroupKey)
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,24 @@ final class LockDistractView: UIView {
|
||||||
(copyTF.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
(copyTF.text ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var onPrivilegeAction: (() -> Void)?
|
||||||
|
private(set) var isPrivilegeDenied = false
|
||||||
|
private let privilegeOverlay = PrivilegeDeniedOverlay()
|
||||||
|
|
||||||
|
func setPrivilegeDenied(_ denied: Bool, isCurrentUser: Bool) {
|
||||||
|
isPrivilegeDenied = denied
|
||||||
|
privilegeOverlay.configure(
|
||||||
|
title: "TA未开启权限",
|
||||||
|
message: "无法查看可锁应用",
|
||||||
|
actionTitle: isCurrentUser ? "去开启" : "提醒TA"
|
||||||
|
)
|
||||||
|
privilegeOverlay.onAction = { [weak self] in self?.onPrivilegeAction?() }
|
||||||
|
privilegeOverlay.setVisible(denied)
|
||||||
|
if denied {
|
||||||
|
contentView.bringSubviewToFront(privilegeOverlay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func applyLockedState() {
|
private func applyLockedState() {
|
||||||
updateLockedEmptyImage()
|
updateLockedEmptyImage()
|
||||||
avatarLockView.isHidden = !isLocked
|
avatarLockView.isHidden = !isLocked
|
||||||
|
|
@ -166,6 +184,7 @@ final class LockDistractView: UIView {
|
||||||
bottomStack.addArrangedSubview(styleSectionTitle)
|
bottomStack.addArrangedSubview(styleSectionTitle)
|
||||||
bottomStack.addArrangedSubview(styleCard)
|
bottomStack.addArrangedSubview(styleCard)
|
||||||
bottomStack.addArrangedSubview(lockedEmptyView)
|
bottomStack.addArrangedSubview(lockedEmptyView)
|
||||||
|
contentView.addSubview(privilegeOverlay)
|
||||||
|
|
||||||
navView.layoutChain
|
navView.layoutChain
|
||||||
.edges(excludingEdge: .bottom)
|
.edges(excludingEdge: .bottom)
|
||||||
|
|
@ -228,6 +247,13 @@ final class LockDistractView: UIView {
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
.bottom(20)
|
.bottom(20)
|
||||||
|
|
||||||
|
privilegeOverlay.layoutChain
|
||||||
|
.topToView(appsSectionTitle)
|
||||||
|
.left(16)
|
||||||
|
.right(16)
|
||||||
|
.bottomToView(bottomStack)
|
||||||
|
privilegeOverlay.layer.cornerRadius = 26
|
||||||
|
|
||||||
styleSectionTitle.layoutChain.left(4)
|
styleSectionTitle.layoutChain.left(4)
|
||||||
lockedEmptyView.layoutChain
|
lockedEmptyView.layoutChain
|
||||||
.centerX()
|
.centerX()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ enum LoginSessionHandler {
|
||||||
Defaults[\.loginToken] = model.token
|
Defaults[\.loginToken] = model.token
|
||||||
AppContextManager.shared.saveAccount(model)
|
AppContextManager.shared.saveAccount(model)
|
||||||
RelationStore.shared.preload()
|
RelationStore.shared.preload()
|
||||||
|
if #available(iOS 16.0, *) {
|
||||||
|
AppRestrictManager.shared.checkScreenTimePermissionAfterLogin()
|
||||||
|
}
|
||||||
DLToast.showSuccess(text: "登录成功")
|
DLToast.showSuccess(text: "登录成功")
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let userId = model.uid {
|
if let userId = model.uid {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class AccountVC: BaseViewController {
|
||||||
// Do any additional setup after loading the view.
|
// Do any additional setup after loading the view.
|
||||||
|
|
||||||
reactiveAction()
|
reactiveAction()
|
||||||
|
rootView.refreshProfile()
|
||||||
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
DispatchQueue.global(qos: .background).async {
|
||||||
let totalByte = AppCacheManager.totalCacheSize()
|
let totalByte = AppCacheManager.totalCacheSize()
|
||||||
|
|
@ -40,6 +41,11 @@ class AccountVC: BaseViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
rootView.refreshProfile()
|
||||||
|
}
|
||||||
|
|
||||||
private func reactiveAction() {
|
private func reactiveAction() {
|
||||||
rootView.logoutBtn.rx.tap.subscribe(onNext: { _ in
|
rootView.logoutBtn.rx.tap.subscribe(onNext: { _ in
|
||||||
|
|
@ -61,6 +67,97 @@ class AccountVC: BaseViewController {
|
||||||
|
|
||||||
let versionTap = UITapGestureRecognizer(target: self, action: #selector(handleVersionViewTap))
|
let versionTap = UITapGestureRecognizer(target: self, action: #selector(handleVersionViewTap))
|
||||||
rootView.versionView.addGestureRecognizer(versionTap)
|
rootView.versionView.addGestureRecognizer(versionTap)
|
||||||
|
|
||||||
|
let avatarTap = UITapGestureRecognizer(target: self, action: #selector(handleAvatarTap))
|
||||||
|
rootView.avatarView.addGestureRecognizer(avatarTap)
|
||||||
|
let nickTap = UITapGestureRecognizer(target: self, action: #selector(handleNickNameTap))
|
||||||
|
rootView.nickNameView.addGestureRecognizer(nickTap)
|
||||||
|
let genderTap = UITapGestureRecognizer(target: self, action: #selector(handleGenderTap))
|
||||||
|
rootView.genderView.addGestureRecognizer(genderTap)
|
||||||
|
|
||||||
|
NotificationCenter.default.rx.notification(.RefreshUserConfigNotification)
|
||||||
|
.observe(on: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] _ in
|
||||||
|
self?.rootView.refreshProfile()
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleAvatarTap() {
|
||||||
|
let vc = AvatarIconListVC(iconIndex: AppContextManager.shared.head_pic)
|
||||||
|
vc.onSelectIcon = { [weak self] headPic in
|
||||||
|
self?.requestSetHeadPic(headPic)
|
||||||
|
}
|
||||||
|
navigationController?.pushViewController(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleNickNameTap() {
|
||||||
|
let vc = TextInputViewController(
|
||||||
|
title: "昵称",
|
||||||
|
maxLength: 10,
|
||||||
|
initialText: AppContextManager.shared.name
|
||||||
|
) { [weak self] text in
|
||||||
|
self?.requestSetNickName(text)
|
||||||
|
}
|
||||||
|
present(vc, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func handleGenderTap() {
|
||||||
|
let pop = GenderSelectPopVC(currentSex: AppContextManager.shared.sex) { [weak self] sex in
|
||||||
|
self?.requestSetGender(sex)
|
||||||
|
}
|
||||||
|
present(pop, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestSetHeadPic(_ headPic: String) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
UserService.setHeadPic(headPic: headPic).subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.show(text: "更换成功")
|
||||||
|
if var account = AppContextManager.shared.account {
|
||||||
|
account.head_pic = headPic
|
||||||
|
account.avater = HeadPic.isRemote(headPic) ? headPic : ""
|
||||||
|
AppContextManager.shared.saveAccount(account)
|
||||||
|
}
|
||||||
|
self?.rootView.refreshProfile()
|
||||||
|
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.show(text: error.localizedDescription)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestSetNickName(_ name: String) {
|
||||||
|
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmed.isEmpty else { return }
|
||||||
|
DLToast.showLoading()
|
||||||
|
UserService.setNickName(nick: trimmed).subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.show(text: "修改成功")
|
||||||
|
if var account = AppContextManager.shared.account {
|
||||||
|
account.name = trimmed
|
||||||
|
AppContextManager.shared.saveAccount(account)
|
||||||
|
}
|
||||||
|
self?.rootView.refreshProfile()
|
||||||
|
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.show(text: error.localizedDescription)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestSetGender(_ sex: Int) {
|
||||||
|
DLToast.showLoading()
|
||||||
|
UserService.setGender(sex: sex).subscribe(onNext: { [weak self] _ in
|
||||||
|
DLToast.show(text: "修改成功")
|
||||||
|
if var account = AppContextManager.shared.account {
|
||||||
|
account.sex = sex
|
||||||
|
AppContextManager.shared.saveAccount(account)
|
||||||
|
}
|
||||||
|
self?.rootView.refreshProfile()
|
||||||
|
NotificationCenter.default.post(name: .RefreshUserConfigNotification, object: nil)
|
||||||
|
}, onError: { error in
|
||||||
|
DLToast.dismiss()
|
||||||
|
DLToast.show(text: error.localizedDescription)
|
||||||
|
}).disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - API
|
// MARK: - API
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ class AccountView: UIView {
|
||||||
|
|
||||||
addSubview(infoView)
|
addSubview(infoView)
|
||||||
infoView.addSubview(infoStackView)
|
infoView.addSubview(infoStackView)
|
||||||
|
addSubview(profileView)
|
||||||
|
profileView.addSubview(profileStackView)
|
||||||
addSubview(otherInfoView)
|
addSubview(otherInfoView)
|
||||||
otherInfoView.addSubview(otherInfoStackView)
|
otherInfoView.addSubview(otherInfoStackView)
|
||||||
|
|
||||||
|
|
@ -55,9 +57,18 @@ class AccountView: UIView {
|
||||||
|
|
||||||
userIdView.layoutChain.height(52)
|
userIdView.layoutChain.height(52)
|
||||||
phoneView.layoutChain.height(52)
|
phoneView.layoutChain.height(52)
|
||||||
|
|
||||||
|
profileView.layoutChain
|
||||||
|
.topToBottomOfView(infoView, offset: 18)
|
||||||
|
.edgesHorzontal(16)
|
||||||
|
|
||||||
|
profileStackView.layoutChain.edges()
|
||||||
|
avatarView.layoutChain.height(52)
|
||||||
|
nickNameView.layoutChain.height(52)
|
||||||
|
genderView.layoutChain.height(52)
|
||||||
|
|
||||||
otherInfoView.layoutChain
|
otherInfoView.layoutChain
|
||||||
.topToBottomOfView(infoView, offset: 18)
|
.topToBottomOfView(profileView, offset: 18)
|
||||||
.edgesHorzontal(16)
|
.edgesHorzontal(16)
|
||||||
|
|
||||||
otherInfoStackView.layoutChain.edges()
|
otherInfoStackView.layoutChain.edges()
|
||||||
|
|
@ -193,6 +204,111 @@ class AccountView: UIView {
|
||||||
lab.textColor = UIColor(hexStr: "#1A1A1A")
|
lab.textColor = UIColor(hexStr: "#1A1A1A")
|
||||||
return lab
|
return lab
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
lazy var profileView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
view.layer.shadowColor = UIColor(hexStr: "#0F2846", alpha: 0.1).cgColor
|
||||||
|
view.layer.shadowOffset = CGSize(width: 0, height: 0)
|
||||||
|
view.layer.shadowOpacity = 1
|
||||||
|
view.layer.shadowRadius = 10
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var profileStackView: UIStackView = {
|
||||||
|
let view = UIStackView(arrangedSubviews: [avatarView, nickNameView, genderView])
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .fill
|
||||||
|
view.distribution = .fill
|
||||||
|
view.spacing = 0
|
||||||
|
view.backgroundColor = .white
|
||||||
|
view.cornerRadius = 10
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var avatarView: UIView = {
|
||||||
|
makeProfileRow(title: "头像", accessory: avatarImgView, accessorySize: CGSize(width: 32, height: 32), showsLine: true)
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var avatarImgView: UIImageView = {
|
||||||
|
let iv = UIImageView()
|
||||||
|
iv.contentMode = .scaleAspectFill
|
||||||
|
iv.clipsToBounds = true
|
||||||
|
iv.cornerRadius = 16
|
||||||
|
iv.backgroundColor = UIColor(hexStr: "#E0E0E0")
|
||||||
|
return iv
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var nickNameView: UIView = {
|
||||||
|
makeProfileRow(title: "昵称", accessory: nickNameLab, showsLine: true)
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var nickNameLab: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
lab.textColor = UIColor(hexStr: "#1A1A1A")
|
||||||
|
lab.textAlignment = .right
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var genderView: UIView = {
|
||||||
|
makeProfileRow(title: "性别", accessory: genderLab, showsLine: false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var genderLab: UILabel = {
|
||||||
|
let lab = UILabel()
|
||||||
|
lab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
lab.textColor = UIColor(hexStr: "#1A1A1A")
|
||||||
|
lab.textAlignment = .right
|
||||||
|
return lab
|
||||||
|
}()
|
||||||
|
|
||||||
|
private func makeProfileRow(title: String, accessory: UIView, accessorySize: CGSize? = nil, showsLine: Bool) -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
view.backgroundColor = .clear
|
||||||
|
|
||||||
|
let titleLab = UILabel()
|
||||||
|
titleLab.text = title
|
||||||
|
titleLab.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
titleLab.textColor = UIColor(hexStr: "#1A1A1A")
|
||||||
|
view.addSubview(titleLab)
|
||||||
|
titleLab.layoutChain
|
||||||
|
.left(15)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
let arrow = UIImageView(image: UIImage(named: "Group/arrow"))
|
||||||
|
view.addSubview(arrow)
|
||||||
|
arrow.layoutChain
|
||||||
|
.right(15)
|
||||||
|
.width(14)
|
||||||
|
.height(14)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
view.addSubview(accessory)
|
||||||
|
if let accessorySize {
|
||||||
|
accessory.layoutChain
|
||||||
|
.rightToLeftOfView(arrow, offset: -8)
|
||||||
|
.centerY()
|
||||||
|
.width(accessorySize.width)
|
||||||
|
.height(accessorySize.height)
|
||||||
|
} else {
|
||||||
|
accessory.layoutChain
|
||||||
|
.rightToLeftOfView(arrow, offset: -8)
|
||||||
|
.centerY()
|
||||||
|
.leftToRightOfView(titleLab, offset: 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showsLine {
|
||||||
|
let line = UIView()
|
||||||
|
line.backgroundColor = ThemeManager.shared.color.lineColor
|
||||||
|
view.addSubview(line)
|
||||||
|
line.layoutChain
|
||||||
|
.bottom()
|
||||||
|
.height(0.5)
|
||||||
|
.edgesHorzontal(15)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
// 其它
|
// 其它
|
||||||
lazy var otherInfoView: UIView = {
|
lazy var otherInfoView: UIView = {
|
||||||
|
|
@ -306,11 +422,28 @@ class AccountView: UIView {
|
||||||
return btn
|
return btn
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
func refreshProfile() {
|
||||||
|
avatarImgView.setHeadPic(AppContextManager.shared.head_pic)
|
||||||
|
nickNameLab.text = AppContextManager.shared.name
|
||||||
|
genderLab.text = Self.genderText(AppContextManager.shared.sex)
|
||||||
|
phoneLab.text = AppContextManager.shared.userPhone
|
||||||
|
}
|
||||||
|
|
||||||
|
static func genderText(_ sex: Int) -> String {
|
||||||
|
switch sex {
|
||||||
|
case 1: return "男"
|
||||||
|
case 2: return "女"
|
||||||
|
case 0: return "保密"
|
||||||
|
default: return "未设置"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
setupUI()
|
setupUI()
|
||||||
setupRx()
|
setupRx()
|
||||||
|
refreshProfile()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
|
|
||||||
|
|
@ -463,6 +463,9 @@ final class MinePhotoWallView: UIView {
|
||||||
daysRow.isHidden = true
|
daysRow.isHidden = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
let selfColor = UIColor(hexStr: "#16B3FF")
|
||||||
|
let otherColor = UIColor(hexStr: "#293445")
|
||||||
|
let currentUserId = AppContextManager.shared.userId
|
||||||
if isPairMode {
|
if isPairMode {
|
||||||
titleLab.isHidden = true
|
titleLab.isHidden = true
|
||||||
pairHeartImg.isHidden = members.count < 2
|
pairHeartImg.isHidden = members.count < 2
|
||||||
|
|
@ -474,7 +477,9 @@ final class MinePhotoWallView: UIView {
|
||||||
}
|
}
|
||||||
pairLeftLab.text = names.first ?? " "
|
pairLeftLab.text = names.first ?? " "
|
||||||
pairRightLab.text = names.count >= 2 ? names[1] : ""
|
pairRightLab.text = names.count >= 2 ? names[1] : ""
|
||||||
let selfMember = members.first { $0.user_id == AppContextManager.shared.userId } ?? model
|
pairLeftLab.textColor = otherColor
|
||||||
|
pairRightLab.textColor = otherColor
|
||||||
|
let selfMember = members.first { $0.user_id == currentUserId } ?? model
|
||||||
daysLab.text = selfMember.joinDaysDisplay
|
daysLab.text = selfMember.joinDaysDisplay
|
||||||
} else {
|
} else {
|
||||||
titleLab.isHidden = false
|
titleLab.isHidden = false
|
||||||
|
|
@ -483,6 +488,7 @@ final class MinePhotoWallView: UIView {
|
||||||
pairRightLab.isHidden = true
|
pairRightLab.isHidden = true
|
||||||
let nickname = model.showName.trimmingCharacters(in: .whitespacesAndNewlines)
|
let nickname = model.showName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname
|
titleLab.text = nickname.isEmpty ? "未设置昵称" : nickname
|
||||||
|
titleLab.textColor = model.user_id == currentUserId ? selfColor : otherColor
|
||||||
daysLab.text = model.joinDaysDisplay
|
daysLab.text = model.joinDaysDisplay
|
||||||
}
|
}
|
||||||
daysRow.isHidden = false
|
daysRow.isHidden = false
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ final class MineView: UIView {
|
||||||
|
|
||||||
private let vipCardHeight: CGFloat = 90
|
private let vipCardHeight: CGFloat = 90
|
||||||
private let settingsRowHeight: CGFloat = 52
|
private let settingsRowHeight: CGFloat = 52
|
||||||
private let quickItemTitles = ["权限检测", "数据安全", "状态设置", "账户设置"]
|
private let quickItemTitles = ["权限检测", "隐私设置", "状态设置", "账户设置"]
|
||||||
|
|
||||||
var onQuickTap: ((String) -> Void)?
|
var onQuickTap: ((String) -> Void)?
|
||||||
|
|
||||||
|
|
@ -257,7 +257,7 @@ final class MineView: UIView {
|
||||||
private func setupQuickActions() {
|
private func setupQuickActions() {
|
||||||
let icons = [
|
let icons = [
|
||||||
"Mine/quick_permission",
|
"Mine/quick_permission",
|
||||||
"Mine/quick_data_security",
|
"PrivacySettings/privacy_entry",
|
||||||
"Mine/quick_status",
|
"Mine/quick_status",
|
||||||
"Mine/quick_account"
|
"Mine/quick_account"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -96,17 +96,21 @@ final class MineViewController: BaseViewController {
|
||||||
if !cached.isEmpty {
|
if !cached.isEmpty {
|
||||||
groupKey = cached
|
groupKey = cached
|
||||||
loadPhotoWallMembers(groupKey: cached)
|
loadPhotoWallMembers(groupKey: cached)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
||||||
guard let self = self, let model = response.model else { return }
|
guard let self = self, let model = response.model else { return }
|
||||||
self.groupModel = model
|
self.groupModel = model
|
||||||
self.groupKey = model.default_group_key
|
let key = model.default_group_key
|
||||||
guard !model.default_group_key.isEmpty else {
|
guard !key.isEmpty else {
|
||||||
self.reloadPhotoWall(members: [], groupKey: "")
|
if self.groupKey.isEmpty {
|
||||||
|
self.reloadPhotoWall(members: [], groupKey: "")
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.loadPhotoWallMembers(groupKey: model.default_group_key)
|
if self.groupKey != key {
|
||||||
|
self.groupKey = key
|
||||||
|
self.loadPhotoWallMembers(groupKey: key)
|
||||||
|
}
|
||||||
}).disposed(by: disposeBag)
|
}).disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,7 +158,8 @@ final class MineViewController: BaseViewController {
|
||||||
guard !members.isEmpty else { return }
|
guard !members.isEmpty else { return }
|
||||||
var userInfo: [String: Any] = [
|
var userInfo: [String: Any] = [
|
||||||
"members": members,
|
"members": members,
|
||||||
"userId": selectedUserId
|
"userId": selectedUserId,
|
||||||
|
"groupKey": groupKey
|
||||||
]
|
]
|
||||||
if let group = groupModel {
|
if let group = groupModel {
|
||||||
userInfo["groupJson"] = group.toJSON()
|
userInfo["groupJson"] = group.toJSON()
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ class MineViewModel {
|
||||||
/// 快捷入口
|
/// 快捷入口
|
||||||
let quickItems: [MineMenuItem] = [
|
let quickItems: [MineMenuItem] = [
|
||||||
MineMenuItem(icon: "Mine/quick_permission", title: "权限检测"),
|
MineMenuItem(icon: "Mine/quick_permission", title: "权限检测"),
|
||||||
MineMenuItem(icon: "Mine/quick_data_security", title: "数据安全"),
|
MineMenuItem(icon: "PrivacySettings/privacy_entry", title: "隐私设置"),
|
||||||
MineMenuItem(icon: "Mine/quick_status", title: "状态设置"),
|
MineMenuItem(icon: "Mine/quick_status", title: "状态设置"),
|
||||||
MineMenuItem(icon: "Mine/quick_account", title: "账户设置")
|
MineMenuItem(icon: "Mine/quick_account", title: "账户设置")
|
||||||
]
|
]
|
||||||
|
|
@ -52,14 +52,14 @@ class MineViewModel {
|
||||||
AppRouter.push(Route.account)
|
AppRouter.push(Route.account)
|
||||||
case "紧急联系人":
|
case "紧急联系人":
|
||||||
AppRouter.push(Route.emergencyContact)
|
AppRouter.push(Route.emergencyContact)
|
||||||
case "隐私与协议", "隐私设置":
|
case "隐私与协议":
|
||||||
AppRouter.push(Route.privacyPolicy)
|
AppRouter.push(Route.privacyPolicy)
|
||||||
|
case "隐私设置":
|
||||||
|
AppRouter.push(Route.privacySettings)
|
||||||
case "在线客服":
|
case "在线客服":
|
||||||
requestWechatService()
|
requestWechatService()
|
||||||
case "权限检测":
|
case "权限检测":
|
||||||
AppRouter.push(Route.checkPermission)
|
AppRouter.push(Route.checkPermission)
|
||||||
case "数据安全":
|
|
||||||
DLToast.show(text: "敬请期待")
|
|
||||||
case "状态设置":
|
case "状态设置":
|
||||||
AppRouter.push(Route.moodStatus)
|
AppRouter.push(Route.moodStatus)
|
||||||
case "关于我们":
|
case "关于我们":
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,663 @@
|
||||||
|
//
|
||||||
|
// PrivacySettingsViewController.swift
|
||||||
|
// QuickLocation
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import RxSwift
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
enum PrivacySettingItem: CaseIterable {
|
||||||
|
case phoneReport
|
||||||
|
case groupLock
|
||||||
|
case pigeonMessage
|
||||||
|
|
||||||
|
var title: String {
|
||||||
|
switch self {
|
||||||
|
case .phoneReport:
|
||||||
|
return "手机报告"
|
||||||
|
case .groupLock:
|
||||||
|
return "一键锁机"
|
||||||
|
case .pigeonMessage:
|
||||||
|
return "飞鸽传书"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageName: String {
|
||||||
|
switch self {
|
||||||
|
case .phoneReport:
|
||||||
|
return "PrivacySettings/phone_report"
|
||||||
|
case .groupLock:
|
||||||
|
return "PrivacySettings/group_lock"
|
||||||
|
case .pigeonMessage:
|
||||||
|
return "PrivacySettings/pigeon_message"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiKey: String {
|
||||||
|
switch self {
|
||||||
|
case .phoneReport:
|
||||||
|
return "phone_report"
|
||||||
|
case .groupLock:
|
||||||
|
return "app_lock"
|
||||||
|
case .pigeonMessage:
|
||||||
|
return "pigeon_mail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PrivacySettingsViewController: BaseViewController {
|
||||||
|
|
||||||
|
private var rootView: PrivacySettingsView!
|
||||||
|
private var groups: [GroupInfoModel] = []
|
||||||
|
private var isSubmitting = false
|
||||||
|
|
||||||
|
override func loadView() {
|
||||||
|
rootView = PrivacySettingsView(frame: UIScreen.main.bounds)
|
||||||
|
view = rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
bind()
|
||||||
|
requestGroups()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bind() {
|
||||||
|
rootView.onGroupChanged = { [weak self] in
|
||||||
|
guard let self else { return }
|
||||||
|
self.loadPrivilege(for: self.rootView.selectedGroupKey)
|
||||||
|
}
|
||||||
|
rootView.onToggle = { [weak self] item, isOn in
|
||||||
|
self?.changePrivilege(item: item, isOn: isOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func requestGroups() {
|
||||||
|
rootView.setSettingsEnabled(false)
|
||||||
|
GroupService.groupInfo()
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
guard let model = response.model else {
|
||||||
|
self.groups = []
|
||||||
|
self.rootView.configure(groups: [], selectedGroupKey: "")
|
||||||
|
DLToast.showError(text: response.message ?? "获取圈子信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.groups = model.groups
|
||||||
|
self.rootView.configure(
|
||||||
|
groups: model.groups,
|
||||||
|
selectedGroupKey: model.default_group_key
|
||||||
|
)
|
||||||
|
self.loadPrivilege(for: self.rootView.selectedGroupKey)
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
self?.groups = []
|
||||||
|
self?.rootView.configure(groups: [], selectedGroupKey: "")
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applySelectedPrivilege() {
|
||||||
|
let privilege = groups.first(where: { $0.group_key == rootView.selectedGroupKey })?.privilege
|
||||||
|
?? GroupPrivilege()
|
||||||
|
rootView.apply(privilege)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadPrivilege(for groupKey: String) {
|
||||||
|
guard !groupKey.isEmpty else {
|
||||||
|
applySelectedPrivilege()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rootView.setSettingsEnabled(false)
|
||||||
|
GroupService.groupUsers(groupKey: groupKey)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
|
||||||
|
guard response.isValid(for: groupKey),
|
||||||
|
self.rootView.selectedGroupKey == groupKey else {
|
||||||
|
self.applySelectedPrivilege()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let mine = response.list.first { $0.user_id == AppContextManager.shared.userId }
|
||||||
|
if let index = self.groups.firstIndex(where: { $0.group_key == groupKey }) {
|
||||||
|
self.groups[index].privilege = mine?.extra.privilege ?? GroupPrivilege()
|
||||||
|
}
|
||||||
|
self.applySelectedPrivilege()
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
self?.rootView.setSettingsEnabled(!(self?.groups.isEmpty ?? true))
|
||||||
|
self?.applySelectedPrivilege()
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func changePrivilege(item: PrivacySettingItem, isOn: Bool) {
|
||||||
|
let groupKey = rootView.selectedGroupKey
|
||||||
|
guard !groupKey.isEmpty,
|
||||||
|
let index = groups.firstIndex(where: { $0.group_key == groupKey }) else {
|
||||||
|
applySelectedPrivilege()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !isSubmitting else {
|
||||||
|
applySelectedPrivilege()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item == .groupLock, !isOn {
|
||||||
|
interceptTurnOffAppLock { [weak self] blocked in
|
||||||
|
guard let self else { return }
|
||||||
|
if blocked {
|
||||||
|
self.applySelectedPrivilege()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPrivilege(item: item, isOn: isOn, groupKey: groupKey, index: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func interceptTurnOffAppLock(completion: @escaping (Bool) -> Void) {
|
||||||
|
if let record = lockRecordForPopup(from: PhoneLockSession.currentLocks) {
|
||||||
|
LockedAppPopView.show(record)
|
||||||
|
completion(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let userId = AppContextManager.shared.userId.trimmed
|
||||||
|
let revision = AppUnlockCoordinator.shared.currentLockStateRevision()
|
||||||
|
isSubmitting = true
|
||||||
|
rootView.setSettingsEnabled(false)
|
||||||
|
UserService.phoneLocked(os: "ios")
|
||||||
|
.observe(on: MainScheduler.instance)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isSubmitting = false
|
||||||
|
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
|
||||||
|
let locks = AppUnlockCoordinator.shared.locksForRestoration(
|
||||||
|
response.model?.locks ?? [],
|
||||||
|
startedAt: revision,
|
||||||
|
userId: userId
|
||||||
|
) ?? []
|
||||||
|
if !locks.isEmpty {
|
||||||
|
PhoneLockSession.currentLocks = locks
|
||||||
|
}
|
||||||
|
if let record = self.lockRecordForPopup(from: locks) {
|
||||||
|
LockedAppPopView.show(record)
|
||||||
|
completion(true)
|
||||||
|
} else {
|
||||||
|
completion(false)
|
||||||
|
}
|
||||||
|
}, onError: { [weak self] _ in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isSubmitting = false
|
||||||
|
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
|
||||||
|
completion(false)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func lockRecordForPopup(from locks: [PhoneLockRecord]) -> PhoneLockRecord? {
|
||||||
|
let groupKey = rootView.selectedGroupKey
|
||||||
|
return locks.first { $0.groupKey == groupKey } ?? locks.first
|
||||||
|
}
|
||||||
|
|
||||||
|
private func submitPrivilege(item: PrivacySettingItem, isOn: Bool, groupKey: String, index: Int) {
|
||||||
|
let previous = groups[index].privilege
|
||||||
|
var updated = previous
|
||||||
|
switch item {
|
||||||
|
case .phoneReport:
|
||||||
|
updated.phone_report = isOn
|
||||||
|
case .groupLock:
|
||||||
|
updated.app_lock = isOn
|
||||||
|
case .pigeonMessage:
|
||||||
|
updated.pigeon_mail = isOn
|
||||||
|
}
|
||||||
|
groups[index].privilege = updated
|
||||||
|
rootView.apply(updated)
|
||||||
|
|
||||||
|
isSubmitting = true
|
||||||
|
rootView.setSettingsEnabled(false)
|
||||||
|
GroupService.operate(
|
||||||
|
opType: "changeprivilege",
|
||||||
|
requestData: [
|
||||||
|
"group_key": groupKey,
|
||||||
|
"privilege_code": item.apiKey,
|
||||||
|
"enable": isOn
|
||||||
|
]
|
||||||
|
)
|
||||||
|
.subscribe(onNext: { [weak self] response in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isSubmitting = false
|
||||||
|
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
|
||||||
|
guard response.code == "0" else {
|
||||||
|
self.revertPrivilege(groupKey: groupKey, previous: previous)
|
||||||
|
DLToast.showError(text: response.message ?? "设置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}, onError: { [weak self] error in
|
||||||
|
guard let self else { return }
|
||||||
|
self.isSubmitting = false
|
||||||
|
self.rootView.setSettingsEnabled(!self.groups.isEmpty)
|
||||||
|
self.revertPrivilege(groupKey: groupKey, previous: previous)
|
||||||
|
DLToast.showError(text: error.localizedDescription)
|
||||||
|
})
|
||||||
|
.disposed(by: disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func revertPrivilege(groupKey: String, previous: GroupPrivilege) {
|
||||||
|
if let index = groups.firstIndex(where: { $0.group_key == groupKey }) {
|
||||||
|
groups[index].privilege = previous
|
||||||
|
}
|
||||||
|
applySelectedPrivilege()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PrivacySettingsView: UIView {
|
||||||
|
|
||||||
|
var onGroupChanged: (() -> Void)?
|
||||||
|
var onToggle: ((PrivacySettingItem, Bool) -> Void)?
|
||||||
|
var selectedGroupKey: String { groupSelectorView.selectedGroupKey }
|
||||||
|
|
||||||
|
private let navBackgroundView = UIImageView(image: UIImage(named: "Common/navBar_bg_2"))
|
||||||
|
private let navView = BaseNavigationView(title: "隐私设置")
|
||||||
|
private let scrollView = UIScrollView()
|
||||||
|
private let contentView = UIView()
|
||||||
|
private let groupSelectorView = PrivacyGroupSelectorView()
|
||||||
|
private let phoneReportRow = PrivacySettingRow(item: .phoneReport)
|
||||||
|
private let groupLockRow = PrivacySettingRow(item: .groupLock)
|
||||||
|
private let pigeonMessageRow = PrivacySettingRow(item: .pigeonMessage)
|
||||||
|
private let footerLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
|
||||||
|
groupSelectorView.configure(groups: groups, selectedGroupKey: selectedGroupKey)
|
||||||
|
setSettingsEnabled(!groups.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apply(_ privilege: GroupPrivilege) {
|
||||||
|
phoneReportRow.setOn(privilege.phone_report)
|
||||||
|
groupLockRow.setOn(privilege.app_lock)
|
||||||
|
pigeonMessageRow.setOn(privilege.pigeon_mail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSettingsEnabled(_ enabled: Bool) {
|
||||||
|
[phoneReportRow, groupLockRow, pigeonMessageRow].forEach {
|
||||||
|
$0.setSwitchEnabled(enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupUI() {
|
||||||
|
backgroundColor = UIColor(hexStr: "#FAFAFA")
|
||||||
|
|
||||||
|
navBackgroundView.contentMode = .scaleAspectFill
|
||||||
|
navBackgroundView.clipsToBounds = true
|
||||||
|
addSubview(navBackgroundView)
|
||||||
|
addSubview(navView)
|
||||||
|
|
||||||
|
scrollView.showsVerticalScrollIndicator = false
|
||||||
|
scrollView.alwaysBounceVertical = false
|
||||||
|
addSubview(scrollView)
|
||||||
|
scrollView.addSubview(contentView)
|
||||||
|
|
||||||
|
footerLabel.text = "ⓘ 仅对当前选中圈子生效哟~"
|
||||||
|
footerLabel.textColor = UIColor(hexStr: "#536177")
|
||||||
|
footerLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
footerLabel.textAlignment = .center
|
||||||
|
addSubview(footerLabel)
|
||||||
|
|
||||||
|
contentView.addSubview(groupSelectorView)
|
||||||
|
|
||||||
|
let shareHeader = makeSectionHeader(title: "圈子中可以共享的数据")
|
||||||
|
contentView.addSubview(shareHeader)
|
||||||
|
contentView.addSubview(phoneReportRow)
|
||||||
|
|
||||||
|
let ownerHeader = makeSectionHeader(title: "圈主可以对你使用的功能")
|
||||||
|
contentView.addSubview(ownerHeader)
|
||||||
|
contentView.addSubview(groupLockRow)
|
||||||
|
contentView.addSubview(pigeonMessageRow)
|
||||||
|
|
||||||
|
navBackgroundView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
navView.layoutChain
|
||||||
|
.edges(excludingEdge: .bottom)
|
||||||
|
.height(kNaviHeight)
|
||||||
|
|
||||||
|
footerLabel.layoutChain
|
||||||
|
.left(20)
|
||||||
|
.right(20)
|
||||||
|
.bottom(kSafeBottomMargin + 12)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
scrollView.layoutChain
|
||||||
|
.topToBottomOfView(navView)
|
||||||
|
.left()
|
||||||
|
.right()
|
||||||
|
.bottomToTopOfView(footerLabel, offset: -8)
|
||||||
|
|
||||||
|
contentView.layoutChain
|
||||||
|
.edges()
|
||||||
|
.widthToView(scrollView)
|
||||||
|
|
||||||
|
groupSelectorView.layoutChain
|
||||||
|
.top(12)
|
||||||
|
.left(15)
|
||||||
|
.right(15)
|
||||||
|
.height(100)
|
||||||
|
|
||||||
|
shareHeader.layoutChain
|
||||||
|
.topToBottomOfView(groupSelectorView, offset: 20)
|
||||||
|
.left(17)
|
||||||
|
.right(15)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
phoneReportRow.layoutChain
|
||||||
|
.topToBottomOfView(shareHeader, offset: 12)
|
||||||
|
.left(15)
|
||||||
|
.right(15)
|
||||||
|
.height(64)
|
||||||
|
|
||||||
|
ownerHeader.layoutChain
|
||||||
|
.topToBottomOfView(phoneReportRow, offset: 18)
|
||||||
|
.left(17)
|
||||||
|
.right(15)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
groupLockRow.layoutChain
|
||||||
|
.topToBottomOfView(ownerHeader, offset: 12)
|
||||||
|
.left(15)
|
||||||
|
.right(15)
|
||||||
|
.height(64)
|
||||||
|
|
||||||
|
pigeonMessageRow.layoutChain
|
||||||
|
.topToBottomOfView(groupLockRow, offset: 12)
|
||||||
|
.left(15)
|
||||||
|
.right(15)
|
||||||
|
.height(64)
|
||||||
|
.bottom(20)
|
||||||
|
|
||||||
|
groupSelectorView.onSelectionChanged = { [weak self] in
|
||||||
|
self?.onGroupChanged?()
|
||||||
|
}
|
||||||
|
phoneReportRow.onToggle = { [weak self] isOn in
|
||||||
|
self?.onToggle?(.phoneReport, isOn)
|
||||||
|
}
|
||||||
|
groupLockRow.onToggle = { [weak self] isOn in
|
||||||
|
self?.onToggle?(.groupLock, isOn)
|
||||||
|
}
|
||||||
|
pigeonMessageRow.onToggle = { [weak self] isOn in
|
||||||
|
self?.onToggle?(.pigeonMessage, isOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeSectionHeader(title: String) -> UIView {
|
||||||
|
let view = UIView()
|
||||||
|
|
||||||
|
let sparkleLabel = UILabel()
|
||||||
|
sparkleLabel.text = "✦"
|
||||||
|
sparkleLabel.textColor = UIColor(hexStr: "#08ADEF")
|
||||||
|
sparkleLabel.font = .systemFont(ofSize: 18, weight: .bold)
|
||||||
|
sparkleLabel.textAlignment = .center
|
||||||
|
view.addSubview(sparkleLabel)
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#28364B")
|
||||||
|
titleLabel.font = .systemFont(ofSize: 16, weight: .regular)
|
||||||
|
view.addSubview(titleLabel)
|
||||||
|
|
||||||
|
sparkleLabel.layoutChain
|
||||||
|
.left()
|
||||||
|
.centerY()
|
||||||
|
.width(18)
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
titleLabel.layoutChain
|
||||||
|
.leftToRightOfView(sparkleLabel, offset: 3)
|
||||||
|
.right()
|
||||||
|
.centerY()
|
||||||
|
.height(24)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PrivacyGroupSelectorView: UIView,
|
||||||
|
UICollectionViewDataSource,
|
||||||
|
UICollectionViewDelegateFlowLayout {
|
||||||
|
|
||||||
|
var onSelectionChanged: (() -> Void)?
|
||||||
|
private(set) var selectedGroupKey = ""
|
||||||
|
|
||||||
|
private let layout = UICollectionViewFlowLayout()
|
||||||
|
private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
|
||||||
|
private let emptyLabel = UILabel()
|
||||||
|
private var groups: [GroupInfoModel] = []
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
backgroundColor = .white
|
||||||
|
layer.cornerRadius = 20
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
layout.scrollDirection = .horizontal
|
||||||
|
layout.minimumLineSpacing = 4
|
||||||
|
layout.minimumInteritemSpacing = 4
|
||||||
|
layout.sectionInset = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6)
|
||||||
|
|
||||||
|
collectionView.backgroundColor = .clear
|
||||||
|
collectionView.showsHorizontalScrollIndicator = false
|
||||||
|
collectionView.alwaysBounceHorizontal = true
|
||||||
|
collectionView.dataSource = self
|
||||||
|
collectionView.delegate = self
|
||||||
|
collectionView.register(PrivacyGroupCell.self)
|
||||||
|
addSubview(collectionView)
|
||||||
|
collectionView.layoutChain.edges()
|
||||||
|
|
||||||
|
emptyLabel.text = "暂无圈子"
|
||||||
|
emptyLabel.textColor = UIColor(hexStr: "#9BA3AF")
|
||||||
|
emptyLabel.font = .systemFont(ofSize: 15, weight: .regular)
|
||||||
|
emptyLabel.textAlignment = .center
|
||||||
|
emptyLabel.isHidden = true
|
||||||
|
addSubview(emptyLabel)
|
||||||
|
emptyLabel.layoutChain.edges()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(groups: [GroupInfoModel], selectedGroupKey: String) {
|
||||||
|
self.groups = groups
|
||||||
|
if groups.contains(where: { $0.group_key == selectedGroupKey }) {
|
||||||
|
self.selectedGroupKey = selectedGroupKey
|
||||||
|
} else {
|
||||||
|
self.selectedGroupKey = groups.first?.group_key ?? ""
|
||||||
|
}
|
||||||
|
emptyLabel.isHidden = !groups.isEmpty
|
||||||
|
collectionView.isHidden = groups.isEmpty
|
||||||
|
collectionView.reloadData()
|
||||||
|
|
||||||
|
guard let index = groups.firstIndex(where: { $0.group_key == self.selectedGroupKey }) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
collectionView.layoutIfNeeded()
|
||||||
|
collectionView.scrollToItem(
|
||||||
|
at: IndexPath(item: index, section: 0),
|
||||||
|
at: .centeredHorizontally,
|
||||||
|
animated: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
|
groups.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView,
|
||||||
|
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
|
let cell: PrivacyGroupCell = collectionView.dequeueReusableCell(for: indexPath)
|
||||||
|
let group = groups[indexPath.item]
|
||||||
|
cell.configure(group: group, selected: group.group_key == selectedGroupKey)
|
||||||
|
return cell
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
|
let groupKey = groups[indexPath.item].group_key
|
||||||
|
guard selectedGroupKey != groupKey else { return }
|
||||||
|
selectedGroupKey = groupKey
|
||||||
|
collectionView.reloadData()
|
||||||
|
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
|
||||||
|
onSelectionChanged?()
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView,
|
||||||
|
layout collectionViewLayout: UICollectionViewLayout,
|
||||||
|
sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||||
|
CGSize(width: 76, height: 92)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PrivacyGroupCell: UICollectionViewCell {
|
||||||
|
|
||||||
|
private let selectionView = UIView()
|
||||||
|
private let selectionPointer = UIView()
|
||||||
|
private let iconView = UIImageView()
|
||||||
|
private let nameLabel = UILabel()
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
selectionView.backgroundColor = UIColor(hexStr: "#C9F4FF")
|
||||||
|
selectionView.layer.cornerRadius = 13
|
||||||
|
selectionView.isHidden = true
|
||||||
|
contentView.addSubview(selectionView)
|
||||||
|
|
||||||
|
selectionPointer.backgroundColor = UIColor(hexStr: "#C9F4FF")
|
||||||
|
selectionPointer.transform = CGAffineTransform(rotationAngle: .pi / 4)
|
||||||
|
selectionPointer.isHidden = true
|
||||||
|
contentView.addSubview(selectionPointer)
|
||||||
|
|
||||||
|
iconView.contentMode = .scaleAspectFill
|
||||||
|
iconView.clipsToBounds = true
|
||||||
|
iconView.layer.cornerRadius = 25
|
||||||
|
contentView.addSubview(iconView)
|
||||||
|
|
||||||
|
nameLabel.textColor = UIColor(hexStr: "#26364B")
|
||||||
|
nameLabel.font = .systemFont(ofSize: 14, weight: .regular)
|
||||||
|
nameLabel.textAlignment = .center
|
||||||
|
nameLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
contentView.addSubview(nameLabel)
|
||||||
|
|
||||||
|
selectionView.layoutChain
|
||||||
|
.top(1)
|
||||||
|
.left(2)
|
||||||
|
.right(2)
|
||||||
|
.bottom(4)
|
||||||
|
|
||||||
|
selectionPointer.layoutChain
|
||||||
|
.centerX()
|
||||||
|
.bottom(1)
|
||||||
|
.width(12)
|
||||||
|
.height(12)
|
||||||
|
|
||||||
|
iconView.layoutChain
|
||||||
|
.top(8)
|
||||||
|
.centerX()
|
||||||
|
.width(50)
|
||||||
|
.height(50)
|
||||||
|
|
||||||
|
nameLabel.layoutChain
|
||||||
|
.topToBottomOfView(iconView, offset: 5)
|
||||||
|
.left(5)
|
||||||
|
.right(5)
|
||||||
|
.height(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
iconView.kf.cancelDownloadTask()
|
||||||
|
iconView.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(group: GroupInfoModel, selected: Bool) {
|
||||||
|
nameLabel.text = group.name
|
||||||
|
iconView.setGroupIcon(group)
|
||||||
|
selectionView.isHidden = !selected
|
||||||
|
selectionPointer.isHidden = !selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PrivacySettingRow: UIView {
|
||||||
|
|
||||||
|
var onToggle: ((Bool) -> Void)?
|
||||||
|
private let settingSwitch = UISwitch()
|
||||||
|
|
||||||
|
init(item: PrivacySettingItem) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
backgroundColor = .white
|
||||||
|
layer.cornerRadius = 20
|
||||||
|
|
||||||
|
let imageView = UIImageView(image: UIImage(named: item.imageName))
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
addSubview(imageView)
|
||||||
|
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = item.title
|
||||||
|
titleLabel.textColor = UIColor(hexStr: "#28364B")
|
||||||
|
titleLabel.font = .systemFont(ofSize: 17, weight: .regular)
|
||||||
|
addSubview(titleLabel)
|
||||||
|
|
||||||
|
settingSwitch.onTintColor = UIColor(hexStr: "#08ADEF")
|
||||||
|
settingSwitch.setOn(false, animated: false)
|
||||||
|
settingSwitch.addTarget(self, action: #selector(switchChanged), for: .valueChanged)
|
||||||
|
addSubview(settingSwitch)
|
||||||
|
|
||||||
|
imageView.layoutChain
|
||||||
|
.left(8)
|
||||||
|
.centerY()
|
||||||
|
.width(96)
|
||||||
|
.height(50)
|
||||||
|
|
||||||
|
settingSwitch.layoutChain
|
||||||
|
.right(13)
|
||||||
|
.centerY()
|
||||||
|
|
||||||
|
titleLabel.layoutChain
|
||||||
|
.leftToRightOfView(imageView, offset: 8)
|
||||||
|
.rightToLeftOfView(settingSwitch, offset: -8)
|
||||||
|
.centerY()
|
||||||
|
.height(26)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setSwitchEnabled(_ enabled: Bool) {
|
||||||
|
settingSwitch.isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
func setOn(_ on: Bool) {
|
||||||
|
settingSwitch.setOn(on, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func switchChanged() {
|
||||||
|
onToggle?(settingSwitch.isOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import RxSwift
|
import RxSwift
|
||||||
|
import SwiftyUserDefaults
|
||||||
|
|
||||||
final class PigeonMessageVC: BaseViewController {
|
final class PigeonMessageVC: BaseViewController {
|
||||||
|
|
||||||
|
|
@ -479,6 +480,10 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func sendMessage() {
|
@objc private func sendMessage() {
|
||||||
|
if let wait = pigeonSendWaitSeconds() {
|
||||||
|
DLToast.show(text: "发送过于频繁,请\(Self.formatWaitDuration(wait))后再试")
|
||||||
|
return
|
||||||
|
}
|
||||||
guard !selectedMembers.isEmpty else {
|
guard !selectedMembers.isEmpty else {
|
||||||
DLToast.show(text: "请先选择收件人")
|
DLToast.show(text: "请先选择收件人")
|
||||||
return
|
return
|
||||||
|
|
@ -574,6 +579,7 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
self.rootView.clearImageDraft()
|
self.rootView.clearImageDraft()
|
||||||
self.cleanupAudio(deleteFile: true)
|
self.cleanupAudio(deleteFile: true)
|
||||||
self.rootView.clearVoiceDraft()
|
self.rootView.clearVoiceDraft()
|
||||||
|
self.recordPigeonSendSuccess()
|
||||||
DLToast.show(text: "发送成功")
|
DLToast.show(text: "发送成功")
|
||||||
}, onError: { error in
|
}, onError: { error in
|
||||||
DLToast.dismiss()
|
DLToast.dismiss()
|
||||||
|
|
@ -582,6 +588,44 @@ final class PigeonMessageVC: BaseViewController {
|
||||||
.disposed(by: disposeBag)
|
.disposed(by: disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pigeonSendWaitSeconds() -> Int? {
|
||||||
|
guard let config = AppContextManager.shared.systemConfig?.pigeonConfig,
|
||||||
|
config.isEnabled else { return nil }
|
||||||
|
let userId = AppContextManager.shared.userId
|
||||||
|
guard !userId.isEmpty else { return nil }
|
||||||
|
let window = TimeInterval(config.times)
|
||||||
|
let now = Date().timeIntervalSince1970
|
||||||
|
let recent = (Defaults[\.pigeonSendAtByUser][userId] ?? []).filter { now - $0 < window }
|
||||||
|
guard recent.count >= config.nums else { return nil }
|
||||||
|
let oldest = recent.min() ?? now
|
||||||
|
return max(1, Int(ceil(window - (now - oldest))))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func formatWaitDuration(_ seconds: Int) -> String {
|
||||||
|
let total = max(0, seconds)
|
||||||
|
let hours = total / 3600
|
||||||
|
let minutes = (total % 3600) / 60
|
||||||
|
let secs = total % 60
|
||||||
|
var parts: [String] = []
|
||||||
|
if hours > 0 { parts.append("\(hours)小时") }
|
||||||
|
if minutes > 0 { parts.append("\(minutes)分钟") }
|
||||||
|
if secs > 0 || parts.isEmpty { parts.append("\(secs)秒") }
|
||||||
|
return parts.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recordPigeonSendSuccess() {
|
||||||
|
guard let config = AppContextManager.shared.systemConfig?.pigeonConfig,
|
||||||
|
config.isEnabled else { return }
|
||||||
|
let userId = AppContextManager.shared.userId
|
||||||
|
guard !userId.isEmpty else { return }
|
||||||
|
let now = Date().timeIntervalSince1970
|
||||||
|
let window = TimeInterval(config.times)
|
||||||
|
var store = Defaults[\.pigeonSendAtByUser]
|
||||||
|
let recent = (store[userId] ?? []).filter { now - $0 < window }
|
||||||
|
store[userId] = recent + [now]
|
||||||
|
Defaults[\.pigeonSendAtByUser] = store
|
||||||
|
}
|
||||||
|
|
||||||
private func compressedPigeonJPEG(from image: UIImage) -> Data? {
|
private func compressedPigeonJPEG(from image: UIImage) -> Data? {
|
||||||
let maxEdge: CGFloat = 1280
|
let maxEdge: CGFloat = 1280
|
||||||
let quality: CGFloat = 0.55
|
let quality: CGFloat = 0.55
|
||||||
|
|
|
||||||
|
|
@ -674,9 +674,13 @@ extension PigeonMessageView: UICollectionViewDataSource, UICollectionViewDelegat
|
||||||
private extension PigeonMessageView {
|
private extension PigeonMessageView {
|
||||||
func toggleMember(at index: Int) {
|
func toggleMember(at index: Int) {
|
||||||
guard members.indices.contains(index) else { return }
|
guard members.indices.contains(index) else { return }
|
||||||
let id = members[index].user_id
|
let member = members[index]
|
||||||
|
let id = member.user_id
|
||||||
if selectedMemberIds.contains(id) {
|
if selectedMemberIds.contains(id) {
|
||||||
selectedMemberIds.remove(id)
|
selectedMemberIds.remove(id)
|
||||||
|
} else if !member.extra.privilege.pigeon_mail {
|
||||||
|
DLToast.show(text: "该用户未开启设置,无法接收飞鸽传书")
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
selectedMemberIds.insert(id)
|
selectedMemberIds.insert(id)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,9 +168,8 @@ class FullscreenWebView: WKWebView {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
fd_interactivePopDisabled = true
|
fd_interactivePopDisabled = true
|
||||||
if style == "default" {
|
if style == "default" {
|
||||||
let btn = UIButton(type: .system)
|
let btn = UIButton(type: .custom)
|
||||||
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
btn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||||
btn.tintColor = UIColor(hexStr: "#0F2846")
|
|
||||||
btn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
btn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
||||||
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: btn)
|
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: btn)
|
||||||
}
|
}
|
||||||
|
|
@ -315,17 +314,19 @@ class FullscreenWebView: WKWebView {
|
||||||
// backItem.tintColor = iconColor=="white" ? .white : .black
|
// backItem.tintColor = iconColor=="white" ? .white : .black
|
||||||
// backItem.tag = 1
|
// backItem.tag = 1
|
||||||
|
|
||||||
let backBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19))
|
let backBtn = UIButton(type: .custom)
|
||||||
|
backBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
|
||||||
backBtn.setImage(UIImage(named: "Common/back"), for: .normal)
|
backBtn.setImage(UIImage(named: "Common/back"), for: .normal)
|
||||||
backBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
backBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
||||||
backBtn.tintColor = .black
|
// backBtn.tintColor = .black
|
||||||
backBtn.tag = 1
|
backBtn.tag = 1
|
||||||
let backItem = UIBarButtonItem(customView: backBtn)
|
let backItem = UIBarButtonItem(customView: backBtn)
|
||||||
|
|
||||||
let popBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 19, height: 19))
|
let popBtn = UIButton(type: .custom)
|
||||||
|
popBtn.frame = CGRect(x: 0, y: 0, width: 19, height: 19)
|
||||||
popBtn.setImage(UIImage(named: "Common/x_black"), for: .normal)
|
popBtn.setImage(UIImage(named: "Common/x_black"), for: .normal)
|
||||||
popBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
popBtn.addTarget(self, action: #selector(leftBackClicked(_:)), for: .touchUpInside)
|
||||||
popBtn.tintColor = .black
|
// popBtn.tintColor = .black
|
||||||
popBtn.tag = 2
|
popBtn.tag = 2
|
||||||
|
|
||||||
let popItem = UIBarButtonItem(customView: popBtn)
|
let popItem = UIBarButtonItem(customView: popBtn)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ struct GroupService {
|
||||||
static let disposeBag = DisposeBag()
|
static let disposeBag = DisposeBag()
|
||||||
|
|
||||||
private static let notifyingOperations: Set<String> = [
|
private static let notifyingOperations: Set<String> = [
|
||||||
"create", "join", "setdefault", "editmateinfo"
|
"create", "join", "setdefault", "editmateinfo", "changeprivilege"
|
||||||
]
|
]
|
||||||
|
|
||||||
private static func postGroupDataChange(operation: String,
|
private static func postGroupDataChange(operation: String,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,13 @@ struct SystemService {
|
||||||
.map(ResponseModel.self)
|
.map(ResponseModel.self)
|
||||||
.asObservable()
|
.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func sosList() -> Observable<SOSListResponse> {
|
||||||
|
let api = SystemAPI.sosList.multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(SOSListResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
/// 查找
|
/// 查找
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,27 @@ struct UserService {
|
||||||
.asObservable()
|
.asObservable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func mailboxList(page: Int, size: Int = 10) -> Observable<MailboxListResponse> {
|
||||||
|
let api = UserAPI.mailboxList(page: page, size: size).multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(MailboxListResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func mailboxDetail(id: Int) -> Observable<MailboxDetailResponse> {
|
||||||
|
let api = UserAPI.mailboxOperate(opType: "info", id: id).multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(MailboxDetailResponse.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clearMailbox() -> Observable<ResponseModel> {
|
||||||
|
let api = UserAPI.mailboxOperate(opType: "alldelete", id: nil).multiTarget
|
||||||
|
return APIProvider.request(token: api, handle: false)
|
||||||
|
.map(ResponseModel.self)
|
||||||
|
.asObservable()
|
||||||
|
}
|
||||||
|
|
||||||
/// 提交意见反馈
|
/// 提交意见反馈
|
||||||
static func feedback(
|
static func feedback(
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ public class DLToastConfig {
|
||||||
/// 容器背景色
|
/// 容器背景色
|
||||||
var containerBgColor: UIColor = .black.withAlphaComponent(0.7)
|
var containerBgColor: UIColor = .black.withAlphaComponent(0.7)
|
||||||
/// 容器圆角
|
/// 容器圆角
|
||||||
var containerRadius: CGFloat = 12.0
|
var containerRadius: CGFloat = 20.0
|
||||||
/// 文字颜色
|
/// 文字颜色
|
||||||
var textColor: UIColor = .white
|
var textColor: UIColor = .white
|
||||||
/// 文字大小
|
/// 文字大小
|
||||||
|
|
|
||||||