mirror of
https://github.com/lone-cloud/gerbil
synced 2026-06-03 19:54:44 -07:00
268 lines
8 KiB
TypeScript
268 lines
8 KiB
TypeScript
import {
|
|
Group,
|
|
ActionIcon,
|
|
Box,
|
|
Image,
|
|
Select,
|
|
useComputedColorScheme,
|
|
AppShell,
|
|
} from '@mantine/core';
|
|
import { Minus, Square, X, Copy, CircleFadingArrowUp } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { soundAssets, playSound, initializeAudio } from '@/utils/sounds';
|
|
import { useAppUpdateChecker } from '@/hooks/useAppUpdateChecker';
|
|
import { useLaunchConfigStore } from '@/stores/launchConfig';
|
|
import iconUrl from '/icon.png';
|
|
import { FRONTENDS, PRODUCT_NAME, TITLEBAR_HEIGHT } from '@/constants';
|
|
import type { FrontendPreference, InterfaceTab, Screen } from '@/types';
|
|
|
|
interface TitleBarProps {
|
|
currentScreen: Screen;
|
|
currentTab: InterfaceTab;
|
|
onTabChange: (tab: InterfaceTab) => void;
|
|
onEject: () => void;
|
|
frontendPreference: FrontendPreference;
|
|
}
|
|
|
|
export const TitleBar = ({
|
|
currentScreen,
|
|
currentTab,
|
|
onTabChange,
|
|
onEject,
|
|
frontendPreference,
|
|
}: TitleBarProps) => {
|
|
const computedColorScheme = useComputedColorScheme('light', {
|
|
getInitialValueInEffect: true,
|
|
});
|
|
const { hasUpdate, releaseUrl } = useAppUpdateChecker();
|
|
const { isImageGenerationMode } = useLaunchConfigStore();
|
|
const [logoClickCount, setLogoClickCount] = useState(0);
|
|
const [isElephantMode, setIsElephantMode] = useState(false);
|
|
const [isMouseSqueaking, setIsMouseSqueaking] = useState(false);
|
|
const [isMaximized, setIsMaximized] = useState(false);
|
|
const [isSelectOpen, setIsSelectOpen] = useState(false);
|
|
|
|
const handleMinimize = () => {
|
|
window.electronAPI.app.minimizeWindow();
|
|
};
|
|
|
|
const handleMaximize = async () => {
|
|
await window.electronAPI.app.maximizeWindow();
|
|
setIsMaximized(!isMaximized);
|
|
};
|
|
|
|
const handleClose = () => {
|
|
window.electronAPI.app.closeWindow();
|
|
};
|
|
|
|
const handleLogoClick = async () => {
|
|
await initializeAudio();
|
|
setLogoClickCount((prev) => prev + 1);
|
|
|
|
try {
|
|
if (logoClickCount >= 10 && Math.random() < 0.1) {
|
|
setIsElephantMode(true);
|
|
await playSound(soundAssets.elephant, 0.6);
|
|
|
|
setTimeout(() => {
|
|
setIsElephantMode(false);
|
|
}, 1500);
|
|
} else {
|
|
setIsMouseSqueaking(true);
|
|
const squeakNumber = Math.floor(Math.random() * 5);
|
|
await playSound(soundAssets.mouseSqueaks[squeakNumber], 0.4);
|
|
|
|
setTimeout(() => {
|
|
setIsMouseSqueaking(false);
|
|
}, 300);
|
|
}
|
|
} catch {
|
|
void 0;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AppShell.Header style={{ display: 'flex', flexDirection: 'column' }}>
|
|
<Box
|
|
style={{
|
|
height: TITLEBAR_HEIGHT,
|
|
padding: '0.125rem 0 0.125rem 0.5rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
backgroundColor:
|
|
computedColorScheme === 'dark'
|
|
? 'var(--mantine-color-dark-8)'
|
|
: 'var(--mantine-color-gray-1)',
|
|
borderBottom: '1px solid var(--mantine-color-default-border)',
|
|
WebkitAppRegion: isSelectOpen ? 'no-drag' : 'drag',
|
|
userSelect: 'none',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<Group
|
|
gap="0.5rem"
|
|
align="center"
|
|
style={{ WebkitAppRegion: 'no-drag' }}
|
|
>
|
|
<Image
|
|
src={iconUrl}
|
|
alt={PRODUCT_NAME}
|
|
w={24}
|
|
h={24}
|
|
style={{
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
transition: 'transform 0.15s ease-in-out',
|
|
transform: isElephantMode
|
|
? 'scale(1.3) rotate(5deg)'
|
|
: 'scale(1) rotate(0deg)',
|
|
animation: isElephantMode
|
|
? 'elephantShake 1.5s ease-in-out'
|
|
: isMouseSqueaking
|
|
? 'mouseSqueak 0.3s ease-in-out'
|
|
: 'none',
|
|
}}
|
|
onClick={handleLogoClick}
|
|
/>
|
|
</Group>
|
|
|
|
<Box
|
|
style={{
|
|
position: 'absolute',
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
WebkitAppRegion: 'no-drag',
|
|
}}
|
|
>
|
|
{currentScreen === 'interface' && (
|
|
<Select
|
|
placeholder="Interface"
|
|
value={currentTab}
|
|
onChange={(value) => {
|
|
if (value === 'eject') {
|
|
onEject();
|
|
} else {
|
|
onTabChange(value as InterfaceTab);
|
|
}
|
|
}}
|
|
onDropdownOpen={() => setIsSelectOpen(true)}
|
|
onDropdownClose={() => setIsSelectOpen(false)}
|
|
data={[
|
|
{
|
|
value: 'chat',
|
|
label: (() => {
|
|
if (frontendPreference === 'sillytavern') {
|
|
return FRONTENDS.SILLYTAVERN;
|
|
}
|
|
if (
|
|
frontendPreference === 'openwebui' &&
|
|
!isImageGenerationMode
|
|
) {
|
|
return FRONTENDS.OPENWEBUI;
|
|
}
|
|
return isImageGenerationMode
|
|
? FRONTENDS.STABLE_UI
|
|
: FRONTENDS.KOBOLDAI_LITE;
|
|
})(),
|
|
},
|
|
{ value: 'terminal', label: 'Terminal' },
|
|
{ value: 'eject', label: 'Eject' },
|
|
]}
|
|
renderOption={({ option }) => (
|
|
<Box
|
|
style={{
|
|
textAlign: 'center',
|
|
color:
|
|
option.value === 'eject'
|
|
? 'var(--mantine-color-red-6)'
|
|
: undefined,
|
|
fontWeight: option.value === 'eject' ? 600 : undefined,
|
|
}}
|
|
>
|
|
{option.label}
|
|
</Box>
|
|
)}
|
|
allowDeselect={false}
|
|
variant="unstyled"
|
|
size="sm"
|
|
style={{
|
|
textAlign: 'center',
|
|
minWidth: '7.5rem',
|
|
}}
|
|
styles={{
|
|
input: {
|
|
textAlign: 'center',
|
|
backgroundColor: 'transparent',
|
|
border: 'none',
|
|
fontWeight: 500,
|
|
userSelect: 'none',
|
|
cursor: 'pointer',
|
|
},
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
|
|
<Group gap="0" style={{ WebkitAppRegion: 'no-drag' }}>
|
|
{hasUpdate && releaseUrl && (
|
|
<ActionIcon
|
|
component="a"
|
|
href={releaseUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
variant="subtle"
|
|
color="orange"
|
|
size={TITLEBAR_HEIGHT}
|
|
aria-label="New release available"
|
|
tabIndex={-1}
|
|
style={{
|
|
borderRadius: 0,
|
|
margin: 0,
|
|
}}
|
|
>
|
|
<CircleFadingArrowUp size="1.25rem" />
|
|
</ActionIcon>
|
|
)}
|
|
|
|
{[
|
|
{
|
|
icon: <Minus size="1rem" />,
|
|
onClick: handleMinimize,
|
|
color: undefined,
|
|
label: 'Minimize window',
|
|
},
|
|
{
|
|
icon: isMaximized ? <Copy size="1rem" /> : <Square size="1rem" />,
|
|
onClick: handleMaximize,
|
|
color: undefined,
|
|
label: isMaximized ? 'Restore window' : 'Maximize window',
|
|
},
|
|
{
|
|
icon: <X size="1.25rem" />,
|
|
onClick: handleClose,
|
|
color: 'red' as const,
|
|
label: 'Close window',
|
|
},
|
|
].map((button, index) => (
|
|
<ActionIcon
|
|
key={index}
|
|
variant="subtle"
|
|
size={TITLEBAR_HEIGHT}
|
|
onClick={button.onClick}
|
|
color={button.color}
|
|
aria-label={button.label}
|
|
tabIndex={-1}
|
|
style={{
|
|
borderRadius: 0,
|
|
margin: 0,
|
|
}}
|
|
>
|
|
{button.icon}
|
|
</ActionIcon>
|
|
))}
|
|
</Group>
|
|
</Box>
|
|
</AppShell.Header>
|
|
);
|
|
};
|