Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Sources/CodeIsland/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "Preview",
"panel": "Panel",
"notch_animation_speed": "Open / Close Speed",
"notch_animation_speed_desc": "How fast the island expands and collapses",
"max_visible_sessions": "Max Visible Sessions",
"max_visible_sessions_desc": "Sessions beyond this limit will be scrollable",
"collapsed_width_scale": "Island Width",
Expand Down Expand Up @@ -519,6 +521,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "Vorschau",
"panel": "Panel",
"notch_animation_speed": "Öffnungs-/Schließgeschwindigkeit",
"notch_animation_speed_desc": "Wie schnell die Insel auf- und zuklappt",
"max_visible_sessions": "Maximal sichtbare Sitzungen",
"max_visible_sessions_desc": "Sitzungen über diesem Limit werden scrollbar",
"collapsed_width_scale": "Island-Breite",
Expand Down Expand Up @@ -872,6 +876,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "预览",
"panel": "面板",
"notch_animation_speed": "展开 / 收起速度",
"notch_animation_speed_desc": "灵动岛展开和收起动画的快慢",
"max_visible_sessions": "最大显示会话数",
"max_visible_sessions_desc": "超出数量的会话将通过滚动查看",
"collapsed_width_scale": "灵动岛宽度",
Expand Down Expand Up @@ -1225,6 +1231,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "預覽",
"panel": "面板",
"notch_animation_speed": "展開 / 收合速度",
"notch_animation_speed_desc": "靈動島展開和收合動畫的快慢",
"max_visible_sessions": "最大顯示會話數",
"max_visible_sessions_desc": "超出數量的會話將透過捲動檢視",
"collapsed_width_scale": "靈動島寬度",
Expand Down Expand Up @@ -1578,6 +1586,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "プレビュー",
"panel": "パネル",
"notch_animation_speed": "開閉スピード",
"notch_animation_speed_desc": "アイランドが展開・収縮する速さ",
"max_visible_sessions": "最大表示セッション数",
"max_visible_sessions_desc": "この数を超えるセッションはスクロールして表示します",
"collapsed_width_scale": "アイランドの幅",
Expand Down Expand Up @@ -1931,6 +1941,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "미리보기",
"panel": "패널",
"notch_animation_speed": "열기 / 닫기 속도",
"notch_animation_speed_desc": "아일랜드가 펼쳐지고 접히는 속도",
"max_visible_sessions": "최대 표시 세션 수",
"max_visible_sessions_desc": "이 수를 넘는 세션은 스크롤해서 볼 수 있습니다",
"collapsed_width_scale": "아일랜드 너비",
Expand Down Expand Up @@ -2284,6 +2296,8 @@ final class L10n: ObservableObject {
// Appearance
"preview": "Önizleme",
"panel": "Panel",
"notch_animation_speed": "Açılma / Kapanma Hızı",
"notch_animation_speed_desc": "Adanın ne kadar hızlı açılıp kapandığı",
"max_visible_sessions": "Maksimum Görünür Oturum",
"max_visible_sessions_desc": "Bu sınırdan fazla oturumlar kaydırılabilir olacak",
"collapsed_width_scale": "Ada Genişliği",
Expand Down
8 changes: 6 additions & 2 deletions Sources/CodeIsland/NotchAnimation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ import SwiftUI

enum NotchAnimation {
/// 展开面板:微弹,有少许回弹感
static let open = Animation.spring(response: 0.42, dampingFraction: 0.82)
static var open: Animation {
.spring(response: NotchAnimationMetrics.openResponse, dampingFraction: 0.82)
}
/// 收起面板:临界阻尼,无过冲(防止 NotchPanelShape 底边露出刘海)
static let close = Animation.spring(response: 0.38, dampingFraction: 1.0)
static var close: Animation {
.spring(response: NotchAnimationMetrics.closeResponse, dampingFraction: 1.0)
}
/// 通知弹出:快速弹跳,用于 completion/approval 自动展开
static let pop = Animation.spring(response: 0.3, dampingFraction: 0.65)
/// 微交互:hover 状态变化、按钮高亮等
Expand Down
41 changes: 41 additions & 0 deletions Sources/CodeIsland/NotchAnimationMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

/// Speed multiplier for the island's open and close animations.
/// Higher values shorten the spring response while preserving its damping.
enum NotchAnimationSpeed {
static let minimum = 0.5
static let maximum = 2.0
static let step = 0.1
static let normal = 1.0

static func clamped(_ value: Double) -> Double {
guard value.isFinite else { return normal }
return min(max(value, minimum), maximum)
}

static func response(base: TimeInterval, speed: Double) -> TimeInterval {
base / clamped(speed)
}
}

enum NotchAnimationMetrics {
/// Unscaled spring responses at 1.0x speed.
static let baseOpenResponse: TimeInterval = 0.42
static let baseCloseResponse: TimeInterval = 0.38

static var openResponse: TimeInterval {
NotchAnimationSpeed.response(base: baseOpenResponse, speed: storedSpeed)
}

static var closeResponse: TimeInterval {
NotchAnimationSpeed.response(base: baseCloseResponse, speed: storedSpeed)
}

private static var storedSpeed: Double {
let defaults = UserDefaults.standard
guard defaults.object(forKey: SettingsKey.notchAnimationSpeed) != nil else {
return NotchAnimationSpeed.normal
}
return NotchAnimationSpeed.clamped(defaults.double(forKey: SettingsKey.notchAnimationSpeed))
}
}
8 changes: 8 additions & 0 deletions Sources/CodeIsland/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ enum SettingsKey {
static let hideWhenNoSession = "hideWhenNoSession"
static let smartSuppress = "smartSuppress"
static let collapseOnMouseLeave = "collapseOnMouseLeave"
static let notchAnimationSpeed = "notchAnimationSpeed"
static let autoCollapseAfterSessionJump = "autoCollapseAfterSessionJump"
static let autoExpandOnPermission = "autoExpandOnPermission"
static let autoExpandOnCompletion = "autoExpandOnCompletion"
Expand Down Expand Up @@ -145,6 +146,7 @@ struct SettingsDefaults {
static let hideWhenNoSession = false
static let smartSuppress = true
static let collapseOnMouseLeave = true
static let notchAnimationSpeed = NotchAnimationSpeed.normal
static let autoCollapseAfterSessionJump = false
static let autoExpandOnPermission = true
static let autoExpandOnCompletion = true
Expand Down Expand Up @@ -232,6 +234,7 @@ class SettingsManager {
SettingsKey.hideWhenNoSession: SettingsDefaults.hideWhenNoSession,
SettingsKey.smartSuppress: SettingsDefaults.smartSuppress,
SettingsKey.collapseOnMouseLeave: SettingsDefaults.collapseOnMouseLeave,
SettingsKey.notchAnimationSpeed: SettingsDefaults.notchAnimationSpeed,
SettingsKey.autoCollapseAfterSessionJump: SettingsDefaults.autoCollapseAfterSessionJump,
SettingsKey.autoExpandOnPermission: SettingsDefaults.autoExpandOnPermission,
SettingsKey.autoExpandOnCompletion: SettingsDefaults.autoExpandOnCompletion,
Expand Down Expand Up @@ -336,6 +339,11 @@ class SettingsManager {
set { defaults.set(newValue, forKey: SettingsKey.collapseOnMouseLeave) }
}

var notchAnimationSpeed: Double {
get { NotchAnimationSpeed.clamped(defaults.double(forKey: SettingsKey.notchAnimationSpeed)) }
set { defaults.set(NotchAnimationSpeed.clamped(newValue), forKey: SettingsKey.notchAnimationSpeed) }
}

var hapticOnHover: Bool {
get { defaults.bool(forKey: SettingsKey.hapticOnHover) }
set { defaults.set(newValue, forKey: SettingsKey.hapticOnHover) }
Expand Down
27 changes: 27 additions & 0 deletions Sources/CodeIsland/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,7 @@ private struct AppearancePage: View {
@AppStorage(SettingsKey.collapsedWidthScale) private var collapsedWidthScale = SettingsDefaults.collapsedWidthScale
@AppStorage(SettingsKey.notchHeightMode) private var notchHeightModeRaw = SettingsDefaults.notchHeightMode
@AppStorage(SettingsKey.customNotchHeight) private var customNotchHeight = SettingsDefaults.customNotchHeight
@AppStorage(SettingsKey.notchAnimationSpeed) private var notchAnimationSpeed = SettingsDefaults.notchAnimationSpeed

private var notchHeightMode: Binding<NotchHeightMode> {
Binding(
Expand All @@ -877,6 +878,13 @@ private struct AppearancePage: View {
)
}

private var notchAnimationSpeedBinding: Binding<Double> {
Binding(
get: { NotchAnimationSpeed.clamped(notchAnimationSpeed) },
set: { notchAnimationSpeed = NotchAnimationSpeed.clamped($0) }
)
}

var body: some View {
Form {
Section(l10n["preview"]) {
Expand All @@ -888,6 +896,25 @@ private struct AppearancePage: View {
}

Section(l10n["panel"]) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(l10n["notch_animation_speed"])
Spacer()
Text("\(notchAnimationSpeedBinding.wrappedValue, specifier: "%.1f")×")
.foregroundStyle(.secondary)
.monospacedDigit()
}
Slider(
value: notchAnimationSpeedBinding,
in: NotchAnimationSpeed.minimum...NotchAnimationSpeed.maximum,
step: NotchAnimationSpeed.step
)
.accessibilityLabel(Text(l10n["notch_animation_speed"]))
.accessibilityValue(Text("\(notchAnimationSpeedBinding.wrappedValue, specifier: "%.1f")×"))
Text(l10n["notch_animation_speed_desc"])
.font(.caption)
.foregroundStyle(.secondary)
}
Picker(selection: $maxVisibleSessions) {
Text("3").tag(3)
Text("5").tag(5)
Expand Down
18 changes: 18 additions & 0 deletions Tests/CodeIslandTests/L10nTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,24 @@ final class L10nTests: XCTestCase {
XCTAssertEqual(L10n.shared["quit"], "Beenden")
}

func testNotchAnimationSpeedLabelsAreLocalized() {
let expected: [String: (title: String, description: String)] = [
"en": ("Open / Close Speed", "How fast the island expands and collapses"),
"de": ("Öffnungs-/Schließgeschwindigkeit", "Wie schnell die Insel auf- und zuklappt"),
"zh": ("展开 / 收起速度", "灵动岛展开和收起动画的快慢"),
"zh-Hant": ("展開 / 收合速度", "靈動島展開和收合動畫的快慢"),
"ja": ("開閉スピード", "アイランドが展開・収縮する速さ"),
"ko": ("열기 / 닫기 속도", "아일랜드가 펼쳐지고 접히는 속도"),
"tr": ("Açılma / Kapanma Hızı", "Adanın ne kadar hızlı açılıp kapandığı"),
]

for (language, translation) in expected {
L10n.shared.language = language
XCTAssertEqual(L10n.shared["notch_animation_speed"], translation.title)
XCTAssertEqual(L10n.shared["notch_animation_speed_desc"], translation.description)
}
}

func testEffectiveLanguageReturnsTurkishWhenSystemLocaleIsTurkish() {
L10n.shared.language = "system"

Expand Down
64 changes: 64 additions & 0 deletions Tests/CodeIslandTests/NotchAnimationSpeedTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import XCTest
@testable import CodeIsland

final class NotchAnimationSpeedTests: XCTestCase {
private var previousStoredSpeed: Any?

override func setUp() {
previousStoredSpeed = UserDefaults.standard.object(forKey: SettingsKey.notchAnimationSpeed)
}

override func tearDown() {
if let previousStoredSpeed {
UserDefaults.standard.set(previousStoredSpeed, forKey: SettingsKey.notchAnimationSpeed)
} else {
UserDefaults.standard.removeObject(forKey: SettingsKey.notchAnimationSpeed)
}
}

func testExplicitNonPositiveSpeedClampsToMinimum() {
XCTAssertEqual(NotchAnimationSpeed.clamped(0), 0.5)
XCTAssertEqual(NotchAnimationSpeed.clamped(-1), 0.5)
}

func testSpeedIsClampedToTheSliderRange() {
XCTAssertEqual(NotchAnimationSpeed.clamped(0.1), 0.5)
XCTAssertEqual(NotchAnimationSpeed.clamped(99), 2.0)
XCTAssertEqual(NotchAnimationSpeed.clamped(1.3), 1.3, accuracy: 0.0001)
}

func testHigherSpeedShortensTheSpringResponse() {
XCTAssertEqual(
NotchAnimationSpeed.response(base: 0.42, speed: 2.0),
0.21,
accuracy: 0.0001
)
}

func testLowerSpeedLengthensTheSpringResponse() {
XCTAssertEqual(
NotchAnimationSpeed.response(base: 0.42, speed: 0.5),
0.84,
accuracy: 0.0001
)
}

func testShippedOpenAndCloseResponsesRemainDistinct() {
XCTAssertEqual(NotchAnimationMetrics.baseOpenResponse, 0.42, accuracy: 0.0001)
XCTAssertEqual(NotchAnimationMetrics.baseCloseResponse, 0.38, accuracy: 0.0001)
}

func testStoredSpeedScalesBothAnimationResponses() {
UserDefaults.standard.set(2.0, forKey: SettingsKey.notchAnimationSpeed)

XCTAssertEqual(NotchAnimationMetrics.openResponse, 0.21, accuracy: 0.0001)
XCTAssertEqual(NotchAnimationMetrics.closeResponse, 0.19, accuracy: 0.0001)
}

func testMissingStoredSpeedUsesNormalResponses() {
UserDefaults.standard.removeObject(forKey: SettingsKey.notchAnimationSpeed)

XCTAssertEqual(NotchAnimationMetrics.openResponse, 0.42, accuracy: 0.0001)
XCTAssertEqual(NotchAnimationMetrics.closeResponse, 0.38, accuracy: 0.0001)
}
}