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
2 changes: 2 additions & 0 deletions Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@
"Critical" = "Critical";
"Daily token usage in %@" = "Daily token usage in %@";
"Display" = "Display";
"Show provider logos before hover or expand." = "Show provider logos before hover or expand.";
"General" = "General";
"Follows macOS" = "Follows macOS";
"Glow only on refresh, hover, or limit alerts." = "Glow only on refresh, hover, or limit alerts.";
"Idle" = "Idle";
"Idle logos" = "Idle logos";
"idle" = "idle";
"Input + output" = "Input + output";
"Input + output only. Matches Anthropic's claude.ai stats." = "Input + output only. Matches Anthropic's claude.ai stats.";
Expand Down
2 changes: 2 additions & 0 deletions Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@
"Critical" = "危急";
"Daily token usage in %@" = "%@ 年每日 token 用量";
"Display" = "显示";
"Show provider logos before hover or expand." = "在悬停或展开前显示服务图标。";
"General" = "通用";
"Follows macOS" = "跟随 macOS";
"Glow only on refresh, hover, or limit alerts." = "仅在刷新、悬停或限额提醒时显示辉光。";
"Idle" = "空闲";
"Idle logos" = "空闲图标";
"idle" = "空闲";
"Input + output" = "输入 + 输出";
"Input + output only. Matches Anthropic's claude.ai stats." = "只统计输入 + 输出;与 Anthropic 的 claude.ai 统计一致。";
Expand Down
27 changes: 26 additions & 1 deletion Sources/Model/IslandModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class IslandModel: ObservableObject {
private var rawNotch: NotchInfo
private var activeScreen = ScreenPref.shared.screen
private var overviewDayDetailVisible = false
private var compactLogoPreferenceVisible = LogoVisibilityStore.shared.visible

private var subs: Set<AnyCancellable> = []

Expand All @@ -55,6 +56,7 @@ final class IslandModel: ObservableObject {
self.notch = Self.applyOverride(to: notch, width: IslandSpacingStore.shared.width)
recomputeSize()
subscribeToSpacingStore()
subscribeToLogoVisibilityStore()
subscribeToScreenPref()
}

Expand Down Expand Up @@ -145,6 +147,21 @@ final class IslandModel: ObservableObject {
.store(in: &subs)
}

private func subscribeToLogoVisibilityStore() {
LogoVisibilityStore.shared.$visible
.dropFirst()
.sink { [weak self] visible in
guard let self else { return }
withAnimation(.openMorph) {
self.compactLogoPreferenceVisible = visible
if self.state == .compact {
self.recomputeSize()
}
}
}
.store(in: &subs)
}

private func subscribeToScreenPref() {
ScreenPref.shared.$screen
.dropFirst()
Expand All @@ -166,7 +183,7 @@ final class IslandModel: ObservableObject {
switch state {
case .compact:
size = CGSize(
width: notch.width + tabWidth * 2,
width: notch.width + compactLogoTabWidth * 2,
height: notch.height
)
case .peek:
Expand All @@ -182,6 +199,14 @@ final class IslandModel: ObservableObject {
}
}

private var compactLogoTabWidth: CGFloat {
compactLogosVisible ? tabWidth : 0
}

var compactLogosVisible: Bool {
!notch.hasNotch || compactLogoPreferenceVisible
}

private var expandedContentHeight: CGFloat {
let baseHeight = activeScreen == .overview
? overviewBaseContentHeight
Expand Down
23 changes: 23 additions & 0 deletions Sources/Model/LogoVisibilityStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Foundation

/// User preference for showing the provider logos in the compact rest state.
///
/// Default on preserves the current branded island. On notched displays,
/// turning it off affects only `.compact`: the rest-state silhouette shrinks
/// to the physical notch width, so loading sweeps and alert glow stay fitted
/// to the hardware notch. Non-notched displays ignore the hidden state to
/// avoid leaving an empty synthetic pill at top-center.
@MainActor
final class LogoVisibilityStore: ObservableObject {
static let shared = LogoVisibilityStore()

private static let key = "MacIsland.compactLogosVisible"

@Published var visible: Bool {
didSet { UserDefaults.standard.set(visible, forKey: Self.key) }
}

private init() {
self.visible = Pref.seededBool(key: Self.key, default: true)
}
}
16 changes: 10 additions & 6 deletions Sources/Views/IslandRootView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ struct IslandRootView: View {
image: claudeLogo,
color: IslandColor.claude,
provider: .claude,
state: model.state,
compactLogosVisible: model.compactLogosVisible,
edgePadding: logoEdgePadding,
topPadding: max(0, (model.notch.height - 20) / 2)
)
Expand All @@ -83,6 +85,8 @@ struct IslandRootView: View {
image: openaiLogo,
color: IslandColor.codex,
provider: .codex,
state: model.state,
compactLogosVisible: model.compactLogosVisible,
edgePadding: logoEdgePadding,
topPadding: max(0, (model.notch.height - 20) / 2)
)
Expand Down Expand Up @@ -440,18 +444,17 @@ private struct LogoOverlay: View {
let image: NSImage?
let color: Color
let provider: AlertEngine.Provider
let state: IslandModel.State
let compactLogosVisible: Bool
let edgePadding: CGFloat
let topPadding: CGFloat

@ObservedObject private var visibility = ProviderVisibilityStore.shared

var body: some View {
// Hidden providers fully drop out — header / peek pill / chrome
// are gated identically. `.opacity(isVisible ? 1 : 0)` keeps the
// view in the layout (so other overlays don't reflow) but makes
// it invisible, and the explicit `.animation(.openMorph, value:)`
// pairs the chrome fade with the panel layout swap when the user
// toggles a provider in Settings.
// The logo preference applies only to the compact rest state.
// Peek/expanded remain exactly as before so hover, pills, and panel
// chrome keep their current geometry.
if let image {
Image(nsImage: image)
.resizable()
Expand All @@ -470,6 +473,7 @@ private struct LogoOverlay: View {

private var isVisible: Bool {
visibility.effectiveVisible(provider: provider)
&& (state != .compact || compactLogosVisible)
Comment thread
justice-hwan marked this conversation as resolved.
}

private var providerLabel: String {
Expand Down
28 changes: 28 additions & 0 deletions Sources/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct SettingsView: View {
@ObservedObject private var alwaysShow = AlwaysShowUsageStore.shared
@ObservedObject private var alertPrefs = AlertThresholdStore.shared
@ObservedObject private var spacing = IslandSpacingStore.shared
@ObservedObject private var logos = LogoVisibilityStore.shared
@ObservedObject private var targetDisplay = IslandTargetDisplayStore.shared
@ObservedObject private var appLanguage = AppLanguageStore.shared
@ObservedObject private var usage = UsageStore.shared
Expand Down Expand Up @@ -138,6 +139,9 @@ struct SettingsView: View {
chartSection
costStyleSection
targetDisplaySection
if restChromeSectionVisible {
restChromeSection
}
if spacingSectionVisible {
spacingSection
}
Expand All @@ -152,6 +156,13 @@ struct SettingsView: View {
DisplayInfo.currentTarget()?.notch.hasNotch == false
}

/// The idle-logo toggle only makes sense on a physical notch. On
/// non-notched displays, hiding the rest-state logos would leave an empty
/// synthetic pill floating at top-center.
private var restChromeSectionVisible: Bool {
DisplayInfo.currentTarget()?.notch.hasNotch == true
}

private var providersTab: some View {
VStack(alignment: .leading, spacing: 0) {
providersSection
Expand Down Expand Up @@ -659,6 +670,23 @@ struct SettingsView: View {
.padding(.bottom, 14)
}

private var restChromeSection: some View {
VStack(alignment: .leading, spacing: 0) {
sectionLabel("Idle")
SettingsRow(
title: "Idle logos",
subtitle: "Show provider logos before hover or expand."
) {
SettingsToggle(isOn: logos.visible) {
logos.visible.toggle()
}
}
}
.padding(.horizontal, 14)
.padding(.top, 14)
.padding(.bottom, 14)
}

/// Default-on-the-left: Compact is the new default, so it sits left
/// of Notch-style.
private var spacingSegmented: some View {
Expand Down