From 4be78dedd1639e60cb9bbba1c83dcd671e84f509 Mon Sep 17 00:00:00 2001 From: "Takuto NAKAMURA (Kyome)" Date: Wed, 5 Aug 2026 00:31:56 +0900 Subject: [PATCH] Show the Runner Gallery link in an info popover The Custom Runners section carried the Runner Gallery link in a section footer built from an AttributedString, styling the link range by hand. Move it behind the same info button the Custom Metrics section already uses, and rename that section's help popover state and action to "info" so both sections name the affordance after the button the user taps. Co-Authored-By: Claude Opus 5 (1M context) --- .../Model/Stores/CustomMetricsSettings.swift | 14 +- .../Model/Stores/CustomRunnerSettings.swift | 9 +- .../Resources/Localizable.xcstrings | 132 ++++++++++++++++++ .../CustomMetricsSettingsSectionView.swift | 83 +++++------ .../CustomRunnerSettingsSectionView.swift | 42 ++++-- .../CustomMetricsSettingsTests.swift | 6 +- .../CustomRunnerSettingsTests.swift | 7 + 7 files changed, 228 insertions(+), 65 deletions(-) diff --git a/LocalPackage/Sources/Model/Stores/CustomMetricsSettings.swift b/LocalPackage/Sources/Model/Stores/CustomMetricsSettings.swift index 7127d72..580822a 100644 --- a/LocalPackage/Sources/Model/Stores/CustomMetricsSettings.swift +++ b/LocalPackage/Sources/Model/Stores/CustomMetricsSettings.swift @@ -37,7 +37,7 @@ public final class CustomMetricsSettings: Composable { public var showingFileImporter: Bool public var showingConfirmationDialog: Bool public var pendingRemovalSourceID: UUID? - public var showingHelpPopover: Bool + public var showingInfoPopover: Bool public let action: (Action) async -> Void public init( @@ -47,7 +47,7 @@ public final class CustomMetricsSettings: Composable { showingFileImporter: Bool = false, showingConfirmationDialog: Bool = false, pendingRemovalSourceID: UUID? = nil, - showingHelpPopover: Bool = false, + showingInfoPopover: Bool = false, action: @escaping (Action) async -> Void = { _ in } ) { self.appStateClient = appDependencies.appStateClient @@ -60,7 +60,7 @@ public final class CustomMetricsSettings: Composable { self.showingFileImporter = showingFileImporter self.showingConfirmationDialog = showingConfirmationDialog self.pendingRemovalSourceID = pendingRemovalSourceID - self.showingHelpPopover = showingHelpPopover + self.showingInfoPopover = showingInfoPopover self.action = action } @@ -103,9 +103,6 @@ public final class CustomMetricsSettings: Composable { case .addCustomMetricsSourceButtonTapped: showingFileImporter = true - case .helpButtonTapped: - showingHelpPopover = true - case let .fileImporterResponse(.success(urls)): guard let url = urls.first else { return } do { @@ -131,6 +128,9 @@ public final class CustomMetricsSettings: Composable { case .removingCustomMetricsSourceCancelled: pendingRemovalSourceID = nil + case .infoButtonTapped: + showingInfoPopover = true + case .errorOccurred: return } @@ -154,10 +154,10 @@ public final class CustomMetricsSettings: Composable { case customMetricsSourceLinkTapped(CustomMetricsSource) case customMetricsSourceRowMoved(IndexSet, Int) case addCustomMetricsSourceButtonTapped - case helpButtonTapped case fileImporterResponse(Result<[URL], any Error>) case removingCustomMetricsSourceConfirmed case removingCustomMetricsSourceCancelled + case infoButtonTapped case errorOccurred(RCNError) } } diff --git a/LocalPackage/Sources/Model/Stores/CustomRunnerSettings.swift b/LocalPackage/Sources/Model/Stores/CustomRunnerSettings.swift index 6b66d3e..ba73068 100644 --- a/LocalPackage/Sources/Model/Stores/CustomRunnerSettings.swift +++ b/LocalPackage/Sources/Model/Stores/CustomRunnerSettings.swift @@ -42,6 +42,7 @@ public final class CustomRunnerSettings: Composable { public var previewingFrameImage: FrameImage? public var previewSpeed: Double public var showingFileImporter: Bool + public var showingInfoPopover: Bool public let action: (Action) async -> Void public var canAdd: Bool { @@ -52,6 +53,7 @@ public final class CustomRunnerSettings: Composable { _ appDependencies: AppDependencies, customRunnerBundleList: [RunnerBundle] = [], showingCustomRunnerEditorSheet: Bool = false, + showingInfoPopover: Bool = false, runnerName: String = "", isTemplate: Bool = true, frameImages: [FrameImage] = [], @@ -68,6 +70,7 @@ public final class CustomRunnerSettings: Composable { self.runnerService = .init(appDependencies) self.customRunnerBundleList = customRunnerBundleList self.showingCustomRunnerEditorSheet = showingCustomRunnerEditorSheet + self.showingInfoPopover = showingInfoPopover self.runnerName = runnerName self.isTemplate = isTemplate self.frameImages = frameImages @@ -219,7 +222,10 @@ public final class CustomRunnerSettings: Composable { } catch { logService.critical(.unknown(error)) } - + + case .infoButtonTapped: + showingInfoPopover = true + case .errorOccurred: return } @@ -263,6 +269,7 @@ public final class CustomRunnerSettings: Composable { case deleteFrameButtonTapped case fileImporterResponse(Result<[URL], any Error>) case addButtonTapped + case infoButtonTapped case errorOccurred(RCNError) } } diff --git a/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings b/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings index ab4d0ef..7b93f46 100644 --- a/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings +++ b/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings @@ -2245,6 +2245,72 @@ } } }, + "information" : { + "comment" : "A button that opens a popover with more information about custom metrics.", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informationen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Information" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Información" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Informations" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "情報" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "정보" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Информация" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Thông tin" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "信息" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "資訊" + } + } + } + }, "invalidFormat" : { "comment" : "Error message when a metrics file does not follow the schema.", "isCommentAutoGenerated" : true, @@ -5610,6 +5676,72 @@ } } } + }, + "viewRunnerGallery" : { + "comment" : "A link to the runner gallery.", + "isCommentAutoGenerated" : true, + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Runner Gallery ansehen" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "View Runner Gallery" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ver la Runner Gallery" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Voir la Runner Gallery" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Runner Galleryを見る" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Runner Gallery 보기" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Посмотреть Runner Gallery" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Xem Runner Gallery" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "查看 Runner Gallery" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "查看 Runner Gallery" + } + } + } } }, "version" : "1.2" diff --git a/LocalPackage/Sources/UserInterface/Views/Settings/MetricsSettings/CustomMetricsSettingsSectionView.swift b/LocalPackage/Sources/UserInterface/Views/Settings/MetricsSettings/CustomMetricsSettingsSectionView.swift index 8a55c39..516c371 100644 --- a/LocalPackage/Sources/UserInterface/Views/Settings/MetricsSettings/CustomMetricsSettingsSectionView.swift +++ b/LocalPackage/Sources/UserInterface/Views/Settings/MetricsSettings/CustomMetricsSettingsSectionView.swift @@ -45,6 +45,30 @@ struct CustomMetricsSettingsSectionView: View { } } } + .confirmationDialog( + Text("removeCustomMetrics", bundle: .module), + isPresented: $store.showingConfirmationDialog, + presenting: store.pendingRemovalSourceID, + actions: { sourceID in + Button(role: .destructive) { + Task { + await store.send(.removingCustomMetricsSourceConfirmed) + } + } label: { + Text("remove", bundle: .module) + } + Button(role: .cancel) { + Task { + await store.send(.removingCustomMetricsSourceCancelled) + } + } label: { + Text("cancel", bundle: .module) + } + }, + message: { _ in + Text("customMetricsConfirmationMessage", bundle: .module) + } + ) HStack { Spacer() Button { @@ -58,15 +82,32 @@ struct CustomMetricsSettingsSectionView: View { Image(systemName: "plus") } } + .fileImporter( + isPresented: $store.showingFileImporter, + allowedContentTypes: [.json], + allowsMultipleSelection: false, + onCompletion: { result in + Task { + await store.send(.fileImporterResponse(result)) + } + } + ) + .fileDialogMessage(Text("chooseJsonFile", bundle: .module)) + .fileDialogConfirmationLabel(Text("add", bundle: .module)) Button { Task { - await store.send(.helpButtonTapped) + await store.send(.infoButtonTapped) } } label: { - Image(systemName: "info.circle") + Label { + Text("information", bundle: .module) + } icon: { + Image(systemName: "info.circle") + } + .labelStyle(.iconOnly) } .buttonStyle(.borderless) - .popover(isPresented: $store.showingHelpPopover, arrowEdge: .bottom) { + .popover(isPresented: $store.showingInfoPopover, arrowEdge: .bottom) { VStack(alignment: .leading, spacing: 4) { Text("customMetricsDescription", bundle: .module) .font(.caption) @@ -81,42 +122,6 @@ struct CustomMetricsSettingsSectionView: View { .frame(maxWidth: 360, alignment: .leading) } } - .fileImporter( - isPresented: $store.showingFileImporter, - allowedContentTypes: [.json], - allowsMultipleSelection: false, - onCompletion: { result in - Task { - await store.send(.fileImporterResponse(result)) - } - } - ) - .fileDialogMessage(Text("chooseJsonFile", bundle: .module)) - .fileDialogConfirmationLabel(Text("add", bundle: .module)) - .confirmationDialog( - Text("removeCustomMetrics", bundle: .module), - isPresented: $store.showingConfirmationDialog, - presenting: store.pendingRemovalSourceID, - actions: { sourceID in - Button(role: .destructive) { - Task { - await store.send(.removingCustomMetricsSourceConfirmed) - } - } label: { - Text("remove", bundle: .module) - } - Button(role: .cancel) { - Task { - await store.send(.removingCustomMetricsSourceCancelled) - } - } label: { - Text("cancel", bundle: .module) - } - }, - message: { _ in - Text("customMetricsConfirmationMessage", bundle: .module) - } - ) } header: { Text("customMetrics", bundle: .module) } diff --git a/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerSettingsSectionView.swift b/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerSettingsSectionView.swift index 5590da8..a665912 100644 --- a/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerSettingsSectionView.swift +++ b/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerSettingsSectionView.swift @@ -24,15 +24,6 @@ import SwiftUI struct CustomRunnerSettingsSectionView: View { @State var store: CustomRunnerSettings - private var runnerGalleryGuidance: AttributedString { - var text = AttributedString(String(localized: "runnerGalleryDescription", bundle: .module)) - if let range = text.range(of: "Runner Gallery") { - text[range].link = URL.runnerGallery - text[range].foregroundColor = NSColor.linkColor - } - return text - } - var body: some View { Section { List { @@ -70,15 +61,36 @@ struct CustomRunnerSettingsSectionView: View { } content: { CustomRunnerEditorView(store: store) } + Button { + Task { + await store.send(.infoButtonTapped) + } + } label: { + Label { + Text("information", bundle: .module) + } icon: { + Image(systemName: "info.circle") + } + .labelStyle(.iconOnly) + } + .buttonStyle(.borderless) + .popover(isPresented: $store.showingInfoPopover, arrowEdge: .bottom) { + VStack(alignment: .leading, spacing: 4) { + Text("runnerGalleryDescription", bundle: .module) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + Link(destination: URL.runnerGallery) { + Text("viewRunnerGallery", bundle: .module) + .font(.caption) + } + } + .padding() + .frame(maxWidth: 360, alignment: .leading) + } } } header: { Text("customRunners", bundle: .module) - } footer: { - Text(runnerGalleryGuidance) - .multilineTextAlignment(.leading) - .foregroundStyle(.secondary) - .frame(width: 420) - .fixedSize() } .task { await store.send(.viewAppeared) diff --git a/LocalPackage/Tests/ModelTests/StoreTests/CustomMetricsSettingsTests.swift b/LocalPackage/Tests/ModelTests/StoreTests/CustomMetricsSettingsTests.swift index 99a3d50..71377d1 100644 --- a/LocalPackage/Tests/ModelTests/StoreTests/CustomMetricsSettingsTests.swift +++ b/LocalPackage/Tests/ModelTests/StoreTests/CustomMetricsSettingsTests.swift @@ -213,10 +213,10 @@ struct CustomMetricsSettingsTests { } @MainActor @Test - func send_helpButtonTapped_shows_help_popover() async { + func send_infoButtonTapped_shows_info_popover() async { let sut = CustomMetricsSettings(.testDependencies()) - await sut.send(.helpButtonTapped) - #expect(sut.showingHelpPopover == true) + await sut.send(.infoButtonTapped) + #expect(sut.showingInfoPopover == true) } @MainActor @Test diff --git a/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerSettingsTests.swift b/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerSettingsTests.swift index ed707be..8b88923 100644 --- a/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerSettingsTests.swift +++ b/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerSettingsTests.swift @@ -353,4 +353,11 @@ struct CustomRunnerSettingsTests { #expect(recorder.lock.withLock(\.self) == nil) #expect(sut.customRunnerBundleList.isEmpty) } + + @MainActor @Test + func send_infoButtonTapped_shows_info_popover() async { + let sut = CustomRunnerSettings(.testDependencies()) + await sut.send(.infoButtonTapped) + #expect(sut.showingInfoPopover == true) + } }