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
9 changes: 7 additions & 2 deletions src/ui/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { TitleBar } from "./components/TitleBar";
import { PlayerBar } from "./components/player/PlayerBar";
import { QueuePanel } from "./components/player/QueuePanel";
import { useQueuePanelCollapsed } from "./settings/queuePanel";
import { useNativeWindowControls } from "./settings/windowControls";
import { useNativeWindowControls, useWindowOutline } from "./settings/windowControls";

/** Wide enough for a 44px cover plus breathing room, matching the sidebar rail's feel. */
const COLLAPSED_QUEUE_WIDTH = 62;
Expand Down Expand Up @@ -377,6 +377,7 @@ export default function App() {
const [queuePanelWidth, setQueuePanelWidth] = useState(340);
const isQueuePanelCollapsed = useQueuePanelCollapsed();
const nativeWindowControls = useNativeWindowControls();
const windowOutline = useWindowOutline();
const [loadingScreenState, setLoadingScreenState] = useState<"visible" | "leaving" | "hidden">("visible");
const [onboardingComplete, setOnboardingComplete] = useState<boolean | null>(() =>
readLocalOnboardingComplete() ? true : null
Expand Down Expand Up @@ -1987,10 +1988,14 @@ useEffect(() => {

Both drop out under OS native decorations: the WM already draws a real frame above the
webview there, so this edge would just be a stray line under the OS title bar.
The "Window outline" setting can also turn it off for desktops where it reads as a
white border rather than an edge.
*/}
<div
className={`relative flex h-screen flex-col overflow-hidden rounded-[var(--window-radius)] ${
nativeWindowControls ? "" : "border border-border ring-1 ring-inset ring-[var(--window-edge)]"
nativeWindowControls || !windowOutline
? ""
: "border border-border ring-1 ring-inset ring-[var(--window-edge)]"
}`}
>
{/* {!paperPcMode && <StarField />}
Expand Down
3 changes: 2 additions & 1 deletion src/ui/components/settingsSearchIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,15 @@ const ENTRIES: SettingsSearchEntry[] = [
{ tab: "system", category: "Library", title: "Download quality", description: "Choose quality for offline music." },
{ tab: "system", category: "Library", title: "Lyrics", description: "Translate lyrics, set text size, and choose a source." },
{ tab: "system", category: "Library", title: "Launch at startup", description: "Start Zuno when your computer starts." },
{ tab: "system", category: "Library", title: "Minimize to tray", description: "Keep Zuno available from the system tray." },
{ tab: "system", category: "Library", title: "Remember window size and location", description: "Restore the main window geometry." },
{ tab: "system", category: "Library", title: "Troubleshooting", description: "Open logs or reset Zuno data." },
{ tab: "window", category: "Window", title: "Mini player", description: "Show a compact player when the main window loses focus." },
{ tab: "window", category: "Window", title: "Library sidebar", description: "Choose the playlist rail size and hover behavior." },
{ tab: "window", category: "Window", title: "Window controls", description: "Choose macOS, Windows, or native title-bar buttons." },
{ tab: "window", category: "Window", title: "Minimize to tray", description: "Keep Zuno available from the system tray." },
{ tab: "window", category: "Window", title: "Compact player bar", description: "Use a smaller playback bar." },
{ tab: "window", category: "Window", title: "System media controls", description: "Show playback in system media controls." },
{ tab: "window", category: "Window", title: "Window outline", description: "Show or hide the thin edge around the window." },
{ tab: "shortcuts", category: "Shortcuts", title: "Keyboard shortcuts", description: "View, record, or clear keyboard bindings." },
];

Expand Down
30 changes: 21 additions & 9 deletions src/ui/pages/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ import {
import {
setForceWindowControls,
setNativeWindowControls,
setWindowOutline,
setWindowsStyleWindowControls,
useForceWindowControls,
useNativeWindowControls,
useWindowOutline,
useWindowsStyleWindowControls,
} from "../settings/windowControls";
import {
Expand Down Expand Up @@ -647,6 +649,7 @@ export function SettingsPage({
const windowsStyleWindowControls = useWindowsStyleWindowControls();
const nativeWindowControls = useNativeWindowControls();
const forceWindowControls = useForceWindowControls();
const windowOutline = useWindowOutline();
const tilingWindowManager = useSyncExternalStore(
subscribeTilingWindowManager,
isTilingWindowManager,
Expand Down Expand Up @@ -1755,15 +1758,6 @@ export function SettingsPage({

{autostartError && <p className="text-sm text-destructive">{autostartError}</p>}


<SettingToggle
title="Minimize to tray"
description="Closing the window hides Zuno to the system tray and keeps playing. When off, no tray icon is shown."
checked={minimizeToTray}
onCheckedChange={setMinimizeToTray}
/>


<SettingToggle
title="Remember window size and location"
description="Reopen the main window with its last size and screen position."
Expand Down Expand Up @@ -2011,6 +2005,14 @@ export function SettingsPage({
)}
</SettingRow>

{/* Next to the window buttons, because it decides what the close button does. */}
<SettingToggle
title="Minimize to tray"
description="Closing the window hides Zuno to the system tray and keeps playing. When off, no tray icon is shown."
checked={minimizeToTray}
onCheckedChange={setMinimizeToTray}
/>

{/* Only reachable when it does something: hidden once native chrome takes over,
and off tiling compositors the buttons already show without this. */}
{isLinux && tilingWindowManager && windowControlStyle !== "native" && (
Expand All @@ -2022,6 +2024,16 @@ export function SettingsPage({
/>
)}

{/* The OS frame replaces the outline, so the switch would do nothing there. */}
{windowControlStyle !== "native" && (
<SettingToggle
title="Window outline"
description="Draw a thin edge around the window so it stands apart from what's behind it."
checked={windowOutline}
onCheckedChange={setWindowOutline}
/>
)}

{isLinux && (
<SettingToggle
title="Show in system media controls"
Expand Down
4 changes: 3 additions & 1 deletion src/ui/settings/mainWindowGeometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ async function isGeometryOnAnyMonitor(geometry: MainWindowGeometry): Promise<boo

async function saveCurrentMainWindowGeometry(): Promise<void> {
const win = getCurrentWindow();
// Inner size, because that is what `setSize` restores. Saving the outer size grew the
// window by the height of the OS frame on every launch with native decorations on.
const [position, size, isMaximized, isFullscreen] = await Promise.all([
win.outerPosition(),
win.outerSize(),
win.innerSize(),
win.isMaximized(),
win.isFullscreen(),
]);
Expand Down
17 changes: 17 additions & 0 deletions src/ui/settings/windowControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
const WINDOWS_STYLE_STORAGE_KEY = "windows-style-window-controls";
const NATIVE_CONTROLS_STORAGE_KEY = "native-window-controls";
const FORCE_CONTROLS_STORAGE_KEY = "force-window-controls-on-tiling-wm";
const WINDOW_OUTLINE_STORAGE_KEY = "window-outline-visible";
const CHANGE_EVENT = "window-controls-change";

function readBooleanSetting(key: string) {
Expand Down Expand Up @@ -48,6 +49,13 @@ function readForceWindowControls() {
return readBooleanSetting(FORCE_CONTROLS_STORAGE_KEY);
}

function readWindowOutline() {
// Default on, so the frameless window keeps the edge it has always had. Some desktops
// already separate windows well (shadows, a dark wallpaper), where the light hairline
// reads as a stray white border, so this lets it go.
return readLocalBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, true);
}

function emitWindowControlsChange() {
window.dispatchEvent(new Event(CHANGE_EVENT));
}
Expand All @@ -65,6 +73,10 @@ export function setForceWindowControls(enabled: boolean) {
writeBooleanSetting(FORCE_CONTROLS_STORAGE_KEY, enabled);
}

export function setWindowOutline(visible: boolean) {
writeBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, visible);
}

export async function applyNativeWindowControls(enabled = readNativeWindowControls()) {
try {
await getCurrentWindow().setDecorations(enabled);
Expand All @@ -87,6 +99,7 @@ export async function hydrateWindowControlSettings() {
() => applyNativeWindowControls(),
),
hydrateLocalBooleanSetting(FORCE_CONTROLS_STORAGE_KEY, false, CHANGE_EVENT),
hydrateLocalBooleanSetting(WINDOW_OUTLINE_STORAGE_KEY, true, CHANGE_EVENT),
]);
}

Expand All @@ -101,3 +114,7 @@ export function useNativeWindowControls() {
export function useForceWindowControls() {
return useSyncExternalStore(subscribe, readForceWindowControls, () => false);
}

export function useWindowOutline() {
return useSyncExternalStore(subscribe, readWindowOutline, () => true);
}