feat(ui): add cross-platform window control styles

Persist automatic and explicit macOS, Windows, GNOME, and minimal control presets with localized settings and platform-aware fallback.

Refs #31
This commit is contained in:
NimBold
2026-07-23 16:25:39 +03:30
parent 6adbccdf43
commit 4e3ceb21a5
18 changed files with 294 additions and 6 deletions
+19
View File
@@ -214,6 +214,23 @@ pub enum Theme {
Nord,
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
pub enum WindowControlStyle {
Auto,
Macos,
Windows,
Gnome,
Minimal,
}
impl Default for WindowControlStyle {
fn default() -> Self {
Self::Auto
}
}
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
#[serde(rename_all = "lowercase")]
#[ts(export, export_to = "../../src/bindings/")]
@@ -295,6 +312,8 @@ pub struct SchedulerSettings {
pub struct PersistedSettings {
pub theme: Theme,
#[serde(default)]
pub window_control_style: WindowControlStyle,
#[serde(default)]
pub calendar_preference: CalendarPreference,
#[serde(default = "default_language_preference")]
pub language: String,
+23 -1
View File
@@ -1,6 +1,6 @@
use crate::ipc::{
AppFontSize, CalendarPreference, ListRowDensity, MediaCookieSource, PersistedSettings,
PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme,
PostQueueAction, ProxyMode, SchedulerSettings, SettingsTab, Theme, WindowControlStyle,
};
use serde_json::{Map, Value};
use std::collections::HashMap;
@@ -192,6 +192,11 @@ fn sanitize_persisted_setting_values(state: &mut Value) {
"theme",
&["system", "light", "dark", "dracula", "nord"],
);
sanitize_allowed_string(
state,
"windowControlStyle",
&["auto", "macos", "windows", "gnome", "minimal"],
);
sanitize_allowed_string(
state,
"language",
@@ -417,6 +422,7 @@ fn derived_location_path(base: &str, subfolder: &str) -> String {
fn default_settings() -> PersistedSettings {
PersistedSettings {
theme: Theme::System,
window_control_style: WindowControlStyle::Auto,
calendar_preference: CalendarPreference::Gregorian,
language: "system".to_string(),
base_download_folder: "~/Downloads".to_string(),
@@ -472,6 +478,7 @@ fn default_settings() -> PersistedSettings {
#[cfg(test)]
mod tests {
use crate::ipc::WindowControlStyle;
use super::{
decode_stored_settings, default_settings, preserve_portable_pairing_token,
preserve_scheduler_runtime_keys,
@@ -678,6 +685,21 @@ mod tests {
assert_eq!(settings.sidebar_position, "auto");
}
#[test]
fn invalid_window_control_style_uses_automatic_layout() {
let stored = json!({
"state": {"windowControlStyle": "neon"},
"version": 5
});
let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap();
assert!(matches!(
settings.window_control_style,
WindowControlStyle::Auto
));
}
#[test]
fn clamps_out_of_range_download_settings() {
let stored = json!({
+7 -1
View File
@@ -20,6 +20,7 @@ import { setLogStreamActive } from './utils/logger';
import { updateDockBadge } from './utils/dockBadge';
import { openUrl } from '@tauri-apps/plugin-opener';
import { getPlatformInfo, shouldUseCustomWindowControls, usePlatformInfo } from './utils/platform';
import { resolveWindowControlStyle } from './utils/windowControlStyle';
import {
getKeychainAccessReady,
getKeychainConsentVersion,
@@ -187,6 +188,7 @@ function App() {
});
const theme = useSettingsStore(state => state.theme);
const windowControlStylePreference = useSettingsStore(state => state.windowControlStyle);
const languagePreference = useSettingsStore(state => state.language);
const isSidebarVisible = useSettingsStore(state => state.isSidebarVisible);
const sidebarPosition = useSettingsStore(state => state.sidebarPosition);
@@ -246,6 +248,7 @@ function App() {
const { addToast, removeToast } = useToast();
const isMacUserAgent = navigator.userAgent.includes('Mac');
const usesCustomWindowControls = shouldUseCustomWindowControls(platform.os, navigator.userAgent);
const windowControlStyle = resolveWindowControlStyle(windowControlStylePreference, platform.os, navigator.userAgent);
const isRtl = localeDirection(resolveAppLocale(i18n.language)) === 'rtl';
const isSidebarOnRight = sidebarPosition === 'right' || (sidebarPosition === 'auto' && isRtl);
// Keep dialogs out of the titlebar area while platform detection is still
@@ -1021,7 +1024,10 @@ function App() {
hasWindowChrome ? 'app-shell--window-chrome' : ''
}`}>
{usesCustomWindowControls && (
<WindowControls side={isSidebarOnRight ? 'right' : 'left'} />
<WindowControls
side={isSidebarOnRight ? 'right' : 'left'}
controlStyle={windowControlStyle}
/>
)}
<div
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
+2 -1
View File
@@ -8,5 +8,6 @@ import type { SchedulerSettings } from "./SchedulerSettings";
import type { SettingsTab } from "./SettingsTab";
import type { SiteLogin } from "./SiteLogin";
import type { Theme } from "./Theme";
import type { WindowControlStyle } from "./WindowControlStyle";
export type PersistedSettings = { theme: Theme, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
export type PersistedSettings = { theme: Theme, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
+3
View File
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type WindowControlStyle = "auto" | "macos" | "windows" | "gnome" | "minimal";
+27
View File
@@ -3,6 +3,7 @@ import {
type AppFontSize,
type ListRowDensity,
type SidebarPosition,
type WindowControlStyle,
type SettingsState,
SettingsTab,
runSettingsPersistenceTransaction,
@@ -698,6 +699,13 @@ runEngineChecks(false);
{ value: 'persian', label: t($ => $.settings.lookAndFeel.calendarPersian) },
{ value: 'hebrew', label: t($ => $.settings.lookAndFeel.calendarHebrew) },
] as const;
const windowControlStyleOptions: Array<{ value: WindowControlStyle; label: string }> = [
{ value: 'auto', label: t($ => $.settings.lookAndFeel.windowControlStyleAutomatic) },
{ value: 'macos', label: t($ => $.settings.lookAndFeel.windowControlStyleMacos) },
{ value: 'windows', label: t($ => $.settings.lookAndFeel.windowControlStyleWindows) },
{ value: 'gnome', label: t($ => $.settings.lookAndFeel.windowControlStyleGnome) },
{ value: 'minimal', label: t($ => $.settings.lookAndFeel.windowControlStyleMinimal) },
];
const TabButton = ({ type, icon: Icon, label }: { type: SettingsTab; icon: typeof Download; label: string }) => {
const active = activeTab === type;
@@ -907,6 +915,25 @@ runEngineChecks(false);
</div>
</div>
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.windowControls)}</h2>
<div className="mac-settings-group">
<div className="mac-settings-row">
<div className="settings-row-label">
<span>{t($ => $.settings.lookAndFeel.windowControlStyle)}</span>
<small>{t($ => $.settings.lookAndFeel.windowControlsDescription)}</small>
</div>
<select
value={settings.windowControlStyle}
onChange={(event) => settings.setWindowControlStyle(event.target.value as WindowControlStyle)}
className="app-control w-48"
>
{windowControlStyleOptions.map(option => (
<option key={option.value} value={option.value}>{option.label}</option>
))}
</select>
</div>
</div>
<h2 className="settings-section-title">{t($ => $.settings.lookAndFeel.appTheme)}</h2>
<div className="mac-settings-group">
<div className="mac-settings-row settings-choice-row">
+4 -2
View File
@@ -2,6 +2,7 @@ import { getCurrentWindow } from '@tauri-apps/api/window';
import { Maximize2, Minus, X } from 'lucide-react';
import type { PointerEvent } from 'react';
import { useTranslation } from 'react-i18next';
import type { ResolvedWindowControlStyle } from '../utils/windowControlStyle';
const appWindow = getCurrentWindow();
@@ -11,14 +12,15 @@ const stopTitlebarDrag = (event: PointerEvent<HTMLButtonElement>) => {
interface WindowControlsProps {
side: 'left' | 'right';
controlStyle: ResolvedWindowControlStyle;
}
export function WindowControls({ side }: WindowControlsProps) {
export function WindowControls({ side, controlStyle }: WindowControlsProps) {
const { t } = useTranslation();
return (
<div
className={`window-controls window-controls--${side}`}
className={`window-controls window-controls--${side} window-controls--style-${controlStyle}`}
aria-label={t($ => $.window.controls)}
>
<button
+8
View File
@@ -586,6 +586,14 @@ const common = {
sidebarPositionAutomatic: 'Automatic (recommended)',
sidebarPositionLeft: 'Left',
sidebarPositionRight: 'Right',
windowControls: 'Window controls',
windowControlStyle: 'Control style',
windowControlsDescription: 'Choose how Firelink draws its custom window buttons. Automatic follows your operating system.',
windowControlStyleAutomatic: 'Automatic (recommended)',
windowControlStyleMacos: 'macOS Traffic Lights',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: 'Minimal',
languageSystem: 'System default',
languageEnglish: 'English',
languageChinese: 'Simplified Chinese',
+8
View File
@@ -586,6 +586,14 @@ const fa = {
sidebarPositionAutomatic: 'خودکار (پیشنهادشده)',
sidebarPositionLeft: 'چپ',
sidebarPositionRight: 'راست',
windowControls: 'کنترل‌های پنجره',
windowControlStyle: 'سبک کنترل‌ها',
windowControlsDescription: 'نحوه نمایش دکمه‌های سفارشی پنجره Firelink را انتخاب کنید. حالت خودکار از سیستم‌عامل شما پیروی می‌کند.',
windowControlStyleAutomatic: 'خودکار (پیشنهادشده)',
windowControlStyleMacos: 'چراغ‌های macOS',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: 'ساده',
languageSystem: 'زبان سیستم',
languageEnglish: 'انگلیسی',
languageChinese: 'چینی ساده‌شده',
+8
View File
@@ -586,6 +586,14 @@ const he = {
sidebarPositionAutomatic: 'אוטומטי (מומלץ)',
sidebarPositionLeft: 'שמאל',
sidebarPositionRight: 'ימין',
windowControls: 'פקדי חלון',
windowControlStyle: 'סגנון פקדים',
windowControlsDescription: 'בחר כיצד Firelink מצייר את כפתורי החלון המותאמים אישית. מצב אוטומטי עוקב אחר מערכת ההפעלה שלך.',
windowControlStyleAutomatic: 'אוטומטי (מומלץ)',
windowControlStyleMacos: 'רמזורי macOS',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: 'מינימלי',
languageSystem: 'שפת המערכת',
languageEnglish: 'אנגלית',
languageChinese: 'סינית מפושטת',
+8
View File
@@ -586,6 +586,14 @@ const ru = {
sidebarPositionAutomatic: 'Автоматически (рекомендуется)',
sidebarPositionLeft: 'Слева',
sidebarPositionRight: 'Справа',
windowControls: 'Элементы управления окном',
windowControlStyle: 'Стиль элементов управления',
windowControlsDescription: 'Выберите, как Firelink отображает пользовательские кнопки окна. Автоматический режим учитывает вашу ОС.',
windowControlStyleAutomatic: 'Автоматически (рекомендуется)',
windowControlStyleMacos: 'Светофоры macOS',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: 'Минималистичный',
languageSystem: 'Системный по умолчанию',
languageEnglish: 'Английский',
languageChinese: 'Упрощённый китайский',
+8
View File
@@ -586,6 +586,14 @@ const uk = {
sidebarPositionAutomatic: 'Автоматично (рекомендовано)',
sidebarPositionLeft: 'Ліворуч',
sidebarPositionRight: 'Праворуч',
windowControls: 'Керування вікном',
windowControlStyle: 'Стиль елементів керування',
windowControlsDescription: 'Виберіть, як Firelink відображає власні кнопки вікна. Автоматичний режим враховує вашу ОС.',
windowControlStyleAutomatic: 'Автоматично (рекомендовано)',
windowControlStyleMacos: 'Світлофори macOS',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: 'Мінімальний',
languageSystem: 'Мова системи',
languageEnglish: 'Англійська',
languageChinese: 'Спрощена китайська',
+8
View File
@@ -586,6 +586,14 @@ const zhCN = {
sidebarPositionAutomatic: '自动(推荐)',
sidebarPositionLeft: '左侧',
sidebarPositionRight: '右侧',
windowControls: '窗口控件',
windowControlStyle: '控件样式',
windowControlsDescription: '选择 Firelink 绘制自定义窗口按钮的方式。“自动”会跟随操作系统。',
windowControlStyleAutomatic: '自动(推荐)',
windowControlStyleMacos: 'macOS 交通灯',
windowControlStyleWindows: 'Windows 11 Fluent',
windowControlStyleGnome: 'GNOME / Adwaita',
windowControlStyleMinimal: '极简',
languageSystem: '系统默认',
languageEnglish: '英语',
languageChinese: '简体中文',
+2
View File
@@ -94,6 +94,8 @@ describe('translation catalogs', () => {
'settings.integrations.chromiumZip',
'settings.lookAndFeel.dracula',
'settings.lookAndFeel.nord',
'settings.lookAndFeel.windowControlStyleWindows',
'settings.lookAndFeel.windowControlStyleGnome',
'settings.network.chromeWindows',
'settings.network.chromeMacos',
'settings.network.edgeWindows',
+100
View File
@@ -1783,6 +1783,106 @@ html[data-list-density="relaxed"] {
filter: brightness(0.9);
}
/* Windows 11 caption-button treatment: full-width hit targets, neutral
glyphs, and the conventional minimize/maximize/close order. */
.window-controls--style-windows {
top: 10px;
gap: 0;
flex-direction: row;
}
.window-controls--style-windows .window-control {
width: 46px;
height: 32px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
color: hsl(var(--text-primary) / 0.84);
order: initial;
}
.window-controls--style-windows .window-control.minimize {
order: 1;
}
.window-controls--style-windows .window-control.maximize {
order: 2;
}
.window-controls--style-windows .window-control.close {
order: 3;
}
.window-controls--style-windows .window-control:hover {
color: hsl(var(--text-primary));
background: hsl(var(--item-hover));
filter: none;
}
.window-controls--style-windows .window-control.close:hover {
color: white;
background: #c42b1c;
}
.window-controls--style-windows .window-control:active {
transform: none;
filter: brightness(0.9);
}
/* GNOME/Adwaita header-bar treatment: compact, borderless icon buttons. */
.window-controls--style-gnome {
top: 10px;
gap: 4px;
}
.window-controls--style-gnome .window-control,
.window-controls--style-minimal .window-control {
border: 0;
background: transparent;
box-shadow: none;
color: hsl(var(--text-primary) / 0.84);
}
.window-controls--style-gnome .window-control {
width: 32px;
height: 32px;
border-radius: 6px;
}
.window-controls--style-gnome .window-control:hover,
.window-controls--style-minimal .window-control:hover {
color: hsl(var(--text-primary));
background: hsl(var(--item-hover));
filter: none;
}
.window-controls--style-gnome .window-control.close,
.window-controls--style-gnome .window-control.minimize,
.window-controls--style-gnome .window-control.maximize,
.window-controls--style-minimal .window-control.close,
.window-controls--style-minimal .window-control.minimize,
.window-controls--style-minimal .window-control.maximize {
background: transparent;
}
/* Minimal keeps the compact footprint while removing colored circles. */
.window-controls--style-minimal {
top: 15px;
gap: 4px;
}
.window-controls--style-minimal .window-control {
width: 22px;
height: 22px;
border-radius: 6px;
}
.window-controls--style-minimal .window-control:active,
.window-controls--style-gnome .window-control:active {
filter: none;
}
.main-titlebar {
height: 52px;
display: flex;
+15 -1
View File
@@ -14,6 +14,7 @@ import type { SchedulerSettings } from '../bindings/SchedulerSettings';
import type { SettingsTab } from '../bindings/SettingsTab';
import type { SiteLogin } from '../bindings/SiteLogin';
import type { Theme } from '../bindings/Theme';
import type { WindowControlStyle } from '../bindings/WindowControlStyle';
import {
DEFAULT_CATEGORY_SUBFOLDERS,
normalizeDownloadLocationSettings
@@ -73,6 +74,7 @@ const notifySettingsPersistenceError = () => {
};
const THEME_VALUES = ['system', 'light', 'dark', 'dracula', 'nord'] as const;
const WINDOW_CONTROL_STYLE_VALUES = ['auto', 'macos', 'windows', 'gnome', 'minimal'] as const;
const APP_FONT_SIZE_VALUES = ['small', 'standard', 'large'] as const;
const LIST_ROW_DENSITY_VALUES = ['compact', 'standard', 'relaxed'] as const;
const SIDEBAR_POSITION_VALUES = ['auto', 'left', 'right'] as const;
@@ -168,13 +170,15 @@ export type {
SchedulerSettings,
SettingsTab,
SiteLogin,
Theme
Theme,
WindowControlStyle
};
export type SidebarPosition = 'auto' | 'left' | 'right';
export interface SettingsState {
theme: Theme;
windowControlStyle: WindowControlStyle;
calendarPreference: CalendarPreference;
language: AppLocalePreference;
baseDownloadFolder: string;
@@ -231,6 +235,7 @@ export interface SettingsState {
showKeychainModal: boolean;
setTheme: (theme: Theme) => void;
setWindowControlStyle: (style: WindowControlStyle) => void;
setCalendarPreference: (calendarPreference: CalendarPreference) => void;
setLanguage: (language: AppLocalePreference) => void;
setBaseDownloadFolder: (path: string) => void;
@@ -288,6 +293,7 @@ export const useSettingsStore = create<SettingsState>()(
persist(
(set, get) => ({
theme: 'system',
windowControlStyle: 'auto',
calendarPreference: DEFAULT_CALENDAR_PREFERENCE,
language: 'system',
baseDownloadFolder: '~/Downloads',
@@ -351,6 +357,10 @@ export const useSettingsStore = create<SettingsState>()(
showKeychainModal: false,
setTheme: (theme) => { info('Settings updated: theme'); set({ theme }); },
setWindowControlStyle: (windowControlStyle) => {
info('Settings updated: windowControlStyle');
set({ windowControlStyle });
},
setCalendarPreference: (calendarPreference) => {
info('Settings updated: calendarPreference');
set({ calendarPreference });
@@ -561,6 +571,7 @@ export const useSettingsStore = create<SettingsState>()(
},
partialize: (state): PersistedSettingsSnapshot => ({
theme: state.theme,
windowControlStyle: state.windowControlStyle,
calendarPreference: state.calendarPreference,
language: state.language,
baseDownloadFolder: state.baseDownloadFolder,
@@ -622,6 +633,9 @@ export const useSettingsStore = create<SettingsState>()(
theme: isAllowedSetting(THEME_VALUES, persisted.theme)
? persisted.theme
: currentState.theme,
windowControlStyle: isAllowedSetting(WINDOW_CONTROL_STYLE_VALUES, persisted.windowControlStyle)
? persisted.windowControlStyle
: currentState.windowControlStyle,
calendarPreference: isCalendarPreference(persisted.calendarPreference)
? persisted.calendarPreference
: currentState.calendarPreference,
+28
View File
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { resolveWindowControlStyle } from './windowControlStyle';
describe('resolveWindowControlStyle', () => {
it('uses the platform convention for automatic style', () => {
expect(resolveWindowControlStyle('auto', 'macos')).toBe('macos');
expect(resolveWindowControlStyle('auto', 'windows')).toBe('windows');
expect(resolveWindowControlStyle('auto', 'linux')).toBe('gnome');
});
it('falls back to macOS styling while an unsupported platform is unresolved', () => {
expect(resolveWindowControlStyle('auto', 'unknown')).toBe('macos');
expect(resolveWindowControlStyle('auto', 'android')).toBe('macos');
});
it('uses the desktop user agent while native platform detection is unresolved', () => {
expect(resolveWindowControlStyle('auto', 'unknown', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)')).toBe('windows');
expect(resolveWindowControlStyle('auto', 'unknown', 'Mozilla/5.0 (X11; Linux x86_64)')).toBe('gnome');
expect(resolveWindowControlStyle('auto', 'unknown', 'Mozilla/5.0 (Linux; Android 14)')).toBe('macos');
});
it('preserves an explicit style across platforms', () => {
expect(resolveWindowControlStyle('macos', 'windows')).toBe('macos');
expect(resolveWindowControlStyle('windows', 'linux')).toBe('windows');
expect(resolveWindowControlStyle('gnome', 'macos')).toBe('gnome');
expect(resolveWindowControlStyle('minimal', 'windows')).toBe('minimal');
});
});
+16
View File
@@ -0,0 +1,16 @@
import type { WindowControlStyle } from '../bindings/WindowControlStyle';
export type ResolvedWindowControlStyle = Exclude<WindowControlStyle, 'auto'>;
export const resolveWindowControlStyle = (
style: WindowControlStyle,
os: string,
userAgent = ''
): ResolvedWindowControlStyle => {
if (style !== 'auto') return style;
if (os === 'windows') return 'windows';
if (os === 'linux') return 'gnome';
if (os === 'unknown' && /Windows/i.test(userAgent)) return 'windows';
if (os === 'unknown' && /Linux/i.test(userAgent) && !/Android/i.test(userAgent)) return 'gnome';
return 'macos';
};