From 07a1b096d9290ccbf7ccf916716a9b6c1627d0a8 Mon Sep 17 00:00:00 2001 From: "shuigao.fh" Date: Tue, 1 Sep 2026 19:12:14 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(floating-pet):=20add=20extra-l?= =?UTF-8?q?arge=20mascot=20size?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PingIsland/Core/Settings.swift | 5 ++++ .../Resources/en.lproj/Localizable.strings | 4 +++- .../zh-Hans.lproj/Localizable.strings | 4 +++- .../UI/Views/DetachedIslandPanelView.swift | 8 +++++-- PingIsland/UI/Views/SettingsWindowView.swift | 4 +++- .../AppSettingsPersistenceTests.swift | 6 ++--- .../DetachedIslandWindowControllerTests.swift | 23 +++++++++++++++++++ 7 files changed, 46 insertions(+), 8 deletions(-) diff --git a/PingIsland/Core/Settings.swift b/PingIsland/Core/Settings.swift index 851e1376..a5aa5142 100644 --- a/PingIsland/Core/Settings.swift +++ b/PingIsland/Core/Settings.swift @@ -257,6 +257,7 @@ enum FloatingPetSizeMode: String, CaseIterable, Identifiable { case automatic case standard case large + case extraLarge var id: String { rawValue } @@ -268,6 +269,8 @@ enum FloatingPetSizeMode: String, CaseIterable, Identifiable { return "标准" case .large: return "较大" + case .extraLarge: + return "超大" } } @@ -279,6 +282,8 @@ enum FloatingPetSizeMode: String, CaseIterable, Identifiable { return "固定为旧版悬浮宠物尺寸" case .large: return "在所有显示器上放大宠物形象" + case .extraLarge: + return "固定为 1.75 倍,明显放大宠物形象" } } } diff --git a/PingIsland/Resources/en.lproj/Localizable.strings b/PingIsland/Resources/en.lproj/Localizable.strings index 02c0b841..86724dee 100644 --- a/PingIsland/Resources/en.lproj/Localizable.strings +++ b/PingIsland/Resources/en.lproj/Localizable.strings @@ -653,7 +653,7 @@ "马上就好" = "Almost there"; "设置展示模式示意图,以及没有活跃或待处理会话时刘海/悬浮宠物使用的默认形象。" = "Choose the mascot shown in presentation previews and used by the notch or floating pet when no session is active or needs attention."; "宠物大小" = "Mascot size"; -"自动模式会根据当前显示器分辨率调整;也可以固定为标准尺寸或始终放大。" = "Automatic mode adjusts to the current display resolution. You can also keep the mascot at standard size or always enlarge it."; +"自动模式会根据当前显示器分辨率调整;也可以固定为标准、较大或超大尺寸。" = "Automatic mode adjusts to the current display resolution. You can also choose a fixed standard, larger, or extra-large size."; "空闲时自动保留到终端" = "Keep in terminal automatically when idle"; "键盘和鼠标静默达到设定时长后临时开启上方策略;恢复活跃后回到手动设置。" = "Temporarily enable the policy above after keyboard and mouse inactivity reaches the selected duration. Manual settings resume when activity returns."; "静默时长" = "Idle duration"; @@ -708,9 +708,11 @@ "1 小时" = "1 hour"; "标准" = "Standard"; "较大" = "Larger"; +"超大" = "Extra Large"; "按显示器分辨率调整,高分屏会更醒目" = "Adjust to the display resolution for better visibility on high-density screens"; "固定为旧版悬浮宠物尺寸" = "Use the original floating mascot size"; "在所有显示器上放大宠物形象" = "Enlarge the mascot on every display"; +"固定为 1.75 倍,明显放大宠物形象" = "Use a fixed 1.75x scale for a noticeably larger mascot"; "不显示" = "Hidden"; "主列表里隐藏挂靠在主 Agent 下的子 Agent 项" = "Hide sub-agent items attached to their main agent in the primary list"; "主列表里将明确的子 Agent 挂靠在主 Agent 下展示" = "Show explicit sub-agents under their main agent in the primary list"; diff --git a/PingIsland/Resources/zh-Hans.lproj/Localizable.strings b/PingIsland/Resources/zh-Hans.lproj/Localizable.strings index b41823d8..7c96c645 100644 --- a/PingIsland/Resources/zh-Hans.lproj/Localizable.strings +++ b/PingIsland/Resources/zh-Hans.lproj/Localizable.strings @@ -648,7 +648,7 @@ "马上就好" = "马上就好"; "设置展示模式示意图,以及没有活跃或待处理会话时刘海/悬浮宠物使用的默认形象。" = "设置展示模式示意图,以及没有活跃或待处理会话时刘海/悬浮宠物使用的默认形象。"; "宠物大小" = "宠物大小"; -"自动模式会根据当前显示器分辨率调整;也可以固定为标准尺寸或始终放大。" = "自动模式会根据当前显示器分辨率调整;也可以固定为标准尺寸或始终放大。"; +"自动模式会根据当前显示器分辨率调整;也可以固定为标准、较大或超大尺寸。" = "自动模式会根据当前显示器分辨率调整;也可以固定为标准、较大或超大尺寸。"; "空闲时自动保留到终端" = "空闲时自动保留到终端"; "键盘和鼠标静默达到设定时长后临时开启上方策略;恢复活跃后回到手动设置。" = "键盘和鼠标静默达到设定时长后临时开启上方策略;恢复活跃后回到手动设置。"; "静默时长" = "静默时长"; @@ -708,9 +708,11 @@ "1 小时" = "1 小时"; "标准" = "标准"; "较大" = "较大"; +"超大" = "超大"; "按显示器分辨率调整,高分屏会更醒目" = "按显示器分辨率调整,高分屏会更醒目"; "固定为旧版悬浮宠物尺寸" = "固定为旧版悬浮宠物尺寸"; "在所有显示器上放大宠物形象" = "在所有显示器上放大宠物形象"; +"固定为 1.75 倍,明显放大宠物形象" = "固定为 1.75 倍,明显放大宠物形象"; "不显示" = "不显示"; "主列表里隐藏挂靠在主 Agent 下的子 Agent 项" = "主列表里隐藏挂靠在主 Agent 下的子 Agent 项"; "主列表里将明确的子 Agent 挂靠在主 Agent 下展示" = "主列表里将明确的子 Agent 挂靠在主 Agent 下展示"; diff --git a/PingIsland/UI/Views/DetachedIslandPanelView.swift b/PingIsland/UI/Views/DetachedIslandPanelView.swift index 044c571e..947e1dc2 100644 --- a/PingIsland/UI/Views/DetachedIslandPanelView.swift +++ b/PingIsland/UI/Views/DetachedIslandPanelView.swift @@ -10,10 +10,12 @@ struct DetachedIslandPetMetrics: Equatable { let badgeOffset: CGSize let floatingUsageBoltVerticalOffset: CGFloat - static let standard = DetachedIslandPetMetrics(scale: 1) + static let minimumScale: CGFloat = 1 + static let maximumScale: CGFloat = 1.75 + static let standard = DetachedIslandPetMetrics(scale: minimumScale) init(scale: CGFloat) { - let sanitizedScale = min(max(scale, 1), 1.22) + let sanitizedScale = min(max(scale, Self.minimumScale), Self.maximumScale) self.scale = sanitizedScale self.petVisualFrame = 74 * sanitizedScale self.petHitFrame = 92 * sanitizedScale @@ -71,6 +73,8 @@ enum DetachedIslandPanelMetrics { return automaticResolutionScale(for: screenRect) case .large: return max(1.16, automaticResolutionScale(for: screenRect)) + case .extraLarge: + return DetachedIslandPetMetrics.maximumScale } } diff --git a/PingIsland/UI/Views/SettingsWindowView.swift b/PingIsland/UI/Views/SettingsWindowView.swift index 3d4d9de5..63df35d1 100644 --- a/PingIsland/UI/Views/SettingsWindowView.swift +++ b/PingIsland/UI/Views/SettingsWindowView.swift @@ -3235,7 +3235,7 @@ private struct SettingsPanelContentView: View { SettingsLineDivider() SettingsInfoLine( title: "宠物大小", - subtitle: "自动模式会根据当前显示器分辨率调整;也可以固定为标准尺寸或始终放大。" + subtitle: "自动模式会根据当前显示器分辨率调整;也可以固定为标准、较大或超大尺寸。" ) { FloatingPetSizeModePicker(mode: $settings.floatingPetSizeMode) } @@ -6598,6 +6598,8 @@ private struct IslandSurfaceModePreviewScene: View { return 1 case .large: return 1.16 + case .extraLarge: + return DetachedIslandPetMetrics.maximumScale } } } diff --git a/PingIslandTests/AppSettingsPersistenceTests.swift b/PingIslandTests/AppSettingsPersistenceTests.swift index 92323263..72da79d3 100644 --- a/PingIslandTests/AppSettingsPersistenceTests.swift +++ b/PingIslandTests/AppSettingsPersistenceTests.swift @@ -349,13 +349,13 @@ final class AppSettingsPersistenceTests: XCTestCase { XCTAssertEqual(store.floatingPetSizeMode, .automatic) - store.floatingPetSizeMode = .large + store.floatingPetSizeMode = .extraLarge let reloadedStore = makeStore(defaults: defaults) - XCTAssertEqual(reloadedStore.floatingPetSizeMode, .large) + XCTAssertEqual(reloadedStore.floatingPetSizeMode, .extraLarge) XCTAssertEqual( defaults.string(forKey: AppSettingsDefaultKeys.floatingPetSizeMode), - FloatingPetSizeMode.large.rawValue + FloatingPetSizeMode.extraLarge.rawValue ) } diff --git a/PingIslandTests/DetachedIslandWindowControllerTests.swift b/PingIslandTests/DetachedIslandWindowControllerTests.swift index 97466274..e47b8a18 100644 --- a/PingIslandTests/DetachedIslandWindowControllerTests.swift +++ b/PingIslandTests/DetachedIslandWindowControllerTests.swift @@ -437,6 +437,29 @@ final class DetachedIslandWindowControllerTests: XCTestCase { XCTAssertEqual(petMetrics.petHitFrame, 106.72, accuracy: 0.5) } + func testFloatingPetSizeModeCanForceExtraLargeSizeOnAnyResolution() { + let screenFrames = [ + CGRect(x: 0, y: 0, width: 1440, height: 900), + CGRect(x: 0, y: 0, width: 2560, height: 1440) + ] + + for screenFrame in screenFrames { + let petMetrics = DetachedIslandPanelMetrics.petMetrics( + for: screenFrame, + sizeMode: .extraLarge + ) + + XCTAssertEqual(petMetrics.scale, 1.75, accuracy: 0.001) + XCTAssertEqual(petMetrics.mascotDisplaySize, 80.5, accuracy: 0.5) + XCTAssertEqual(petMetrics.petHitFrame, 161, accuracy: 0.5) + } + } + + func testFloatingPetMetricsClampToSupportedScaleRange() { + XCTAssertEqual(DetachedIslandPetMetrics(scale: 0.5).scale, 1, accuracy: 0.001) + XCTAssertEqual(DetachedIslandPetMetrics(scale: 2).scale, 1.75, accuracy: 0.001) + } + func testActiveCountOnlyTracksActiveSessions() { let sessions = [ makeSession(id: "processing", phase: .processing),