From 369b67661559fd98e94864a9b25c1a908b8e4021 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Tue, 6 May 2025 23:05:23 +0800 Subject: [PATCH 01/15] fix(preview): change deprecated SSS to OF-1 --- MeoAsstMac/Views/CopilotView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeoAsstMac/Views/CopilotView.swift b/MeoAsstMac/Views/CopilotView.swift index b00662e38..891d45c59 100644 --- a/MeoAsstMac/Views/CopilotView.swift +++ b/MeoAsstMac/Views/CopilotView.swift @@ -118,7 +118,7 @@ struct CopilotView_Previews: PreviewProvider { static let url = Bundle.main.resourceURL! .appendingPathComponent("resource") .appendingPathComponent("copilot") - .appendingPathComponent("SSS_约翰老妈新建地块") + .appendingPathComponent("OF-1_credit_fight") .appendingPathExtension("json") static var previews: some View { From 364a969c7eb06064de31830a06ff730fba026d28 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Tue, 13 May 2025 22:19:55 +0800 Subject: [PATCH 02/15] fix(preview): add MAAViewModel for preview --- MeoAsstMac/Navigation/MAAContent.swift | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/MeoAsstMac/Navigation/MAAContent.swift b/MeoAsstMac/Navigation/MAAContent.swift index 9093e4165..4c5f1a574 100644 --- a/MeoAsstMac/Navigation/MAAContent.swift +++ b/MeoAsstMac/Navigation/MAAContent.swift @@ -14,14 +14,14 @@ struct MAAContent: View { var body: some View { Group { switch sidebar { - case .daily: - TasksContent(selection: $selection.task) - case .copilot: - CopilotContent(selection: $selection.copilot) - case .utility: - UtilityContent(selection: $selection.utility) - case .none: - Text("请从边栏选择功能") + case .daily: + TasksContent(selection: $selection.task) + case .copilot: + CopilotContent(selection: $selection.copilot) + case .utility: + UtilityContent(selection: $selection.utility) + case .none: + Text("请从边栏选择功能") } } .frame(minWidth: 260) @@ -30,8 +30,11 @@ struct MAAContent: View { struct MAAContent_Previews: PreviewProvider { static var previews: some View { - MAAContent(sidebar: .constant(.daily), - selection: .constant(.init())) + MAAContent( + sidebar: .constant(.daily), + selection: .constant(.init()) + ) + .environmentObject(MAAViewModel()) } } From adc17453f93148b64bf7e6ebfcab03fe63244149 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 03:28:42 +0800 Subject: [PATCH 03/15] fix(preview): add MAAViewModel for UtilityContent --- MeoAsstMac/Navigation/UtilityContent.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/MeoAsstMac/Navigation/UtilityContent.swift b/MeoAsstMac/Navigation/UtilityContent.swift index 311561275..e1678f2a9 100644 --- a/MeoAsstMac/Navigation/UtilityContent.swift +++ b/MeoAsstMac/Navigation/UtilityContent.swift @@ -74,6 +74,7 @@ struct UtilityDetailContent_Previews: PreviewProvider { static var previews: some View { NavigationView { UtilityContent(selection: .constant(.recruit)) + .environmentObject(MAAViewModel()) } } } From 95dc02493561fd7189059da94a88d6f5addf40b0 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 02:36:22 +0800 Subject: [PATCH 04/15] fix: typo in TaskDetail fix typo in TaskDetail (misspelled ViewDetailTabButton) --- MeoAsstMac/Navigation/TaskDetail.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MeoAsstMac/Navigation/TaskDetail.swift b/MeoAsstMac/Navigation/TaskDetail.swift index 6d55c2f32..d5ccca428 100644 --- a/MeoAsstMac/Navigation/TaskDetail.swift +++ b/MeoAsstMac/Navigation/TaskDetail.swift @@ -77,9 +77,9 @@ struct TaskDetail: View { HStack { Divider() - ViewDetaiTabButton(mode: .taskConfig, icon: "gearshape", selection: $viewModel.dailyTasksDetailMode) - ViewDetaiTabButton(mode: .log, icon: "note.text", selection: $viewModel.dailyTasksDetailMode) - ViewDetaiTabButton( + ViewDetailTabButton(mode: .taskConfig, icon: "gearshape", selection: $viewModel.dailyTasksDetailMode) + ViewDetailTabButton(mode: .log, icon: "note.text", selection: $viewModel.dailyTasksDetailMode) + ViewDetailTabButton( mode: .timerConfig, icon: "clock.arrow.2.circlepath", selection: $viewModel.dailyTasksDetailMode) } } @@ -93,7 +93,7 @@ struct TaskDetail: View { } } -struct ViewDetaiTabButton: View { +struct ViewDetailTabButton: View { let mode: MAAViewModel.DailyTasksDetailMode let icon: String @Binding var selection: MAAViewModel.DailyTasksDetailMode From 3e22c3b82fc014a0ec6644e91c848ce7cd111db5 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 02:39:38 +0800 Subject: [PATCH 05/15] fix: no permission reading user downloads --- MeoAsstMac/MAADev.entitlements | 2 ++ MeoAsstMac/MeoAsstMac.entitlements | 2 ++ 2 files changed, 4 insertions(+) diff --git a/MeoAsstMac/MAADev.entitlements b/MeoAsstMac/MAADev.entitlements index 9c8769bfc..46996a506 100644 --- a/MeoAsstMac/MAADev.entitlements +++ b/MeoAsstMac/MAADev.entitlements @@ -8,6 +8,8 @@ com.apple.security.device.usb + com.apple.security.files.downloads.read-only + com.apple.security.files.user-selected.read-only com.apple.security.network.client diff --git a/MeoAsstMac/MeoAsstMac.entitlements b/MeoAsstMac/MeoAsstMac.entitlements index 50231f75b..eb3c727d3 100644 --- a/MeoAsstMac/MeoAsstMac.entitlements +++ b/MeoAsstMac/MeoAsstMac.entitlements @@ -6,6 +6,8 @@ com.apple.security.device.usb + com.apple.security.files.downloads.read-only + com.apple.security.files.user-selected.read-only com.apple.security.network.client From 5c96b19491cb95b8538e7d1fc3aeb209d7d4f3cb Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 02:45:27 +0800 Subject: [PATCH 06/15] feat: copilot list models preparation --- MeoAsstMac.xcodeproj/project.pbxproj | 8 +++++ MeoAsstMac/Model/MAACopilot.swift | 8 +++++ .../CopilotConfiguration.swift | 5 ++++ .../CopilotItemConfiguration.swift | 19 ++++++++++++ .../CopilotListConfiguration.swift | 30 +++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 MeoAsstMac/Task Configurations/CopilotItemConfiguration.swift create mode 100644 MeoAsstMac/Task Configurations/CopilotListConfiguration.swift diff --git a/MeoAsstMac.xcodeproj/project.pbxproj b/MeoAsstMac.xcodeproj/project.pbxproj index 73e367b4a..dddd479f1 100644 --- a/MeoAsstMac.xcodeproj/project.pbxproj +++ b/MeoAsstMac.xcodeproj/project.pbxproj @@ -94,6 +94,8 @@ 92EB82872A0CD315006DD9D2 /* SystemSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92EB82862A0CD315006DD9D2 /* SystemSettingsView.swift */; }; 92F78D4D2A0A7B2E00999BD0 /* TaskTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F78D4C2A0A7B2E00999BD0 /* TaskTimerView.swift */; }; 92F78D4F2A0B361C00999BD0 /* TaskTimerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */; }; + 9BEBD1002DD3CA3C006D212B /* CopilotListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */; }; + 9BEBD1012DD3CA3C006D212B /* CopilotItemConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -212,6 +214,8 @@ 92EB82862A0CD315006DD9D2 /* SystemSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSettingsView.swift; sourceTree = ""; }; 92F78D4C2A0A7B2E00999BD0 /* TaskTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTimerView.swift; sourceTree = ""; }; 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTimerManager.swift; sourceTree = ""; }; + 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotItemConfiguration.swift; sourceTree = ""; }; + 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotListConfiguration.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -257,6 +261,8 @@ 83264A3429EBEE8D0074E33A /* AwardConfiguration.swift */, 8341194F29EC249E00044F45 /* RoguelikeConfiguration.swift */, 8341195429EC55C600044F45 /* CopilotConfiguration.swift */, + 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */, + 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */, 832985AF29EEF483009F7ED5 /* ReclamationConfiguration.swift */, 83782A2A2CFFAF2900A32BDD /* LegacyConfigurations.swift */, ); @@ -618,6 +624,8 @@ 838FF82729FDD0F9000566A0 /* GachaView.swift in Sources */, 83782A2B2CFFAF2900A32BDD /* LegacyConfigurations.swift in Sources */, 83264A2F29EBEE140074E33A /* RecruitConfiguration.swift in Sources */, + 9BEBD1002DD3CA3C006D212B /* CopilotListConfiguration.swift in Sources */, + 9BEBD1012DD3CA3C006D212B /* CopilotItemConfiguration.swift in Sources */, 8341195029EC249E00044F45 /* RoguelikeConfiguration.swift in Sources */, 83264A2B29EBED780074E33A /* StartupConfiguration.swift in Sources */, ); diff --git a/MeoAsstMac/Model/MAACopilot.swift b/MeoAsstMac/Model/MAACopilot.swift index 8899e8693..664633b6d 100644 --- a/MeoAsstMac/Model/MAACopilot.swift +++ b/MeoAsstMac/Model/MAACopilot.swift @@ -7,12 +7,20 @@ import Foundation +enum DifficultyFlags: Int, Codable { + case none = 0 + case normal = 1 + case raid = 2 + case normal_raid = 3 +} + struct MAACopilot: Codable, Equatable { let stage_name: String let opers: [Operator] let groups: [Group]? let minimum_required: String let doc: Documentation? + var difficulty: DifficultyFlags? = nil // MARK: SSS diff --git a/MeoAsstMac/Task Configurations/CopilotConfiguration.swift b/MeoAsstMac/Task Configurations/CopilotConfiguration.swift index 6221ceb88..526667dca 100644 --- a/MeoAsstMac/Task Configurations/CopilotConfiguration.swift +++ b/MeoAsstMac/Task Configurations/CopilotConfiguration.swift @@ -12,6 +12,11 @@ struct RegularCopilotConfiguration: Codable { var filename: String var formation = false var add_trust = false + var is_raid = false + var loop_times = 1 + var use_sanity_potion = false + var need_navigate = false + var navigate_name: String? } struct SSSCopilotConfiguration: Codable { diff --git a/MeoAsstMac/Task Configurations/CopilotItemConfiguration.swift b/MeoAsstMac/Task Configurations/CopilotItemConfiguration.swift new file mode 100644 index 000000000..d1280deec --- /dev/null +++ b/MeoAsstMac/Task Configurations/CopilotItemConfiguration.swift @@ -0,0 +1,19 @@ +import Foundation + +struct CopilotItemConfiguration: Codable { + var enabled: Bool = true + var filename: String + var name: String + var is_raid: Bool + var need_navigate: Bool + var navigate_name: String + + enum CodingKeys: String, CodingKey { + case enabled + case filename + case name + case is_raid + case need_navigate + case navigate_name + } +} diff --git a/MeoAsstMac/Task Configurations/CopilotListConfiguration.swift b/MeoAsstMac/Task Configurations/CopilotListConfiguration.swift new file mode 100644 index 000000000..f12edcb5c --- /dev/null +++ b/MeoAsstMac/Task Configurations/CopilotListConfiguration.swift @@ -0,0 +1,30 @@ +// +// CopilotListConfiguration.swift +// MAA +// +// Created by ninekirin on 2025/5/6. +// + +import Foundation + +struct CopilotListConfiguration: Codable { + var items: [CopilotItemConfiguration] = [] + var formation: Bool = true + var add_trust: Bool = false + var use_sanity_potion: Bool = false + + enum CodingKeys: String, CodingKey { + case items + case formation + case add_trust + case use_sanity_potion + } +} + +extension CopilotListConfiguration { + func jsonString() throws -> String { + let encoder = JSONEncoder() + let data = try encoder.encode(self) + return String(data: data, encoding: .utf8) ?? "" + } +} From dd321d92e6864396b248e4d842497d87e0a0cf14 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 02:51:38 +0800 Subject: [PATCH 07/15] feat: MapHelper for copilot list navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用于寻找地图的 stage_name (例如 act42side_10) 对应的 navigate_name (MT-10) --- MeoAsstMac.xcodeproj/project.pbxproj | 4 +++ MeoAsstMac/Utils/MapHelper.swift | 53 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 MeoAsstMac/Utils/MapHelper.swift diff --git a/MeoAsstMac.xcodeproj/project.pbxproj b/MeoAsstMac.xcodeproj/project.pbxproj index dddd479f1..9eff6c472 100644 --- a/MeoAsstMac.xcodeproj/project.pbxproj +++ b/MeoAsstMac.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 92F78D4F2A0B361C00999BD0 /* TaskTimerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */; }; 9BEBD1002DD3CA3C006D212B /* CopilotListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */; }; 9BEBD1012DD3CA3C006D212B /* CopilotItemConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */; }; + 9BEBD1052DD3CBB2006D212B /* MapHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD1042DD3CBB2006D212B /* MapHelper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -216,6 +217,7 @@ 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskTimerManager.swift; sourceTree = ""; }; 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotItemConfiguration.swift; sourceTree = ""; }; 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotListConfiguration.swift; sourceTree = ""; }; + 9BEBD1042DD3CBB2006D212B /* MapHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -457,6 +459,7 @@ 92CDF6202A065E20005669EC /* Utils */ = { isa = PBXGroup; children = ( + 9BEBD1042DD3CBB2006D212B /* MapHelper.swift */, 92CDF6212A065E55005669EC /* Codeable+JSON.swift */, 92F78D4E2A0B361C00999BD0 /* TaskTimerManager.swift */, 8347B5AE2CDF925400AEB627 /* URLSession+downloadProgress.swift */, @@ -604,6 +607,7 @@ 832985B629EF1890009F7ED5 /* MAADepot.swift in Sources */, 83D3283928F3048E00018D10 /* RoguelikeSettingsView.swift in Sources */, 92EB82872A0CD315006DD9D2 /* SystemSettingsView.swift in Sources */, + 9BEBD1052DD3CBB2006D212B /* MapHelper.swift in Sources */, 8315B0BD290E48F300AE7FDC /* StartupSettingsView.swift in Sources */, 83264A3529EBEE8D0074E33A /* AwardConfiguration.swift in Sources */, 832985BD29EFA43B009F7ED5 /* MAADetail.swift in Sources */, diff --git a/MeoAsstMac/Utils/MapHelper.swift b/MeoAsstMac/Utils/MapHelper.swift new file mode 100644 index 000000000..bad8c52e6 --- /dev/null +++ b/MeoAsstMac/Utils/MapHelper.swift @@ -0,0 +1,53 @@ +// +// MapHelper.swift +// MAA +// +// Created by ninekirin on 2025/5/8. +// + +import Foundation + +struct MapInfo: Codable { + var code: String? + var filename: String? + var levelId: String? + var name: String? + var stageId: String? + var height: Int + var width: Int + + private enum CodingKeys: String, CodingKey { + case code = "code" + case filename = "filename" + case levelId = "levelId" + case name = "name" + case stageId = "stageId" + case height = "height" + case width = "width" + } +} + +class MapHelper { + static private(set) var mapData: [MapInfo] = [] + + static func loadMapData() { + let path = Bundle.main.resourceURL! + .appendingPathComponent("resource") + .appendingPathComponent("Arknights-Tile-Pos") + .appendingPathComponent("overview.json") + + guard let data = try? Data(contentsOf: path), + let jsonObj = try? JSONDecoder().decode([String: MapInfo].self, from: data) + else { + return + } + + mapData = Array(jsonObj.values) + } + + static func findMap(_ mapId: String) -> MapInfo? { + return mapData.first { map in + [map.code, map.name, map.stageId, map.levelId].contains { $0 == mapId } + } + } +} From 203ca9b18637c6f1c3669f66f7a981b755d9e26d Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 03:28:08 +0800 Subject: [PATCH 08/15] feat: update all views for the copilot list --- MeoAsstMac.xcodeproj/project.pbxproj | 12 +- MeoAsstMac/Core/MaaMessage.swift | 25 ++ MeoAsstMac/Model/MAAViewModel.swift | 377 +++++++++++++++++- MeoAsstMac/Navigation/CopilotContent.swift | 295 ++------------ MeoAsstMac/Navigation/CopilotDetail.swift | 61 ++- MeoAsstMac/Navigation/CopilotList.swift | 72 ++++ MeoAsstMac/Navigation/MAAContent.swift | 2 +- MeoAsstMac/Navigation/MAADetail.swift | 16 +- .../Navigation/RegularCopilotList.swift | 84 ++++ MeoAsstMac/Views/CopilotView.swift | 26 +- Resources/Localizable.xcstrings | 111 +++++- 11 files changed, 778 insertions(+), 303 deletions(-) create mode 100644 MeoAsstMac/Navigation/CopilotList.swift create mode 100644 MeoAsstMac/Navigation/RegularCopilotList.swift diff --git a/MeoAsstMac.xcodeproj/project.pbxproj b/MeoAsstMac.xcodeproj/project.pbxproj index 9eff6c472..f318e9276 100644 --- a/MeoAsstMac.xcodeproj/project.pbxproj +++ b/MeoAsstMac.xcodeproj/project.pbxproj @@ -97,6 +97,8 @@ 9BEBD1002DD3CA3C006D212B /* CopilotListConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */; }; 9BEBD1012DD3CA3C006D212B /* CopilotItemConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */; }; 9BEBD1052DD3CBB2006D212B /* MapHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD1042DD3CBB2006D212B /* MapHelper.swift */; }; + 9BEBD1082DD3CD52006D212B /* RegularCopilotList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD1072DD3CD52006D212B /* RegularCopilotList.swift */; }; + 9BEBD1092DD3CD52006D212B /* CopilotList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BEBD1062DD3CD52006D212B /* CopilotList.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -218,6 +220,8 @@ 9BEBD0FE2DD3CA3C006D212B /* CopilotItemConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotItemConfiguration.swift; sourceTree = ""; }; 9BEBD0FF2DD3CA3C006D212B /* CopilotListConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotListConfiguration.swift; sourceTree = ""; }; 9BEBD1042DD3CBB2006D212B /* MapHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapHelper.swift; sourceTree = ""; }; + 9BEBD1062DD3CD52006D212B /* CopilotList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopilotList.swift; sourceTree = ""; }; + 9BEBD1072DD3CD52006D212B /* RegularCopilotList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegularCopilotList.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -332,10 +336,12 @@ 832985B829EF9D50009F7ED5 /* MAAContent.swift */, 832985BC29EFA43B009F7ED5 /* MAADetail.swift */, 8355E7B329E918A2005D5A1C /* TasksContent.swift */, - 8341195C29EC719B00044F45 /* CopilotContent.swift */, - 832985AD29EEE672009F7ED5 /* UtilityContent.swift */, 832985BA29EFA26D009F7ED5 /* TaskDetail.swift */, + 9BEBD1072DD3CD52006D212B /* RegularCopilotList.swift */, + 9BEBD1062DD3CD52006D212B /* CopilotList.swift */, + 8341195C29EC719B00044F45 /* CopilotContent.swift */, 8329858929ED121A009F7ED5 /* CopilotDetail.swift */, + 832985AD29EEE672009F7ED5 /* UtilityContent.swift */, 832985C029EFB665009F7ED5 /* UtilityDetail.swift */, ); path = Navigation; @@ -568,6 +574,8 @@ 83BFABC82AF1D5BA001A79F4 /* AwardSettingsView.swift in Sources */, 832985BB29EFA26D009F7ED5 /* TaskDetail.swift in Sources */, 834DEC122C4D94BD0073B5D4 /* ClosedownConfiguration.swift in Sources */, + 9BEBD1082DD3CD52006D212B /* RegularCopilotList.swift in Sources */, + 9BEBD1092DD3CD52006D212B /* CopilotList.swift in Sources */, 83477CB429FEA7A500A5FF99 /* TaskButtons.swift in Sources */, 8355E7B829E9E2B2005D5A1C /* Sidebar.swift in Sources */, 83661E2C29F77CFF007793CA /* VideoRecogView.swift in Sources */, diff --git a/MeoAsstMac/Core/MaaMessage.swift b/MeoAsstMac/Core/MaaMessage.swift index 55ee4ae51..47e317e54 100644 --- a/MeoAsstMac/Core/MaaMessage.swift +++ b/MeoAsstMac/Core/MaaMessage.swift @@ -139,12 +139,24 @@ extension MAAViewModel { logError("TaskError \(taskChain)") if isCopilot { logError("CombatError") + if isCopilotListRunning { + Task { try await stop() } + self.copilotListConfig = copilotListConfig + if let stage = getCurrentCopilotStageName() { + logError("战斗出错:\(stage)") + } + isCopilotListRunning = false + logError("CopilotListError") + } } case .TaskChainStart: if let id = taskID(taskDetails: message.details) { taskStatus[id] = .running } + if isCopilot, let stage = getCurrentCopilotStageName() { + logInfo("开始关卡:\(stage)") + } logTrace("StartTask \(taskChain)") case .TaskChainCompleted: @@ -173,6 +185,15 @@ extension MAAViewModel { if isCopilot { logInfo("CompleteCombat") + if isCopilotListRunning { + if let stage = getCurrentCopilotStageName() { + logInfo("完成关卡:\(stage)") + } + if let idx = copilotListConfig.items.firstIndex(where: { $0.enabled }) { + copilotListConfig.items[idx].enabled = false + self.copilotListConfig = copilotListConfig + } + } } case .TaskChainExtraInfo: @@ -180,6 +201,10 @@ extension MAAViewModel { case .AllTasksCompleted: logTrace("AllTasksComplete") + if isCopilotListRunning { + logInfo("CopilotListCompleted") + isCopilotListRunning = false + } resetStatus() default: diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index 8fcdb7b0d..b73cc4d58 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -86,10 +86,21 @@ import SwiftUI } @Published var copilot: CopilotConfiguration? - @Published var downloadCopilot: String? @Published var showImportCopilot = false @Published var copilotDetailMode: CopilotDetailMode = .log + @Published var copilots = Set() + @Published var downloading = false + @Published var selectedCopilotURL: URL? + + @AppStorage("MAAUseCopilotList") var useCopilotList = false + + @Published var isCopilotListRunning = false + + @AppStorage("MAACopilotListConfig") private var serializedCopilotListConfig: String? + + @Published var copilotListConfig = CopilotListConfiguration() + // MARK: - Recognition @Published var recruitConfig = RecruitConfiguration.recognition @@ -157,6 +168,10 @@ import SwiftUI logError("日志文件出错: \(error.localizedDescription)") } + loadUserCopilots() + + initCopilotListConfig() + do { let data = try Data(contentsOf: tasksURL) tasks = try PropertyListDecoder().decode([DailyTask].self, from: data) @@ -282,6 +297,9 @@ extension MAAViewModel { /// /// Should be the outermost call to load resources. private func loadResource(channel: MAAClientChannel) async throws { + // Initialize map data + MapHelper.loadMapData() + let (preferUser, currentResourceVersion) = try resourceChannel.version() try await loadResource(url: Bundle.main.resourceURL!, channel: channel) @@ -454,31 +472,279 @@ extension MAAViewModel { } } -// MARK: Copilot +// MARK: - Copilot Management + +extension MAAViewModel { + func loadUserCopilots() { + copilots.formUnion(externalDirectory.copilots) + copilots.formUnion(recordingDirectory.copilots) + } + + func addCopilots(_ providers: [NSItemProvider]) -> Bool { + Task { + for provider in providers { + if let url = try? await provider.loadURL() { + let value = try? url.resourceValues(forKeys: [.contentTypeKey]) + if value?.contentType == .json { + copilots.insert(url) + } else if value?.contentType?.conforms(to: .movie) == true { + try? await recognizeVideo(video: url) + } + } + } + self.useCopilotList = false + self.selectedCopilotURL = self.copilots.urls.last + } + return true + } + + func addCopilots(_ results: Result<[URL], Error>) { + if case let .success(urls) = results { + copilots.formUnion(urls) + } + } + + func downloadCopilot(id: String?) { + guard let id else { return } + + let file = + externalDirectory + .appendingPathComponent(id) + .appendingPathExtension("json") + + let url = URL(string: "https://prts.maa.plus/copilot/get/\(id)")! + Task { + self.downloading = true + do { + let data = try await URLSession.shared.data(from: url).0 + let response = try JSONDecoder().decode(CopilotResponse.self, from: data) + try response.data.content.write(toFile: file.path, atomically: true, encoding: .utf8) + copilots.insert(file) + logInfo("下载成功: \(file.lastPathComponent)") + self.useCopilotList = false + self.selectedCopilotURL = file + } catch { + print(error) + logInfo("下载失败: \(error.localizedDescription)") + } + self.downloading = false + } + } + + func deleteCopilot(url: URL) { + copilots.remove(url) + + copilotListConfig.items.removeAll(where: { $0.filename == url.path }) + + guard canDeleteFile(url) else { return } + try? FileManager.default.removeItem(at: url) + } + + func canDeleteFile(_ url: URL?) -> Bool { + [externalDirectory, recordingDirectory] + .compactMap { url?.path.starts(with: $0.path) } + .first(where: { $0 }) + ?? false + } + + var bundledDirectory: URL { + Bundle.main.resourceURL! + .appendingPathComponent("resource") + .appendingPathComponent("copilot") + } + + var externalDirectory: URL { + let directory = FileManager.default + .urls(for: .documentDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("copilot") + + if !FileManager.default.fileExists(atPath: directory.path) { + try? FileManager.default.createDirectory( + at: directory, withIntermediateDirectories: true) + } + + return directory + } + + var recordingDirectory: URL { + FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("cache") + .appendingPathComponent("CombatRecord") + } +} + +// MARK: Copilot List extension MAAViewModel { func startCopilot() async throws { status = .pending defer { handleEarlyReturn(backTo: .idle) } - guard let copilot, - let params = copilot.params - else { + if useCopilotList { + try await ensureHandle() + + logTrace("开始添加任务到战斗列表") + + guard copilotListConfig.items.firstIndex(where: { $0.enabled }) != nil else { + logTrace("没有启用的战斗列表任务") + return + } + + for (_, item) in copilotListConfig.items.enumerated() where item.enabled { + + let config = RegularCopilotConfiguration( + filename: item.filename, + formation: copilotListConfig.formation, + add_trust: copilotListConfig.add_trust, + is_raid: item.is_raid, + use_sanity_potion: copilotListConfig.use_sanity_potion, + need_navigate: item.need_navigate, + navigate_name: item.navigate_name + ) + + let copilot = CopilotConfiguration.regular(config) + + guard let params = copilot.params else { + continue + } + + logTrace("添加关卡:\(item.navigate_name) \(item.is_raid ? "突袭" : "普通")") + + do { + _ = try await handle?.appendTask(type: .Copilot, params: params) + } catch { + logError("添加关卡失败:\(error.localizedDescription)") + return + } + } + + isCopilotListRunning = true + try await handle?.start() + + } else { + guard let copilot, + let params = copilot.params + else { + return + } + + try await ensureHandle() + + switch copilot { + case .regular: + _ = try await handle?.appendTask(type: .Copilot, params: params) + case .sss: + _ = try await handle?.appendTask(type: .SSSCopilot, params: params) + } + + try await handle?.start() + } + + status = .busy + } + + func addToCopilotList(copilot: MAACopilot, url: URL) { + let name = copilot.doc?.title ?? url.lastPathComponent + + let stage_name = copilot.stage_name + guard !stage_name.isEmpty else { + logError("关卡名不能为空") return } - try await ensureHandle() + guard let navigate_name = MapHelper.findMap(stage_name)?.code else { + logError("找不到关卡 '\(stage_name)' 的地图数据,请更新资源") + + let alert = NSAlert() + alert.messageText = "地图数据不存在" + alert.informativeText = "找不到关卡 '\(stage_name)' 的地图数据,是否需要更新资源?" + alert.alertStyle = .warning + alert.addButton(withTitle: "更新") + alert.addButton(withTitle: "取消") - switch copilot { - case .regular: - _ = try await handle?.appendTask(type: .Copilot, params: params) - case .sss: - _ = try await handle?.appendTask(type: .SSSCopilot, params: params) + let response = alert.runModal() + if response == .alertFirstButtonReturn { + showResourceUpdate = true + } + return } - try await handle?.start() + // is_raid: DifficultyFlags == .raid or .normal_raid + // If DifficultyFlags is .normal_raid, add normal item first + let is_raid = + copilot.difficulty?.rawValue == DifficultyFlags.raid.rawValue + || copilot.difficulty?.rawValue == DifficultyFlags.normal_raid.rawValue + if copilot.difficulty?.rawValue == DifficultyFlags.normal_raid.rawValue { + let item = CopilotItemConfiguration( + enabled: true, + filename: url.path, + name: name, + is_raid: false, + need_navigate: true, + navigate_name: navigate_name + ) + copilotListConfig.items.append(item) + } - status = .busy + let item = CopilotItemConfiguration( + enabled: true, + filename: url.path, + name: name, + is_raid: is_raid, + need_navigate: true, + navigate_name: navigate_name, + ) + copilotListConfig.items.append(item) + } + + func removeFromCopilotList(at index: Int) { + copilotListConfig.items.remove(at: index) + } + + func moveCopilotItem(from source: Int, to destination: Int) { + copilotListConfig.items.move(fromOffsets: IndexSet(integer: source), toOffset: destination) + } + + func getCurrentCopilotStageName() -> String? { + guard isCopilotListRunning else { return nil } + return copilotListConfig.items.first(where: { $0.enabled }).map { item in + "\(item.navigate_name) \(item.is_raid ? "突袭" : "普通")" + } + } + + private func initCopilotListConfig() { + if let serializedString = serializedCopilotListConfig, + let data = serializedString.data(using: .utf8), + let config = try? JSONDecoder().decode(CopilotListConfiguration.self, from: data) + { + self.copilotListConfig = config + } else { + if serializedCopilotListConfig != nil { + logError("Failed to decode CopilotListConfiguration from saved data.") + } + } + + $copilotListConfig + .sink { [weak self] newConfig in + guard let self else { return } + + do { + let data = try JSONEncoder().encode(newConfig) + if let jsonString = String(data: data, encoding: .utf8) { + if jsonString != self.serializedCopilotListConfig { + self.serializedCopilotListConfig = jsonString + } + } else { + logError("Failed to serialize CopilotListConfiguration to string.") + } + } catch { + logError("Failed to encode CopilotListConfiguration: \(error.localizedDescription)") + } + } + .store(in: &cancellables) } } @@ -654,3 +920,88 @@ extension MAAViewModel { try await client.terminate() } } + +// MARK: - Value Extensions + +extension URL { + var copilots: [URL] { + guard + let urls = try? FileManager.default.contentsOfDirectory( + at: self, + includingPropertiesForKeys: [.contentTypeKey], + options: .skipsHiddenFiles) + else { return [] } + + return urls.filter { url in + let value = try? url.resourceValues(forKeys: [.contentTypeKey]) + return value?.contentType == .json + } + .sorted { lhs, rhs in + lhs.lastPathComponent < rhs.lastPathComponent + } + } +} + +extension Set where Element == URL { + var urls: [URL] { sorted { $0.lastPathComponent < $1.lastPathComponent } } +} + +// MARK: - Download Model + +private struct CopilotResponse: Codable { + let data: CopilotData + + struct CopilotData: Codable { + let content: String + } +} + +// MARK: - NSItemProvider Extension + +extension NSItemProvider { + @MainActor fileprivate func loadURL() async throws -> URL { + let handle = ProgressActor() + + return try await withTaskCancellationHandler { + try await withCheckedThrowingContinuation { continuation in + let progress = loadObject(ofClass: URL.self) { object, error in + if let error { + continuation.resume(throwing: error) + return + } + + guard let object else { + continuation.resume(throwing: MAAError.emptyItemObject) + return + } + + continuation.resume(returning: object) + } + + Task { + await handle.bind(progress: progress) + } + } + } onCancel: { + Task { + await handle.cancel() + } + } + } +} + +private actor ProgressActor { + private var progress: Progress? + private var cancelled = false + + func bind(progress: Progress) { + guard !cancelled else { return } + self.progress = progress + progress.resume() + } + + func cancel() { + cancelled = true + progress?.cancel() + } +} diff --git a/MeoAsstMac/Navigation/CopilotContent.swift b/MeoAsstMac/Navigation/CopilotContent.swift index f65545737..1cbc9bb6a 100644 --- a/MeoAsstMac/Navigation/CopilotContent.swift +++ b/MeoAsstMac/Navigation/CopilotContent.swift @@ -11,69 +11,45 @@ struct CopilotContent: View { @EnvironmentObject private var viewModel: MAAViewModel @Binding var selection: URL? - @State private var copilots = Set() - @State private var downloading = false - @State private var expanded = false + private func toggleCopilotList() { + viewModel.useCopilotList.toggle() + if viewModel.useCopilotList && viewModel.status == .idle { + // 战斗列表模式,未运行默认展示全局设置 + viewModel.copilotDetailMode = .copilotConfig + } + } var body: some View { - List(selection: $selection) { - DisclosureGroup(isExpanded: $expanded) { - ForEach(bundledCopilots, id: \.self) { url in - Text(url.lastPathComponent) - } - } label: { - Text("内置作业") - .font(.subheadline) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - withAnimation { - expanded.toggle() - } - } - } - - Section { - ForEach(copilots.urls, id: \.self) { url in - Text(url.lastPathComponent) - } - } header: { - HStack { - Text("外部作业(可拖入文件)") - if downloading { - Spacer() - ProgressView().controlSize(.small) - } - } + Group { + if viewModel.useCopilotList { + CopilotList() + } else { + RegularCopilotList(selection: $selection) } } .toolbar(content: listToolbar) - .animation(.default, value: copilots) - .animation(.default, value: downloading) - .onAppear(perform: loadUserCopilots) - .onDrop(of: [.fileURL], isTargeted: .none, perform: addCopilots) - .onReceive(viewModel.$copilotDetailMode, perform: deselectCopilot) - .onReceive(viewModel.$downloadCopilot, perform: downloadCopilot) - .onReceive(viewModel.$videoRecoginition, perform: selectNewCopilot) - .fileImporter( - isPresented: $viewModel.showImportCopilot, - allowedContentTypes: [.json], - allowsMultipleSelection: true, - onCompletion: addCopilots) } - // MARK: - Toolbar - @ToolbarContentBuilder private func listToolbar() -> some ToolbarContent { ToolbarItemGroup { - Button(action: deleteSelectedCopilot) { - Label("移除", systemImage: "trash") + if !viewModel.useCopilotList { + Button(action: deleteSelectedCopilot) { + Label("移除", systemImage: "trash") + } + .help("移除作业") + .disabled(shouldDisableDeletion) + .keyboardShortcut(.delete, modifiers: [.command]) + } + } + + ToolbarItemGroup { + Button(action: toggleCopilotList) { + Label( + "战斗列表", + systemImage: viewModel.useCopilotList ? "list.bullet.rectangle.fill" : "list.bullet.rectangle" + ) } - .help("移除作业") - .disabled(shouldDisableDeletion) - .keyboardShortcut(.delete, modifiers: [.command]) + .help(viewModel.useCopilotList ? "切换回单个作业模式" : "切换到战斗列表模式") } ToolbarItemGroup { @@ -112,72 +88,12 @@ struct CopilotContent: View { } } - private func loadUserCopilots() { - copilots.formUnion(externalDirectory.copilots) - copilots.formUnion(recordingDirectory.copilots) - } - - private func addCopilots(_ providers: [NSItemProvider]) -> Bool { - Task { - for provider in providers { - if let url = try? await provider.loadURL() { - let value = try? url.resourceValues(forKeys: [.contentTypeKey]) - if value?.contentType == .json { - copilots.insert(url) - } else if value?.contentType?.conforms(to: .movie) == true { - try? await viewModel.recognizeVideo(video: url) - } - } - } - self.selection = self.copilots.urls.last - } - - return true - } - - private func addCopilots(_ results: Result<[URL], Error>) { - if case let .success(urls) = results { - copilots.formUnion(urls) - selection = copilots.urls.last - } - } - - private func downloadCopilot(id: String?) { - guard let id else { return } - - let file = - externalDirectory - .appendingPathComponent(id) - .appendingPathExtension("json") - - let url = URL(string: "https://prts.maa.plus/copilot/get/\(id)")! - Task { - self.downloading = true - do { - let data = try await URLSession.shared.data(from: url).0 - let response = try JSONDecoder().decode(CopilotResponse.self, from: data) - try response.data.content.write(toFile: file.path, atomically: true, encoding: .utf8) - copilots.insert(file) - self.selection = file - } catch { - print(error) - } - self.downloading = false - } - } - - private func deleteCopilot(url: URL) { - copilots.remove(url) - guard canDelete(url) else { return } - try? FileManager.default.removeItem(at: url) - } - private func deleteSelectedCopilot() { - guard let selection, let index = copilots.urls.firstIndex(of: selection) else { return } + guard let selection, let index = viewModel.copilots.urls.firstIndex(of: selection) else { return } - deleteCopilot(url: selection) + viewModel.deleteCopilot(url: selection) - let urls = copilots.urls + let urls = viewModel.copilots.urls if index < urls.count { self.selection = urls[index] } else { @@ -185,19 +101,6 @@ struct CopilotContent: View { } } - private func deselectCopilot(_ viewMode: MAAViewModel.CopilotDetailMode) { - if viewMode != .copilotConfig { - selection = nil - } - } - - private func selectNewCopilot(url: URL?) { - if let url { - copilots.insert(url) - selection = copilots.urls.last - } - } - // MARK: - State Wrappers private var shouldDisableDeletion: Bool { @@ -205,137 +108,11 @@ struct CopilotContent: View { } private func isBundled(_ url: URL?) -> Bool { - return url?.path.starts(with: bundledDirectory.path) ?? false - } - - private func canDelete(_ url: URL?) -> Bool { - [externalDirectory, recordingDirectory] - .compactMap { url?.path.starts(with: $0.path) } - .first(where: { $0 }) - ?? false - } - - // MARK: - File Paths - - private var bundledCopilots: [URL] { bundledDirectory.copilots } - - private var bundledDirectory: URL { - Bundle.main.resourceURL! - .appendingPathComponent("resource") - .appendingPathComponent("copilot") - } - - private var externalDirectory: URL { - let directory = FileManager.default - .urls(for: .documentDirectory, in: .userDomainMask) - .first! - .appendingPathComponent("copilot") - - if !FileManager.default.fileExists(atPath: directory.path) { - try? FileManager.default.createDirectory( - at: directory, withIntermediateDirectories: true) - } - - return directory - } - - private var recordingDirectory: URL { - FileManager.default - .urls(for: .applicationSupportDirectory, in: .userDomainMask) - .first! - .appendingPathComponent("cache") - .appendingPathComponent("CombatRecord") - } -} - -struct CopilotContent_Previews: PreviewProvider { - static var previews: some View { - CopilotContent(selection: .constant(nil)) - .environmentObject(MAAViewModel()) + return url?.path.starts(with: viewModel.bundledDirectory.path) ?? false } } -// MARK: - Value Extensions - -extension URL { - fileprivate var copilots: [URL] { - guard - let urls = try? FileManager.default.contentsOfDirectory( - at: self, - includingPropertiesForKeys: [.contentTypeKey], - options: .skipsHiddenFiles) - else { return [] } - - return urls.filter { url in - let value = try? url.resourceValues(forKeys: [.contentTypeKey]) - return value?.contentType == .json - } - .sorted { lhs, rhs in - lhs.lastPathComponent < rhs.lastPathComponent - } - } -} - -extension Set where Element == URL { - fileprivate var urls: [URL] { sorted { $0.lastPathComponent < $1.lastPathComponent } } -} - -// MARK: - Download Model - -private struct CopilotResponse: Codable { - let data: CopilotData - - struct CopilotData: Codable { - let content: String - } -} - -// MARK: - Convenience Methods - -extension NSItemProvider { - @MainActor fileprivate func loadURL() async throws -> URL { - let handle = ProgressActor() - - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - let progress = loadObject(ofClass: URL.self) { object, error in - if let error { - continuation.resume(throwing: error) - return - } - - guard let object else { - continuation.resume(throwing: MAAError.emptyItemObject) - return - } - - continuation.resume(returning: object) - } - - Task { - await handle.bind(progress: progress) - } - } - } onCancel: { - Task { - await handle.cancel() - } - } - } -} - -private actor ProgressActor { - private var progress: Progress? - private var cancelled = false - - func bind(progress: Progress) { - guard !cancelled else { return } - self.progress = progress - progress.resume() - } - - func cancel() { - cancelled = true - progress?.cancel() - } +#Preview { + CopilotContent(selection: .constant(nil)) + .environmentObject(MAAViewModel()) } diff --git a/MeoAsstMac/Navigation/CopilotDetail.swift b/MeoAsstMac/Navigation/CopilotDetail.swift index ad868e73a..c86d265f8 100644 --- a/MeoAsstMac/Navigation/CopilotDetail.swift +++ b/MeoAsstMac/Navigation/CopilotDetail.swift @@ -15,7 +15,14 @@ struct CopilotDetail: View { var body: some View { VStack { - if let url { + if viewModel.useCopilotList { + switch viewModel.copilotDetailMode { + case .copilotConfig: + CopilotListSettings() + case .log: + LogView() + } + } else if let url { CopilotView(url: url) } else { LogView() @@ -25,6 +32,20 @@ struct CopilotDetail: View { .toolbar(content: detailToolbar) } + private struct CopilotListSettings: View { + @EnvironmentObject private var viewModel: MAAViewModel + + var body: some View { + Form { + Section("全局设置") { + Toggle("自动编队", isOn: $viewModel.copilotListConfig.formation) + Toggle("信赖干员", isOn: $viewModel.copilotListConfig.add_trust) + Toggle("允许使用理智药", isOn: $viewModel.copilotListConfig.use_sanity_potion) + } + } + } + } + // MARK: - Toolbar @ToolbarContentBuilder private func detailToolbar() -> some ToolbarContent { @@ -39,13 +60,15 @@ struct CopilotDetail: View { } ToolbarItemGroup { - Button { - viewModel.copilotDetailMode = .log - } label: { - Label("日志", systemImage: "note.text") - .foregroundColor(url == nil ? Color.accentColor : nil) + HStack { + Divider() + + if viewModel.useCopilotList { + CopilotDetailTabButton( + mode: .copilotConfig, icon: "gearshape", selection: $viewModel.copilotDetailMode) + } + CopilotDetailTabButton(mode: .log, icon: "note.text", selection: $viewModel.copilotDetailMode) } - .help("运行日志") } } @@ -64,7 +87,7 @@ struct CopilotDetail: View { Divider() Button { - viewModel.downloadCopilot = prtsCode.parsedID + viewModel.downloadCopilot(id: prtsCode.parsedID) showAdd = false } label: { Label("下载作业", systemImage: "arrow.down.doc") @@ -78,7 +101,7 @@ struct CopilotDetail: View { let parsedID = clipboardString.parsedID { prtsCode = clipboardString - viewModel.downloadCopilot = parsedID + viewModel.downloadCopilot(id: parsedID) showAdd = false } } label: { @@ -94,12 +117,32 @@ struct CopilotDetail: View { .frame(maxWidth: .infinity) } .buttonStyle(.bordered) + .fileImporter( + isPresented: $viewModel.showImportCopilot, + allowedContentTypes: [.json], + allowsMultipleSelection: true, + onCompletion: viewModel.addCopilots) } .frame(width: 200) .padding() } } +struct CopilotDetailTabButton: View { + let mode: MAAViewModel.CopilotDetailMode + let icon: String + @Binding var selection: MAAViewModel.CopilotDetailMode + + var body: some View { + Button { + selection = mode + } label: { + Image(systemName: icon) + .foregroundColor(mode == selection ? Color.accentColor : nil) + } + } +} + struct CopilotDetail_Previews: PreviewProvider { static let url = Bundle.main.resourceURL! .appendingPathComponent("resource") diff --git a/MeoAsstMac/Navigation/CopilotList.swift b/MeoAsstMac/Navigation/CopilotList.swift new file mode 100644 index 000000000..76009b337 --- /dev/null +++ b/MeoAsstMac/Navigation/CopilotList.swift @@ -0,0 +1,72 @@ +// +// CopilotList.swift +// MAA +// +// Created by ninekirin on 2025/5/6. +// + +import SwiftUI + +struct CopilotList: View { + @EnvironmentObject private var viewModel: MAAViewModel + + var body: some View { + List { + Section("战斗列表(自动模式)") { + ForEach(Array(viewModel.copilotListConfig.items.enumerated()), id: \.0) { index, item in + HStack { + Toggle( + "", + isOn: .init( + get: { item.enabled }, + set: { viewModel.copilotListConfig.items[index].enabled = $0 } + ) + ) + .labelsHidden() + + Text(item.name) + .frame(maxWidth: .infinity, alignment: .leading) + + if item.is_raid { + Text("突袭") + .foregroundStyle(.secondary) + } + + Menu { + Button(action: { + var newItem = item + newItem.is_raid.toggle() + viewModel.copilotListConfig.items[index] = newItem + }) { + Label( + item.is_raid ? "普通模式" : "突袭模式", + systemImage: item.is_raid ? "shield.slash" : "shield.fill") + } + + Button( + role: .destructive, + action: { + viewModel.removeFromCopilotList(at: index) + } + ) { + Label("删除", systemImage: "trash") + } + } label: { + Image(systemName: "ellipsis.circle") + } + .buttonStyle(.borderless) + } + } + .onMove { from, to in + viewModel.moveCopilotItem(from: from.first ?? 0, to: to) + } + } + } + .animation(.default, value: viewModel.copilotListConfig.items.count) + } +} + +#Preview { + CopilotList() + .environmentObject(MAAViewModel()) +} diff --git a/MeoAsstMac/Navigation/MAAContent.swift b/MeoAsstMac/Navigation/MAAContent.swift index 4c5f1a574..64b97904c 100644 --- a/MeoAsstMac/Navigation/MAAContent.swift +++ b/MeoAsstMac/Navigation/MAAContent.swift @@ -24,7 +24,7 @@ struct MAAContent: View { Text("请从边栏选择功能") } } - .frame(minWidth: 260) + .frame(minWidth: 290) } } diff --git a/MeoAsstMac/Navigation/MAADetail.swift b/MeoAsstMac/Navigation/MAADetail.swift index 5e80a13e9..c4fde2b35 100644 --- a/MeoAsstMac/Navigation/MAADetail.swift +++ b/MeoAsstMac/Navigation/MAADetail.swift @@ -14,14 +14,14 @@ struct MAADetail: View { var body: some View { Group { switch sidebar { - case .daily: - TaskDetail(id: selection.task) - case .copilot: - CopilotDetail(url: selection.copilot) - case .utility: - UtilityDetail(entry: selection.utility) - case .none: - Text("请选择内容项目") + case .daily: + TaskDetail(id: selection.task) + case .copilot: + CopilotDetail(url: selection.copilot) + case .utility: + UtilityDetail(entry: selection.utility) + case .none: + Text("请选择内容项目") } } .frame(minWidth: 450, minHeight: 360) diff --git a/MeoAsstMac/Navigation/RegularCopilotList.swift b/MeoAsstMac/Navigation/RegularCopilotList.swift new file mode 100644 index 000000000..57030bade --- /dev/null +++ b/MeoAsstMac/Navigation/RegularCopilotList.swift @@ -0,0 +1,84 @@ +// +// RegularCopilotList.swift +// MAA +// +// Created by ninekirin on 2025/5/6. +// + +import SwiftUI + +struct RegularCopilotList: View { + @EnvironmentObject private var viewModel: MAAViewModel + @Binding var selection: URL? + + @State private var expanded = false + + private var bundledCopilots: [URL] { viewModel.bundledDirectory.copilots } + + var body: some View { + List(selection: $selection) { + DisclosureGroup(isExpanded: $expanded) { + ForEach(bundledCopilots, id: \.self) { url in + Text(url.lastPathComponent) + } + } label: { + Text("内置作业") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + .onTapGesture { + withAnimation { + expanded.toggle() + } + } + } + + Section { + ForEach(viewModel.copilots.urls, id: \.self) { url in + Text(url.lastPathComponent) + } + } header: { + HStack { + Text("外部作业(可拖入文件)") + if viewModel.downloading { + Spacer() + ProgressView().controlSize(.small) + } + } + } + } + .animation(.default, value: viewModel.copilots) + .animation(.default, value: viewModel.downloading) + .onDrop(of: [.fileURL], isTargeted: .none, perform: viewModel.addCopilots) + .onReceive(viewModel.$copilotDetailMode, perform: deselectCopilot) + .onReceive(viewModel.$videoRecoginition, perform: selectNewCopilot) + // 这里我不太会处理双向数据绑定,先用这种方式 + .onChange(of: selection) { newValue in + viewModel.selectedCopilotURL = newValue + viewModel.copilotDetailMode = newValue == nil ? .log : .copilotConfig + } + .onChange(of: viewModel.selectedCopilotURL) { newValue in + selection = newValue + } + } + + private func deselectCopilot(_ viewMode: MAAViewModel.CopilotDetailMode) { + if viewMode != .copilotConfig { + selection = nil + } + } + + private func selectNewCopilot(url: URL?) { + if let url { + viewModel.copilots.insert(url) + selection = viewModel.copilots.urls.last + } + } +} + +#Preview { + RegularCopilotList(selection: .constant(nil)) + .environmentObject(MAAViewModel()) +} diff --git a/MeoAsstMac/Views/CopilotView.swift b/MeoAsstMac/Views/CopilotView.swift index 891d45c59..fecada38b 100644 --- a/MeoAsstMac/Views/CopilotView.swift +++ b/MeoAsstMac/Views/CopilotView.swift @@ -37,8 +37,9 @@ struct CopilotView: View { } } - private var copilot: MAACopilot? { - MAACopilot(url: url) + private func addToCopilotList() { + guard let pilot = MAACopilot(url: url) else { return } + viewModel.addToCopilotList(copilot: pilot, url: url) } // MARK: - Copilot Config @@ -51,9 +52,24 @@ struct CopilotView: View { } set: { newValue in viewModel.copilot = .regular(newValue) } - HStack { - Toggle("自动编队", isOn: binding.formation) - Toggle("信赖干员", isOn: binding.add_trust) + VStack { + HStack { + Toggle("自动编队", isOn: binding.formation) + Toggle("信赖干员", isOn: binding.add_trust) + } + + Button(action: addToCopilotList) { + Label("添加到战斗列表", systemImage: "plus.rectangle.on.rectangle") + } + .buttonStyle(.borderedProminent) + + if viewModel.copilotListConfig.items.contains(where: { $0.filename == innerConfig.filename }) { + Text("已添加到战斗列表") + .foregroundColor(.green) + } else { + Text("未添加到战斗列表") + .foregroundColor(.red) + } } case .sss(let innerConfig): diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index e82ed7de8..25232d8f8 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -367,6 +367,26 @@ } } }, + "CopilotListCompleted" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动战斗列表已全部完成!" + } + } + } + }, + "CopilotListError" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "自动战斗列表出错,已停止执行后续任务" + } + } + } + }, "CurrentStage %@" : { "localizations" : { "ko" : { @@ -488,6 +508,15 @@ } } } + }, + "Failed to decode CopilotListConfiguration from saved data." : { + + }, + "Failed to encode CopilotListConfiguration: %@" : { + + }, + "Failed to serialize CopilotListConfiguration to string." : { + }, "FailedToOpenClient" : { "localizations" : { @@ -1581,6 +1610,12 @@ }, "下载作业" : { + }, + "下载失败: %@" : { + + }, + "下载成功: %@" : { + }, "不使用" : { @@ -1786,6 +1821,9 @@ }, "允许使用 Gzip" : { + }, + "允许使用理智药" : { + }, "允许阻止系统睡眠" : { "localizations" : { @@ -1799,6 +1837,9 @@ }, "免费单抽" : { + }, + "全局设置" : { + }, "全部取消" : { "localizations" : { @@ -1832,6 +1873,9 @@ } } } + }, + "关卡名不能为空" : { + }, "关卡选择" : { "localizations" : { @@ -1892,6 +1936,12 @@ }, "分队开局密文板" : { + }, + "切换到战斗列表模式" : { + + }, + "切换回单个作业模式" : { + }, "初始化失败: %@" : { @@ -2082,6 +2132,9 @@ }, "如果长时间连接不上或出错,请尝试下载使用“文件” > “PlayCover链接…”中的最新版本" : { + }, + "完成关卡:%@" : { + }, "官服" : { @@ -2153,6 +2206,9 @@ }, "已拥有干员:%lld" : { + }, + "已添加到战斗列表" : { + }, "希望" : { @@ -2201,9 +2257,15 @@ } } } + }, + "开始关卡:%@" : { + }, "开始唤醒" : { + }, + "开始添加任务到战斗列表" : { + }, "开局分队" : { @@ -2249,6 +2311,15 @@ } } } + }, + "战斗出错:%@" : { + + }, + "战斗列表" : { + + }, + "战斗列表(自动模式)" : { + }, "手动确认1星" : { @@ -2261,6 +2332,9 @@ }, "打开自定义排班文件夹…" : { + }, + "找不到关卡 '%@' 的地图数据,请更新资源" : { + }, "投资后进二层" : { @@ -2371,9 +2445,6 @@ }, "日常任务定时执行会在系统休眠之后失效, 打开此功能可以阻止系统自动睡眠" : { - }, - "日志" : { - }, "日志文件出错: %@" : { @@ -2420,6 +2491,9 @@ } } } + }, + "普通模式" : { + }, "更新失败:%@" : { @@ -2461,6 +2535,9 @@ }, "未拥有干员:%lld" : { + }, + "未添加到战斗列表" : { + }, "未知排班" : { @@ -2510,6 +2587,9 @@ }, "沙洲遗闻" : { + }, + "没有启用的战斗列表任务" : { + }, "深入调查" : { @@ -2542,6 +2622,22 @@ }, "添加作业" : { + }, + "添加关卡:%@ %@" : { + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "new", + "value" : "添加关卡:%1$@ %2$@" + } + } + } + }, + "添加关卡失败:%@" : { + + }, + "添加到战斗列表" : { + }, "游戏设置" : { "localizations" : { @@ -2630,6 +2726,12 @@ } } } + }, + "突袭" : { + + }, + "突袭模式" : { + }, "策略" : { @@ -2952,9 +3054,6 @@ }, "跟着鸭总喝口汤" : { - }, - "运行日志" : { - }, "进行每日免费单抽" : { "localizations" : { From ab001ca1847be4ed8a0dae96783a2ff549682065 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Wed, 14 May 2025 02:11:21 +0800 Subject: [PATCH 09/15] chore: code format & cleanup --- MeoAsstMac/Core/Maa.swift | 18 ++++---- MeoAsstMac/Core/MaaMessage.swift | 4 +- MeoAsstMac/Core/MaaToolClient.swift | 20 ++++---- .../Main Menu/CheckForUpdatesView.swift | 3 +- MeoAsstMac/Model/MAACopilot.swift | 2 +- MeoAsstMac/Model/MAADepot.swift | 10 ++-- MeoAsstMac/Model/MAAOperBox.swift | 2 +- MeoAsstMac/Model/MAATask.swift | 2 +- MeoAsstMac/Model/MAAViewModel.swift | 6 +-- .../MAATaskConfiguration.swift | 2 +- MeoAsstMac/Views/CopilotView.swift | 2 +- MeoAsstMac/Views/TaskTimerView.swift | 46 +++++++++++-------- 12 files changed, 65 insertions(+), 52 deletions(-) diff --git a/MeoAsstMac/Core/Maa.swift b/MeoAsstMac/Core/Maa.swift index b261aebc6..b42624fee 100644 --- a/MeoAsstMac/Core/Maa.swift +++ b/MeoAsstMac/Core/Maa.swift @@ -34,7 +34,7 @@ actor MAAHandle { private let callback: AsstApiCallback = { msg, details_json, custom_arg in guard let details_json, let custom_arg, - let detailString = String(cString: details_json, encoding: .utf8) + let detailString = String(cString: details_json, encoding: .utf8) else { return } @@ -43,9 +43,10 @@ actor MAAHandle { let message = MaaMessage(code: Int(msg), details: details) let uuid = custom_arg.assumingMemoryBound(to: UUID.self).pointee - NotificationCenter.default.post(name: .MAAReceivedCallbackMessage, - object: message, - userInfo: ["uuid": uuid]) + NotificationCenter.default.post( + name: .MAAReceivedCallbackMessage, + object: message, + userInfo: ["uuid": uuid]) } init(options: MAAInstanceOptions = [:]) throws { @@ -103,8 +104,9 @@ actor MAAHandle { let data = Data(bytesNoCopy: buffer, count: size, deallocator: .free) guard let provider = CGDataProvider(data: data as CFData), - let image = CGImage(pngDataProviderSource: provider, decode: nil, - shouldInterpolate: false, intent: .defaultIntent) + let image = CGImage( + pngDataProviderSource: provider, decode: nil, + shouldInterpolate: false, intent: .defaultIntent) else { throw MaaCoreError.getImageFailed } @@ -175,8 +177,8 @@ extension JSON { } } -private extension AsstBool { - var isTrue: Bool { self != 0 } +extension AsstBool { + fileprivate var isTrue: Bool { self != 0 } } struct MAAResourceVersion: Codable { diff --git a/MeoAsstMac/Core/MaaMessage.swift b/MeoAsstMac/Core/MaaMessage.swift index 47e317e54..bc5d87af8 100644 --- a/MeoAsstMac/Core/MaaMessage.swift +++ b/MeoAsstMac/Core/MaaMessage.swift @@ -568,7 +568,7 @@ extension MAAViewModel { } } - // MARK: Recruit Recoginition + // MARK: - Recruit Recoginition private func processRecruitMessage(details: JSON) { guard let what = details["what"].string else { @@ -590,7 +590,7 @@ extension MAAViewModel { } } - // MARK: Video Recognition + // MARK: - Video Recognition private func processVideoMessage(details: JSON) { guard let what = details["what"].string else { diff --git a/MeoAsstMac/Core/MaaToolClient.swift b/MeoAsstMac/Core/MaaToolClient.swift index e01f0a124..a32b7558e 100644 --- a/MeoAsstMac/Core/MaaToolClient.swift +++ b/MeoAsstMac/Core/MaaToolClient.swift @@ -14,8 +14,8 @@ actor MaaToolClient { init?(address: String) async { let parts = address.split(separator: ":") guard parts.count >= 2, - let portNumber = UInt16(parts[1]), - let port = NWEndpoint.Port(rawValue: portNumber) + let portNumber = UInt16(parts[1]), + let port = NWEndpoint.Port(rawValue: portNumber) else { return nil } @@ -62,13 +62,15 @@ actor MaaToolClient { let data = Data([0x4d, 0x41, 0x41, 0x00, 0x00, 0x04, 0x54, 0x45, 0x52, 0x4d]) return try await withCheckedThrowingContinuation { continuation in - connection.send(content: data, completion: .contentProcessed { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - }) + connection.send( + content: data, + completion: .contentProcessed { error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + }) } } } diff --git a/MeoAsstMac/Main Menu/CheckForUpdatesView.swift b/MeoAsstMac/Main Menu/CheckForUpdatesView.swift index 4f911c45f..a172c43cb 100644 --- a/MeoAsstMac/Main Menu/CheckForUpdatesView.swift +++ b/MeoAsstMac/Main Menu/CheckForUpdatesView.swift @@ -33,7 +33,8 @@ struct CheckForUpdatesView: View { } struct CheckForUpdatesView_Previews: PreviewProvider { - private static let updateController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + private static let updateController = SPUStandardUpdaterController( + startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) static var previews: some View { CheckForUpdatesView(updater: updateController.updater) diff --git a/MeoAsstMac/Model/MAACopilot.swift b/MeoAsstMac/Model/MAACopilot.swift index 664633b6d..300d53587 100644 --- a/MeoAsstMac/Model/MAACopilot.swift +++ b/MeoAsstMac/Model/MAACopilot.swift @@ -22,7 +22,7 @@ struct MAACopilot: Codable, Equatable { let doc: Documentation? var difficulty: DifficultyFlags? = nil - // MARK: SSS + // MARK: - SSS let type: String? let equipment: [String]? diff --git a/MeoAsstMac/Model/MAADepot.swift b/MeoAsstMac/Model/MAADepot.swift index 604d781b6..278956a12 100644 --- a/MeoAsstMac/Model/MAADepot.swift +++ b/MeoAsstMac/Model/MAADepot.swift @@ -11,22 +11,22 @@ struct MAADepot: Codable { let done: Bool let arkplanner: Arkplanner let lolicon: Lolicon - + struct Arkplanner: Codable { let object: ArkplannerObject let data: String } - + struct ArkplannerObject: Codable { let items: [ArkplannerItem] } - + struct ArkplannerItem: Codable { let id: String let have: Int let name: String } - + struct Lolicon: Codable { let object: [String: Int] let data: String @@ -39,7 +39,7 @@ extension MAADepot: CustomStringConvertible { .sorted { $0.id < $1.id } .map { "\($0.name): \($0.have)" } } - + var description: String { contents.joined(separator: "\n") } diff --git a/MeoAsstMac/Model/MAAOperBox.swift b/MeoAsstMac/Model/MAAOperBox.swift index 1c85cbebe..94234dd55 100644 --- a/MeoAsstMac/Model/MAAOperBox.swift +++ b/MeoAsstMac/Model/MAAOperBox.swift @@ -50,7 +50,7 @@ extension MAAOperBox.OwnedOper: Comparable { { $0.elite > $1.elite }, { $0.level > $1.level }, { $0.rarity > $1.rarity }, - { $0.id < $1.id } + { $0.id < $1.id }, ] } diff --git a/MeoAsstMac/Model/MAATask.swift b/MeoAsstMac/Model/MAATask.swift index 817ef2c57..6dd1d7469 100644 --- a/MeoAsstMac/Model/MAATask.swift +++ b/MeoAsstMac/Model/MAATask.swift @@ -68,7 +68,7 @@ extension MAATaskType: Codable, CustomStringConvertible { } } -// MARK: Task Persistance +// MARK: - Task Persistance struct DailyTask: Codable, Equatable, Identifiable { let id: UUID diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index b73cc4d58..81a398ae6 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -382,7 +382,7 @@ extension MAAViewModel { } } -// MARK: Daily Tasks +// MARK: - Daily Tasks extension MAAViewModel { func tryStartTasks() async { @@ -576,7 +576,7 @@ extension MAAViewModel { } } -// MARK: Copilot List +// MARK: - Copilot List extension MAAViewModel { func startCopilot() async throws { @@ -748,7 +748,7 @@ extension MAAViewModel { } } -// MARK: Utility +// MARK: - Utility extension MAAViewModel { func recognizeRecruit() async throws { diff --git a/MeoAsstMac/Task Configurations/MAATaskConfiguration.swift b/MeoAsstMac/Task Configurations/MAATaskConfiguration.swift index bc96103d7..6d538f6c6 100644 --- a/MeoAsstMac/Task Configurations/MAATaskConfiguration.swift +++ b/MeoAsstMac/Task Configurations/MAATaskConfiguration.swift @@ -28,7 +28,7 @@ extension MAATaskConfiguration { } } -// MARK: JSON TaskParams +// MARK: - JSON TaskParams extension MAAHandle { func appendTask(_ task: MAATask) throws -> Int32 { diff --git a/MeoAsstMac/Views/CopilotView.swift b/MeoAsstMac/Views/CopilotView.swift index fecada38b..c2b057eb7 100644 --- a/MeoAsstMac/Views/CopilotView.swift +++ b/MeoAsstMac/Views/CopilotView.swift @@ -57,7 +57,7 @@ struct CopilotView: View { Toggle("自动编队", isOn: binding.formation) Toggle("信赖干员", isOn: binding.add_trust) } - + Button(action: addToCopilotList) { Label("添加到战斗列表", systemImage: "plus.rectangle.on.rectangle") } diff --git a/MeoAsstMac/Views/TaskTimerView.swift b/MeoAsstMac/Views/TaskTimerView.swift index 7c2a119c4..a55e9a1c0 100644 --- a/MeoAsstMac/Views/TaskTimerView.swift +++ b/MeoAsstMac/Views/TaskTimerView.swift @@ -25,16 +25,18 @@ struct TaskTimerView: View { ) } } - .alert("允许阻止系统睡眠", - isPresented: $showingAlertForPreventingSleep, - actions: { - Button("允许") { - viewModel.preventSystemSleeping = true - } - Button("取消", role: .cancel) {} - }, message: { - Text("日常任务定时执行会在系统休眠之后失效, 打开此功能可以阻止系统自动睡眠") - }) + .alert( + "允许阻止系统睡眠", + isPresented: $showingAlertForPreventingSleep, + actions: { + Button("允许") { + viewModel.preventSystemSleeping = true + } + Button("取消", role: .cancel) {} + }, + message: { + Text("日常任务定时执行会在系统休眠之后失效, 打开此功能可以阻止系统自动睡眠") + }) } private func showAlertIfNeeded() { @@ -53,12 +55,15 @@ struct TaskTimerItem: View { var body: some View { HStack { - Button(action: { - onDelete() - }, label: { - Image(systemName: "minus.circle") - .foregroundColor(.red) - }) + Button( + action: { + onDelete() + }, + label: { + Image(systemName: "minus.circle") + .foregroundColor(.red) + } + ) .buttonStyle(PlainButtonStyle()) Spacer() @@ -79,9 +84,12 @@ struct TaskTimerItem: View { Spacer() - Toggle(isOn: $taskTimer.isEnabled, label: { - Text("开启") - }) + Toggle( + isOn: $taskTimer.isEnabled, + label: { + Text("开启") + } + ) .toggleStyle(.switch) .onChange(of: taskTimer.isEnabled) { if $0 { From 5f0ccb6c5e44f35929dd4a0c20803f5df42e8e2b Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Thu, 15 May 2025 00:45:05 +0800 Subject: [PATCH 10/15] fix: select new file after switching back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fixed the newly downloaded copilot file can not be selected automatically after switching back from the copilot list after downloading - added onSubmit to accepts keyboard Enter input for prtsCode - 修复了在战斗列表页面下载作业自动切回来之后不会选中新下载的作业的问题 - 神秘代码的 TextField 绑定 onSubmit,接受键盘 Enter 输入 --- MeoAsstMac/Navigation/CopilotContent.swift | 8 ++++++++ MeoAsstMac/Navigation/CopilotDetail.swift | 6 ++++++ MeoAsstMac/Navigation/RegularCopilotList.swift | 8 -------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/MeoAsstMac/Navigation/CopilotContent.swift b/MeoAsstMac/Navigation/CopilotContent.swift index 1cbc9bb6a..ceca17af2 100644 --- a/MeoAsstMac/Navigation/CopilotContent.swift +++ b/MeoAsstMac/Navigation/CopilotContent.swift @@ -28,6 +28,14 @@ struct CopilotContent: View { } } .toolbar(content: listToolbar) + // 这里我不太会处理双向数据绑定,先用这种方式 + .onChange(of: selection) { newValue in + viewModel.selectedCopilotURL = newValue + viewModel.copilotDetailMode = newValue == nil ? .log : .copilotConfig + } + .onChange(of: viewModel.selectedCopilotURL) { newValue in + selection = newValue + } } @ToolbarContentBuilder private func listToolbar() -> some ToolbarContent { diff --git a/MeoAsstMac/Navigation/CopilotDetail.swift b/MeoAsstMac/Navigation/CopilotDetail.swift index c86d265f8..cce684a51 100644 --- a/MeoAsstMac/Navigation/CopilotDetail.swift +++ b/MeoAsstMac/Navigation/CopilotDetail.swift @@ -82,6 +82,12 @@ struct CopilotDetail: View { .foregroundColor(.secondary) TextField("maa://", text: $prtsCode) .textFieldStyle(.roundedBorder) + .onSubmit { + if let parsedID = prtsCode.parsedID { + viewModel.downloadCopilot(id: parsedID) + showAdd = false + } + } } Divider() diff --git a/MeoAsstMac/Navigation/RegularCopilotList.swift b/MeoAsstMac/Navigation/RegularCopilotList.swift index 57030bade..4c4a61265 100644 --- a/MeoAsstMac/Navigation/RegularCopilotList.swift +++ b/MeoAsstMac/Navigation/RegularCopilotList.swift @@ -54,14 +54,6 @@ struct RegularCopilotList: View { .onDrop(of: [.fileURL], isTargeted: .none, perform: viewModel.addCopilots) .onReceive(viewModel.$copilotDetailMode, perform: deselectCopilot) .onReceive(viewModel.$videoRecoginition, perform: selectNewCopilot) - // 这里我不太会处理双向数据绑定,先用这种方式 - .onChange(of: selection) { newValue in - viewModel.selectedCopilotURL = newValue - viewModel.copilotDetailMode = newValue == nil ? .log : .copilotConfig - } - .onChange(of: viewModel.selectedCopilotURL) { newValue in - selection = newValue - } } private func deselectCopilot(_ viewMode: MAAViewModel.CopilotDetailMode) { From a0e3d57e12c74815a0408f3f8d4f578761af90d2 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Thu, 15 May 2025 00:47:30 +0800 Subject: [PATCH 11/15] fix: remove MAAUseCopilotList persistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit removed MAAUseCopilotList persistence and show RegularCopilotList by default on startup 移除了 MAAUseCopilotList 的持久化,应用启动默认展示 RegularCopilotList --- MeoAsstMac/Model/MAAViewModel.swift | 11 +++++------ Resources/Localizable.xcstrings | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index 81a398ae6..04e03065c 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -93,11 +93,10 @@ import SwiftUI @Published var downloading = false @Published var selectedCopilotURL: URL? - @AppStorage("MAAUseCopilotList") var useCopilotList = false - + @Published var useCopilotList = false @Published var isCopilotListRunning = false - @AppStorage("MAACopilotListConfig") private var serializedCopilotListConfig: String? + @AppStorage("MAACopilotListConfig") var serializedCopilotListConfig: String? @Published var copilotListConfig = CopilotListConfiguration() @@ -520,12 +519,12 @@ extension MAAViewModel { let response = try JSONDecoder().decode(CopilotResponse.self, from: data) try response.data.content.write(toFile: file.path, atomically: true, encoding: .utf8) copilots.insert(file) - logInfo("下载成功: \(file.lastPathComponent)") - self.useCopilotList = false + logTrace("下载成功:\(file.lastPathComponent)") self.selectedCopilotURL = file + self.useCopilotList = false } catch { print(error) - logInfo("下载失败: \(error.localizedDescription)") + logTrace("下载失败:\(error.localizedDescription)") } self.downloading = false } diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 25232d8f8..961ac77b5 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1611,10 +1611,10 @@ "下载作业" : { }, - "下载失败: %@" : { + "下载失败:%@" : { }, - "下载成功: %@" : { + "下载成功:%@" : { }, "不使用" : { From 4fda9568a58b12fcdc4e2778b9bf71df1c83bbf7 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Thu, 15 May 2025 00:53:56 +0800 Subject: [PATCH 12/15] feat: change toggle button style --- MeoAsstMac/Navigation/CopilotContent.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MeoAsstMac/Navigation/CopilotContent.swift b/MeoAsstMac/Navigation/CopilotContent.swift index ceca17af2..54c59a76f 100644 --- a/MeoAsstMac/Navigation/CopilotContent.swift +++ b/MeoAsstMac/Navigation/CopilotContent.swift @@ -54,9 +54,11 @@ struct CopilotContent: View { Button(action: toggleCopilotList) { Label( "战斗列表", - systemImage: viewModel.useCopilotList ? "list.bullet.rectangle.fill" : "list.bullet.rectangle" + systemImage: "list.bullet" ) } + .background(viewModel.useCopilotList ? .gray.opacity(0.2) : .clear) + .cornerRadius(4) .help(viewModel.useCopilotList ? "切换回单个作业模式" : "切换到战斗列表模式") } From 5cbae1f7d06193e0006d7d497235fafa23f7ae34 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Sun, 18 May 2025 16:30:08 +0800 Subject: [PATCH 13/15] chore: remove redundant comma --- MeoAsstMac/Model/MAAViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MeoAsstMac/Model/MAAViewModel.swift b/MeoAsstMac/Model/MAAViewModel.swift index 04e03065c..557f7818a 100644 --- a/MeoAsstMac/Model/MAAViewModel.swift +++ b/MeoAsstMac/Model/MAAViewModel.swift @@ -694,7 +694,7 @@ extension MAAViewModel { name: name, is_raid: is_raid, need_navigate: true, - navigate_name: navigate_name, + navigate_name: navigate_name ) copilotListConfig.items.append(item) } From 8e4c88ad5353a0865b9f1583473a3880351bc56b Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Sun, 22 Jun 2025 02:15:22 +0800 Subject: [PATCH 14/15] feat: implement user_additional param --- .../CopilotConfiguration.swift | 7 ++ MeoAsstMac/Views/CopilotView.swift | 87 +++++++++++++++++-- Resources/Localizable.xcstrings | 20 ++++- 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/MeoAsstMac/Task Configurations/CopilotConfiguration.swift b/MeoAsstMac/Task Configurations/CopilotConfiguration.swift index 526667dca..2cf3853cc 100644 --- a/MeoAsstMac/Task Configurations/CopilotConfiguration.swift +++ b/MeoAsstMac/Task Configurations/CopilotConfiguration.swift @@ -7,6 +7,12 @@ import Foundation +struct UserAdditional: Codable, Hashable, Identifiable { + var id = UUID() + var name: String + var skill: Int +} + struct RegularCopilotConfiguration: Codable { var enable = true var filename: String @@ -17,6 +23,7 @@ struct RegularCopilotConfiguration: Codable { var use_sanity_potion = false var need_navigate = false var navigate_name: String? + var user_additional: [UserAdditional]? = nil } struct SSSCopilotConfiguration: Codable { diff --git a/MeoAsstMac/Views/CopilotView.swift b/MeoAsstMac/Views/CopilotView.swift index c2b057eb7..9edf05d27 100644 --- a/MeoAsstMac/Views/CopilotView.swift +++ b/MeoAsstMac/Views/CopilotView.swift @@ -11,6 +11,11 @@ struct CopilotView: View { @EnvironmentObject private var viewModel: MAAViewModel let url: URL + @State private var userAdditionals: [UserAdditional] = [] + @State private var additionalOperName: String = "" + @State private var additionalOperSkill: String = "1" + @State private var showUserAdditional: Bool = false + var body: some View { if let copilot = MAACopilot(url: url) { VStack(spacing: 20) { @@ -48,16 +53,15 @@ struct CopilotView: View { switch viewModel.copilot { case .regular(let innerConfig): let binding = Binding { - innerConfig + var config = innerConfig + config.user_additional = userAdditionals.isEmpty ? nil : userAdditionals + return config } set: { newValue in + userAdditionals = newValue.user_additional ?? [] viewModel.copilot = .regular(newValue) } VStack { - HStack { - Toggle("自动编队", isOn: binding.formation) - Toggle("信赖干员", isOn: binding.add_trust) - } - + // 战斗列表相关 Button(action: addToCopilotList) { Label("添加到战斗列表", systemImage: "plus.rectangle.on.rectangle") } @@ -70,6 +74,60 @@ struct CopilotView: View { Text("未添加到战斗列表") .foregroundColor(.red) } + + // 自动编队相关 + HStack { + VStack(alignment: .leading) { + Toggle("自动编队", isOn: binding.formation) + if binding.formation.wrappedValue { + Toggle("追加低信赖干员", isOn: binding.add_trust) + Toggle("追加自定干员", isOn: $showUserAdditional) + } + } + } + // 仅自动编队选中时展示 + if binding.formation.wrappedValue { + if showUserAdditional { + VStack(alignment: .leading, spacing: 4) { + Text("追加自定干员") + .font(.subheadline) + ForEach($userAdditionals) { $additional in + HStack { + TextField("干员名", text: $additional.name) + .frame(width: 100) + TextField("技能", text: Binding( + get: { String($additional.skill.wrappedValue) }, + set: { $additional.skill.wrappedValue = Int($0) ?? 1 } + )) + .frame(width: 40) + Button(action: { + userAdditionals.removeAll { $0.id == additional.id } + updateCopilotUserAdditionals() + }) { + Image(systemName: "minus.circle.fill").foregroundColor(.red) + } + } + } + HStack { + TextField("干员名", text: $additionalOperName) + .frame(width: 100) + TextField("技能", text: $additionalOperSkill) + .frame(width: 40) + Button(action: { + guard !additionalOperName.isEmpty, let skill = Int(additionalOperSkill) else { return } + let newAdd = UserAdditional(name: additionalOperName, skill: skill) + userAdditionals.append(newAdd) + additionalOperName = "" + additionalOperSkill = "1" + updateCopilotUserAdditionals() + }) { + Image(systemName: "plus.circle.fill").foregroundColor(.green) + } + } + } + .padding(.vertical, 4) + } + } } case .sss(let innerConfig): @@ -128,6 +186,23 @@ struct CopilotView: View { Text(toolmen.sorted { $0.key < $1.key }.map { "\($1)\($0)" }.joined(separator: ", ")) } } + + private func updateCopilotUserAdditionals() { + if case .regular(var config) = viewModel.copilot { + if bindingFormation() && showUserAdditional { + config.user_additional = userAdditionals.isEmpty ? nil : userAdditionals + } else { + config.user_additional = nil + } + viewModel.copilot = .regular(config) + } + } + private func bindingFormation() -> Bool { + if case .regular(let config) = viewModel.copilot { + return config.formation + } + return false + } } struct CopilotView_Previews: PreviewProvider { diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index a92721adf..b157c4048 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1868,7 +1868,10 @@ } } }, - "关卡数据获取失败: %@" : { + "关卡名不能为空" : { + + }, + "关卡资源获取失败: %@" : { }, "关卡选择" : { @@ -2212,6 +2215,9 @@ }, "干员列表" : { + }, + "干员名" : { + }, "干员寻访" : { @@ -2329,6 +2335,9 @@ }, "找不到关卡 '%@' 的地图数据,请更新资源" : { + }, + "技能" : { + }, "投资后进二层" : { @@ -3074,6 +3083,15 @@ } } } + }, + "追加低信赖干员" : { + + }, + "追加自定干员" : { + + }, + "退出" : { + }, "选择本地文件…" : { From 798f02458f64ca34d86d358fbf293bf2f97e0dc2 Mon Sep 17 00:00:00 2001 From: KirinBaka Date: Sun, 22 Jun 2025 02:24:42 +0800 Subject: [PATCH 15/15] fix: i18n --- Resources/Localizable.xcstrings | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index b157c4048..53b84742e 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1871,7 +1871,7 @@ "关卡名不能为空" : { }, - "关卡资源获取失败: %@" : { + "关卡数据获取失败: %@" : { }, "关卡选择" : { @@ -3089,9 +3089,6 @@ }, "追加自定干员" : { - }, - "退出" : { - }, "选择本地文件…" : {