diff --git a/LocalPackage/Sources/Model/Stores/CustomRunnerEditor.swift b/LocalPackage/Sources/Model/Stores/CustomRunnerEditor.swift index 9fe74f4..80cd3da 100644 --- a/LocalPackage/Sources/Model/Stores/CustomRunnerEditor.swift +++ b/LocalPackage/Sources/Model/Stores/CustomRunnerEditor.swift @@ -35,6 +35,7 @@ public final class CustomRunnerEditor: Composable, Identifiable { public let id: UUID public var runnerName: String public var isTemplate: Bool + public var allowsWideFrames: Bool public var frameImages: [FrameImage] public var selectingFrameImage: FrameImage? public var previewingFrameImage: FrameImage? @@ -53,6 +54,7 @@ public final class CustomRunnerEditor: Composable, Identifiable { id: UUID, runnerName: String = "", isTemplate: Bool = true, + allowsWideFrames: Bool = false, frameImages: [FrameImage] = [], selectingFrameImage: FrameImage? = nil, previewingFrameImage: FrameImage? = nil, @@ -69,6 +71,7 @@ public final class CustomRunnerEditor: Composable, Identifiable { self.id = id self.runnerName = runnerName self.isTemplate = isTemplate + self.allowsWideFrames = allowsWideFrames self.frameImages = frameImages self.selectingFrameImage = selectingFrameImage self.previewingFrameImage = previewingFrameImage @@ -98,6 +101,9 @@ public final class CustomRunnerEditor: Composable, Identifiable { case let .renderingModePickerSelected(renderingMode): isTemplate = renderingMode.isTemplate + case let .allowWideFramesToggleSwitched(allowsWideFrames): + self.allowsWideFrames = allowsWideFrames + case let .frameImageCellTapped(frameImage): selectingFrameImage = frameImage @@ -200,11 +206,15 @@ public final class CustomRunnerEditor: Composable, Identifiable { } } + private var maxFrameWidth: Int { + allowsWideFrames ? 300 : 100 + } + private func appendFrameImage(from url: URL) throws { guard let nsImage = NSImage(contentsOf: url), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil), cgImage.height == 36, - (10 ... 100).contains(cgImage.width) else { + (10 ... maxFrameWidth).contains(cgImage.width) else { throw RCNError.customRunner(.invalidFrameImage) } guard frameImages.count < 30 else { @@ -217,6 +227,7 @@ public final class CustomRunnerEditor: Composable, Identifiable { case viewAppeared case viewDisappeared case renderingModePickerSelected(RenderingMode) + case allowWideFramesToggleSwitched(Bool) case frameImageCellTapped(FrameImage) case collectionBackgroundTapped case filesDropped([URL]) diff --git a/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings b/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings index 1fc44e9..e6ae171 100644 --- a/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings +++ b/LocalPackage/Sources/UserInterface/Resources/Localizable.xcstrings @@ -397,6 +397,23 @@ } } }, + "allowWideFrames" : { + "comment" : "A label for a toggle that allows custom runner frames wider than 100px.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allow wide frames (up to 300px)" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "幅広フレームを許可(最大300px)" + } + } + } + }, "appearance" : { "comment" : "A heading for the appearance settings.", "isCommentAutoGenerated" : true, @@ -5742,6 +5759,23 @@ } } } + }, + "wideFramesWarning" : { + "comment" : "A warning shown when wide custom runner frames are allowed.", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frames wider than 100px may be hidden behind the camera notch on MacBooks. RunCat Neo keeps running, but the runner can look as if it has stopped." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "幅が100pxを超えるフレームは、MacBookのカメラノッチに隠れて表示されないことがあります。RunCat Neoは動作し続けますが、ランナーが止まったように見えることがあります。" + } + } + } } }, "version" : "1.2" diff --git a/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerEditorView.swift b/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerEditorView.swift index f454f8f..dacf819 100644 --- a/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerEditorView.swift +++ b/LocalPackage/Sources/UserInterface/Views/Settings/RunnerSettings/CustomRunnerEditorView.swift @@ -43,6 +43,15 @@ struct CustomRunnerEditorView: View { } label: { Text("renderingMode", bundle: .module) } + Toggle(isOn: Binding( + get: { store.allowsWideFrames }, + asyncSet: { await store.send(.allowWideFramesToggleSwitched($0)) } + )) { + Text("allowWideFrames", bundle: .module) + if store.allowsWideFrames { + Text("wideFramesWarning", bundle: .module) + } + } LabeledContent { FrameImagesCollectionView(store: store) } label: { diff --git a/LocalPackage/Tests/ModelTests/Resources/solid_red_200x36.png b/LocalPackage/Tests/ModelTests/Resources/solid_red_200x36.png new file mode 100644 index 0000000..53cddb2 Binary files /dev/null and b/LocalPackage/Tests/ModelTests/Resources/solid_red_200x36.png differ diff --git a/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerEditorTests.swift b/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerEditorTests.swift index 7390ea0..e9b5536 100644 --- a/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerEditorTests.swift +++ b/LocalPackage/Tests/ModelTests/StoreTests/CustomRunnerEditorTests.swift @@ -73,6 +73,26 @@ struct CustomRunnerEditorTests { #expect(sut.frameImages.isEmpty) } + @MainActor @Test + func send_filesDropped_forwards_error_when_image_is_wider_than_100px_by_default() async { + let recorder = errorRecorder() + let sut = CustomRunnerEditor(.testDependencies(), id: .init(), action: recorder.action) + await sut.send(.filesDropped([URL.fixture(name: "solid_red_200x36")])) + #expect(recorder.lock.withLock(\.self) == .customRunner(.invalidFrameImage)) + #expect(sut.frameImages.isEmpty) + } + + @MainActor @Test + func send_filesDropped_appends_wide_image_when_wide_frames_are_allowed() async { + let recorder = errorRecorder() + let sut = CustomRunnerEditor(.testDependencies(), id: .init(), action: recorder.action) + await sut.send(.allowWideFramesToggleSwitched(true)) + #expect(sut.allowsWideFrames) + await sut.send(.filesDropped([URL.fixture(name: "solid_red_200x36")])) + #expect(sut.frameImages.count == 1) + #expect(recorder.lock.withLock(\.self) == nil) + } + @MainActor @Test func send_filesDropped_forwards_error_when_frame_limit_is_reached() async { let recorder = errorRecorder()