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