Skip to content

Commit 21ecf86

Browse files
committed
feat: full screen mode for macos
1 parent 4d67a2c commit 21ecf86

10 files changed

Lines changed: 352 additions & 94 deletions

File tree

src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"core:window:allow-minimize",
1111
"core:window:allow-maximize",
1212
"core:window:allow-unmaximize",
13-
"core:window:allow-close"
13+
"core:window:allow-close",
14+
"core:window:allow-set-fullscreen"
1415
]
1516
}

src/App.tsx

Lines changed: 15 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,31 @@
11
import { Routes, Route } from 'react-router-dom';
2-
import { Sidebar } from './components/layout/sidebar';
3-
import { Header } from './components/layout/header';
2+
import { Sidebar, Header, WindowFrame } from './components/layout';
43
import { Toaster } from './components/ui/sonner';
54
import { ThemeProvider } from './lib/theme-provider';
6-
import { Window } from '@tauri-apps/api/window';
7-
import { useEffect } from 'react';
85
import ContainersPage from './pages/ContainersPage';
96
import ImagesPage from './pages/ImagesPage';
107
import NetworksPage from './pages/NetworksPage';
118
import VolumesPage from './pages/VolumesPage';
129
import SettingsPage from './pages/SettingsPage';
1310

1411
function App() {
15-
useEffect(() => {
16-
const checkInitialState = async () => {
17-
try {
18-
const window = Window.getCurrent();
19-
const maximized = await window.isMaximized();
20-
console.log('Initial maximize state:', maximized);
21-
} catch (error) {
22-
console.error('Failed to check initial window state:', error);
23-
}
24-
};
25-
26-
checkInitialState();
27-
}, []);
28-
29-
const handleMinimize = () => {
30-
console.log('Minimize clicked');
31-
Window.getCurrent().minimize();
32-
};
33-
34-
const handleMaximize = async () => {
35-
console.log('Maximize clicked');
36-
try {
37-
const window = Window.getCurrent();
38-
const currentMaximized = await window.isMaximized();
39-
console.log('Current maximize state:', currentMaximized);
40-
41-
if (currentMaximized) {
42-
await window.unmaximize();
43-
console.log('Window unmaximized');
44-
} else {
45-
await window.maximize();
46-
console.log('Window maximized');
47-
}
48-
} catch (error) {
49-
console.error('Maximize error:', error);
50-
}
51-
};
52-
53-
const handleClose = () => {
54-
console.log('Close clicked');
55-
Window.getCurrent().close();
56-
};
57-
5812
return (
5913
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem>
60-
<div className="h-screen flex flex-col">
61-
{/* Window Controls */}
62-
<div className="window-frame h-8 flex items-center justify-between px-4 bg-background border-b border-border/50">
63-
<div className="flex items-center space-x-2">
64-
<div
65-
className="w-3 h-3 rounded-full bg-red-500 hover:bg-red-600 cursor-pointer transition-colors window-controls"
66-
onClick={handleClose}
67-
title="Close"
68-
/>
69-
<div
70-
className="w-3 h-3 rounded-full bg-yellow-500 hover:bg-yellow-600 cursor-pointer transition-colors window-controls"
71-
onClick={handleMinimize}
72-
title="Minimize"
73-
/>
74-
<div
75-
className="w-3 h-3 rounded-full bg-green-500 hover:bg-green-600 cursor-pointer transition-colors window-controls"
76-
onClick={handleMaximize}
77-
title="Maximize"
78-
/>
79-
</div>
80-
<div
81-
className="flex-1 text-center select-none"
82-
data-tauri-drag-region
83-
>
84-
<span className="text-sm font-medium text-foreground/70">
85-
Nookat
86-
</span>
87-
</div>
88-
<div className="w-12" />
89-
</div>
90-
91-
<div className="flex flex-1 min-h-0">
92-
<Sidebar />
93-
<div className="flex-1 flex flex-col min-w-0">
94-
<Header />
95-
<main className="flex-1 overflow-auto">
96-
<Routes>
97-
<Route path="/" element={<ContainersPage />} />
98-
<Route path="/images" element={<ImagesPage />} />
99-
<Route path="/networks" element={<NetworksPage />} />
100-
<Route path="/volumes" element={<VolumesPage />} />
101-
<Route path="/settings" element={<SettingsPage />} />
102-
</Routes>
103-
</main>
104-
</div>
14+
<WindowFrame>
15+
<Sidebar />
16+
<div className="flex-1 flex flex-col min-w-0">
17+
<Header />
18+
<main className="flex-1 overflow-auto">
19+
<Routes>
20+
<Route path="/" element={<ContainersPage />} />
21+
<Route path="/images" element={<ImagesPage />} />
22+
<Route path="/networks" element={<NetworksPage />} />
23+
<Route path="/volumes" element={<VolumesPage />} />
24+
<Route path="/settings" element={<SettingsPage />} />
25+
</Routes>
26+
</main>
10527
</div>
106-
</div>
28+
</WindowFrame>
10729
<Toaster />
10830
</ThemeProvider>
10931
);

src/components/layout/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Core layout components
2+
export { Header } from './header';
3+
export { Sidebar } from './sidebar';
4+
5+
// Window management components (from os-controls)
6+
export {
7+
WindowControls,
8+
OSWindowControls,
9+
WindowFrame,
10+
useWindowState,
11+
useOSDetection,
12+
OS_STYLES,
13+
type OS,
14+
} from './os-controls';
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Window management components
2+
export { WindowControls } from './window-controls';
3+
export { WindowFrame } from './window-frame';
4+
export { OSWindowControls } from './os-window-controls';
5+
6+
// Hooks
7+
export { useWindowState } from './use-window-state';
8+
export { useOSDetection } from './use-os-detection';
9+
10+
// Constants and types
11+
export { OS_STYLES, type OS } from './os-styles';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const OS_STYLES = {
2+
macos: {
3+
close: 'bg-red-500 hover:bg-red-600',
4+
minimize: 'bg-yellow-500 hover:bg-yellow-600',
5+
maximize: 'bg-green-500 hover:bg-green-600',
6+
buttonSize: 'w-3 h-3',
7+
spacing: 'space-x-2',
8+
},
9+
windows: {
10+
close: 'bg-red-500 hover:bg-red-600',
11+
minimize: 'bg-yellow-500 hover:bg-yellow-600',
12+
maximize: 'bg-green-500 hover:bg-green-600',
13+
buttonSize: 'w-3 h-3',
14+
spacing: 'space-x-2',
15+
},
16+
linux: {
17+
close: 'bg-red-500 hover:bg-red-600',
18+
minimize: 'bg-yellow-500 hover:bg-yellow-600',
19+
maximize: 'bg-green-500 hover:bg-green-600',
20+
buttonSize: 'w-3 h-3',
21+
spacing: 'space-x-2',
22+
},
23+
} as const;
24+
25+
export type OS = keyof typeof OS_STYLES;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useWindowState } from './use-window-state';
2+
import { useOSDetection } from './use-os-detection';
3+
import { OS_STYLES, type OS } from './os-styles';
4+
5+
interface OSWindowControlsProps {
6+
className?: string;
7+
os?: OS;
8+
}
9+
10+
interface WindowButtonProps {
11+
onClick: () => void;
12+
title: string;
13+
className: string;
14+
children?: React.ReactNode;
15+
}
16+
17+
const WindowButton: React.FC<WindowButtonProps> = ({
18+
onClick,
19+
title,
20+
className,
21+
children,
22+
}) => (
23+
<button
24+
className={`w-3 h-3 rounded-full transition-colors ${className}`}
25+
onClick={onClick}
26+
title={title}
27+
type="button"
28+
>
29+
{children}
30+
</button>
31+
);
32+
33+
const getOSStyles = (os: OS) => {
34+
return OS_STYLES[os] || OS_STYLES.macos;
35+
};
36+
37+
export const OSWindowControls: React.FC<OSWindowControlsProps> = ({
38+
className = '',
39+
os,
40+
}) => {
41+
const { isMaximized, minimize, maximize, close } = useWindowState();
42+
const detectedOS = useOSDetection();
43+
const currentOS = os || detectedOS;
44+
const styles = getOSStyles(currentOS);
45+
46+
const handleMinimize = () => {
47+
minimize();
48+
};
49+
50+
const handleMaximize = () => {
51+
maximize();
52+
};
53+
54+
const handleClose = () => {
55+
close();
56+
};
57+
58+
return (
59+
<div className={`flex items-center space-x-2 ${className}`}>
60+
<WindowButton
61+
onClick={handleClose}
62+
title="Close"
63+
className={`${styles.close} cursor-pointer`}
64+
/>
65+
<WindowButton
66+
onClick={handleMinimize}
67+
title="Minimize"
68+
className={`${styles.minimize} cursor-pointer`}
69+
/>
70+
<WindowButton
71+
onClick={handleMaximize}
72+
title={isMaximized ? 'Restore' : 'Maximize'}
73+
className={`${styles.maximize} cursor-pointer`}
74+
/>
75+
</div>
76+
);
77+
};
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useEffect, useState } from 'react';
2+
import { type OS } from './os-styles';
3+
4+
export const useOSDetection = (): OS => {
5+
const [os, setOS] = useState<OS>('macos');
6+
7+
useEffect(() => {
8+
const detectOS = (): OS => {
9+
// Use userAgent as a more reliable method
10+
const userAgent = navigator.userAgent.toLowerCase();
11+
12+
if (userAgent.includes('mac')) {
13+
return 'macos';
14+
} else if (userAgent.includes('win')) {
15+
return 'windows';
16+
} else if (userAgent.includes('linux')) {
17+
return 'linux';
18+
}
19+
20+
// Fallback to platform if userAgent doesn't help
21+
const platform = navigator.platform?.toLowerCase() || '';
22+
if (platform.includes('mac')) {
23+
return 'macos';
24+
} else if (platform.includes('win')) {
25+
return 'windows';
26+
} else if (platform.includes('linux')) {
27+
return 'linux';
28+
}
29+
30+
return 'macos'; // Default to macOS
31+
};
32+
33+
setOS(detectOS());
34+
}, []);
35+
36+
return os;
37+
};
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { Window } from '@tauri-apps/api/window';
2+
import { useEffect, useState } from 'react';
3+
import { useOSDetection } from './use-os-detection';
4+
5+
export const useWindowState = () => {
6+
const [isMaximized, setIsMaximized] = useState(false);
7+
const [isFullscreen, setIsFullscreen] = useState(false);
8+
const os = useOSDetection();
9+
10+
useEffect(() => {
11+
const checkInitialState = async () => {
12+
try {
13+
const window = Window.getCurrent();
14+
const maximized = await window.isMaximized();
15+
const fullscreen = await window.isFullscreen();
16+
setIsMaximized(maximized);
17+
setIsFullscreen(fullscreen);
18+
} catch (error) {
19+
console.error('Failed to check initial window state:', error);
20+
}
21+
};
22+
23+
checkInitialState();
24+
}, []);
25+
26+
const minimize = () => {
27+
Window.getCurrent().minimize();
28+
};
29+
30+
const maximize = async () => {
31+
try {
32+
const window = Window.getCurrent();
33+
34+
if (os === 'macos') {
35+
// For macOS, use fullscreen mode
36+
if (isFullscreen) {
37+
await window.setFullscreen(false);
38+
setIsFullscreen(false);
39+
} else {
40+
await window.setFullscreen(true);
41+
setIsFullscreen(true);
42+
}
43+
} else {
44+
// For other OSs (Windows, Linux), use maximize/unmaximize
45+
if (isMaximized) {
46+
await window.unmaximize();
47+
setIsMaximized(false);
48+
} else {
49+
await window.maximize();
50+
setIsMaximized(true);
51+
}
52+
}
53+
} catch (error) {
54+
console.error('Maximize error:', error);
55+
}
56+
};
57+
58+
const close = () => {
59+
Window.getCurrent().close();
60+
};
61+
62+
return {
63+
isMaximized,
64+
isFullscreen,
65+
minimize,
66+
maximize,
67+
close,
68+
};
69+
};

0 commit comments

Comments
 (0)