better theming, new button to open install dir, new button to view the app config

This commit is contained in:
lone-cloud 2025-09-20 22:09:00 -07:00
parent 96e432b664
commit adc015b76f
13 changed files with 248 additions and 111 deletions

View file

@ -100,7 +100,7 @@ export const UpdateAvailableModal = ({
</Text> </Text>
<Anchor <Anchor
size="xs" size="xs"
href={`https://github.com/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`} href={`${GITHUB_API.GITHUB_BASE_URL}/${GITHUB_API.KOBOLDCPP_REPO}/releases/tag/v${availableUpdate?.version}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >

View file

@ -12,11 +12,11 @@ import {
ActionIcon, ActionIcon,
Tooltip, Tooltip,
} from '@mantine/core'; } from '@mantine/core';
import { Github, FolderOpen, Copy } from 'lucide-react'; import { Github, FolderOpen, Copy, FileText } from 'lucide-react';
import { safeExecute } from '@/utils/logger'; import { safeExecute } from '@/utils/logger';
import { useLogoClickSounds } from '@/hooks/useLogoClickSounds'; import { useLogoClickSounds } from '@/hooks/useLogoClickSounds';
import type { VersionInfo } from '@/types/electron'; import type { VersionInfo } from '@/types/electron';
import { PRODUCT_NAME } from '@/constants'; import { PRODUCT_NAME, GITHUB_API } from '@/constants';
import icon from '/icon.png'; import icon from '/icon.png';
export const AboutTab = () => { export const AboutTab = () => {
@ -76,6 +76,25 @@ export const AboutTab = () => {
); );
}; };
const actionButtons = [
{
icon: Github,
label: 'GitHub',
onClick: () =>
window.electronAPI.app.openExternal(GITHUB_API.GERBIL_GITHUB_URL),
},
{
icon: FolderOpen,
label: 'Show Logs',
onClick: () => window.electronAPI.app.showLogsFolder(),
},
{
icon: FileText,
label: 'View Config',
onClick: () => window.electronAPI.app.viewConfigFile(),
},
];
return ( return (
<Stack gap="md"> <Stack gap="md">
<Card withBorder radius="md" p="xs"> <Card withBorder radius="md" p="xs">
@ -110,32 +129,24 @@ export const AboutTab = () => {
Run Large Language Models locally Run Large Language Models locally
</Text> </Text>
<Group gap="md" mt="md"> <Group gap="md" mt="md">
<Button {actionButtons.map((button) => (
variant="light" <Button
size="compact-sm" key={button.label}
leftSection={ variant="light"
<Github style={{ width: rem(16), height: rem(16) }} /> size="compact-sm"
} leftSection={
onClick={() => <button.icon style={{ width: rem(16), height: rem(16) }} />
window.electronAPI.app.openExternal( }
'https://github.com/lone-cloud/gerbil' onClick={button.onClick}
) style={
} button.label === 'GitHub'
style={{ textDecoration: 'none' }} ? { textDecoration: 'none' }
> : undefined
GitHub }
</Button> >
{button.label}
<Button </Button>
variant="light" ))}
size="compact-sm"
leftSection={
<FolderOpen style={{ width: rem(16), height: rem(16) }} />
}
onClick={() => window.electronAPI.app.showLogsFolder()}
>
Show Logs
</Button>
</Group> </Group>
</div> </div>
</Group> </Group>

View file

@ -10,7 +10,7 @@ import {
Box, Box,
Anchor, Anchor,
} from '@mantine/core'; } from '@mantine/core';
import { Folder, FolderOpen, Monitor } from 'lucide-react'; import { Folder, FolderOpen, Monitor, ExternalLink } from 'lucide-react';
import type { FrontendPreference } from '@/types'; import type { FrontendPreference } from '@/types';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { FRONTENDS } from '@/constants'; import { FRONTENDS } from '@/constants';
@ -175,6 +175,12 @@ export const GeneralTab = ({
} }
}; };
const handleOpenInstallDir = async () => {
if (installDir) {
await window.electronAPI.app.openPath(installDir);
}
};
const handleFrontendPreferenceChange = async (value: string | null) => { const handleFrontendPreferenceChange = async (value: string | null) => {
if ( if (
!value || !value ||
@ -212,6 +218,16 @@ export const GeneralTab = ({
> >
Browse Browse
</Button> </Button>
<Button
variant="outline"
onClick={handleOpenInstallDir}
disabled={!installDir}
leftSection={
<ExternalLink style={{ width: rem(16), height: rem(16) }} />
}
>
Open
</Button>
</Group> </Group>
</div> </div>

View file

@ -47,6 +47,7 @@ export const COMFYUI = {
export const GITHUB_API = { export const GITHUB_API = {
BASE_URL: 'https://api.github.com', BASE_URL: 'https://api.github.com',
GITHUB_BASE_URL: 'https://github.com',
KOBOLDCPP_REPO: 'LostRuins/koboldcpp', KOBOLDCPP_REPO: 'LostRuins/koboldcpp',
KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm', KOBOLDCPP_ROCM_REPO: 'YellowRoseCx/koboldcpp-rocm',
GERBIL_REPO: 'lone-cloud/gerbil', GERBIL_REPO: 'lone-cloud/gerbil',
@ -61,11 +62,14 @@ export const GITHUB_API = {
return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest`; return `${this.BASE_URL}/repos/${this.KOBOLDCPP_ROCM_REPO}/releases/latest`;
}, },
get COMFYUI_DOWNLOAD_URL() { get COMFYUI_DOWNLOAD_URL() {
return `https://github.com/${this.COMFYUI_REPO}/archive/refs/heads/master.zip`; return `${this.GITHUB_BASE_URL}/${this.COMFYUI_REPO}/archive/refs/heads/master.zip`;
}, },
get COMFYUI_LATEST_COMMIT_URL() { get COMFYUI_LATEST_COMMIT_URL() {
return `${this.BASE_URL}/repos/${this.COMFYUI_REPO}/commits/master`; return `${this.BASE_URL}/repos/${this.COMFYUI_REPO}/commits/master`;
}, },
get GERBIL_GITHUB_URL() {
return `${this.GITHUB_BASE_URL}/${this.GERBIL_REPO}`;
},
} as const; } as const;
export const ASSET_SUFFIXES = { export const ASSET_SUFFIXES = {

View file

@ -1,19 +1,17 @@
import { StrictMode } from 'react'; import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { MantineProvider, createTheme } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { App } from '@/components/App'; import { App } from '@/components/App';
import { theme, cssVariablesResolver } from '@/theme';
import '@/styles/index.css'; import '@/styles/index.css';
const theme = createTheme({
fontFamily: 'Inter, sans-serif',
headings: {
fontFamily: 'Inter, sans-serif',
},
});
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<MantineProvider theme={theme} defaultColorScheme="auto"> <MantineProvider
theme={theme}
cssVariablesResolver={cssVariablesResolver}
defaultColorScheme="auto"
>
<App /> <App />
</MantineProvider> </MantineProvider>
</StrictMode> </StrictMode>

View file

@ -26,6 +26,7 @@ import {
getColorScheme, getColorScheme,
setColorScheme, setColorScheme,
} from '@/main/modules/config'; } from '@/main/modules/config';
import { getConfigDir } from '@/utils/node/path';
import { logError } from '@/main/modules/logging'; import { logError } from '@/main/modules/logging';
import { safeExecute } from '@/utils/node/logger'; import { safeExecute } from '@/utils/node/logger';
import { import {
@ -200,18 +201,26 @@ export function setupIPCHandlers() {
}; };
}); });
ipcMain.handle( const openPathHandler = async (path: string) =>
'app:showLogsFolder', (await safeExecute(async () => {
async () => await shell.openPath(path);
(await safeExecute(async () => { return { success: true };
const logsDir = join(app.getPath('userData'), 'logs'); }, 'Failed to open path')) || {
await shell.openPath(logsDir); success: false,
return { success: true }; error: 'Failed to open path',
}, 'Failed to open logs folder')) || { };
success: false,
error: 'Failed to open logs folder', ipcMain.handle('app:openPath', (_, path: string) => openPathHandler(path));
}
); ipcMain.handle('app:showLogsFolder', () => {
const logsDir = join(app.getPath('userData'), 'logs');
return openPathHandler(logsDir);
});
ipcMain.handle('app:viewConfigFile', () => {
const configDir = getConfigDir();
return openPathHandler(configDir);
});
ipcMain.handle('app:minimizeWindow', () => mainWindow.minimize()); ipcMain.handle('app:minimizeWindow', () => mainWindow.minimize());

View file

@ -127,11 +127,7 @@ export function getBackgroundColor() {
export const getWindowBounds = () => config.windowBounds; export const getWindowBounds = () => config.windowBounds;
export function setWindowBounds(bounds: WindowBounds) { export function setWindowBounds(bounds: WindowBounds) {
if (bounds.isMaximized) { config.windowBounds = bounds;
config.windowBounds = { isMaximized: true } as WindowBounds;
} else {
config.windowBounds = bounds;
}
saveConfigAsync(); saveConfigAsync();
} }

View file

@ -9,7 +9,7 @@ const LINUX_PERFORMANCE_APPS = [
'ksysguard', 'ksysguard',
'htop', 'htop',
'top', 'top',
]; ] as const;
async function tryLaunchCommand(command: string, args: string[] = []) { async function tryLaunchCommand(command: string, args: string[] = []) {
return new Promise((resolve) => { return new Promise((resolve) => {

View file

@ -67,23 +67,83 @@ export function createMainWindow() {
mainWindow.maximize(); mainWindow.maximize();
} }
let restoreBounds: {
x: number;
y: number;
width: number;
height: number;
} | null = savedBounds
? {
x: savedBounds.x || 0,
y: savedBounds.y || 0,
width: savedBounds.width,
height: savedBounds.height,
}
: null;
const saveBounds = () => { const saveBounds = () => {
if (mainWindow) { if (mainWindow) {
const bounds = mainWindow.getBounds();
const isMaximized = mainWindow.isMaximized(); const isMaximized = mainWindow.isMaximized();
const currentBounds = mainWindow.getBounds();
setWindowBounds({ if (isMaximized) {
x: bounds.x, if (restoreBounds) {
y: bounds.y, setWindowBounds({
width: bounds.width, x: restoreBounds.x,
height: bounds.height, y: restoreBounds.y,
isMaximized, width: restoreBounds.width,
}); height: restoreBounds.height,
isMaximized: true,
});
}
} else {
restoreBounds = currentBounds;
setWindowBounds({
x: currentBounds.x,
y: currentBounds.y,
width: currentBounds.width,
height: currentBounds.height,
isMaximized: false,
});
}
} }
}; };
mainWindow.on('moved', saveBounds); let lastSavedBounds: string | null = null;
mainWindow.on('resized', saveBounds); let saveTimeout: ReturnType<typeof setTimeout> | null = null;
const debouncedSave = () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
saveBounds();
saveTimeout = null;
}, 1000);
};
const checkBounds = () => {
if (mainWindow && !mainWindow.isDestroyed()) {
const currentBounds = mainWindow.getBounds();
const isMaximized = mainWindow.isMaximized();
const boundsString = JSON.stringify({ ...currentBounds, isMaximized });
if (boundsString !== lastSavedBounds) {
lastSavedBounds = boundsString;
debouncedSave();
}
}
};
const boundsInterval = setInterval(checkBounds, 500);
mainWindow.on('moved', () => {
debouncedSave();
});
mainWindow.on('resized', () => {
debouncedSave();
});
mainWindow.on('maximize', () => { mainWindow.on('maximize', () => {
saveBounds(); saveBounds();
sendToRenderer('window-maximized'); sendToRenderer('window-maximized');
@ -93,6 +153,14 @@ export function createMainWindow() {
sendToRenderer('window-unmaximized'); sendToRenderer('window-unmaximized');
}); });
mainWindow.on('closed', () => {
if (saveTimeout) {
clearTimeout(saveTimeout);
}
clearInterval(boundsInterval);
mainWindow = null;
});
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
mainWindow?.show(); mainWindow?.show();
}); });

View file

@ -85,6 +85,8 @@ const koboldAPI: KoboldAPI = {
const appAPI: AppAPI = { const appAPI: AppAPI = {
showLogsFolder: () => ipcRenderer.invoke('app:showLogsFolder'), showLogsFolder: () => ipcRenderer.invoke('app:showLogsFolder'),
viewConfigFile: () => ipcRenderer.invoke('app:viewConfigFile'),
openPath: (path) => ipcRenderer.invoke('app:openPath', path),
getVersion: () => ipcRenderer.invoke('app:getVersion'), getVersion: () => ipcRenderer.invoke('app:getVersion'),
getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'), getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'),
minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'), minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'),

View file

@ -3,31 +3,6 @@
@import '@fontsource/inter/latin-500.css'; @import '@fontsource/inter/latin-500.css';
@import '@fontsource/inter/latin-600.css'; @import '@fontsource/inter/latin-600.css';
:root,
.mantine-Modal-root,
.mantine-Portal {
--mantine-color-body: #fafafa;
--mantine-color-white: #fafafa;
--mantine-color-gray-0: #f8f9fa;
--mantine-color-gray-1: #f1f3f4;
--mantine-color-gray-2: #e9ecef;
--mantine-color-gray-3: #dee2e6;
--mantine-color-default-border: #e5e7eb;
}
[data-mantine-color-scheme='dark'],
[data-mantine-color-scheme='dark'] .mantine-Modal-root,
[data-mantine-color-scheme='dark'] .mantine-Portal {
--mantine-color-body: #0f0f0f;
--mantine-color-dark-7: #1a1a1a;
--mantine-color-dark-6: #2d2d2d;
--mantine-color-dark-5: #3d3d3d;
--mantine-color-default-border: #2a2a2a;
}
#root { #root {
height: 100vh; height: 100vh;
width: 100vw; width: 100vw;
@ -125,20 +100,3 @@
transform: scale(1) rotate(0deg); transform: scale(1) rotate(0deg);
} }
} }
.mantine-Tooltip-tooltip {
background-color: var(--mantine-color-dark-6) !important;
color: var(--mantine-color-white) !important;
}
.mantine-Tooltip-arrow {
background-color: var(--mantine-color-dark-6) !important;
}
[data-mantine-color-scheme='dark'] .mantine-Modal-content {
background-color: var(--mantine-color-dark-7) !important;
}
[data-mantine-color-scheme='dark'] .mantine-Modal-header {
background-color: var(--mantine-color-dark-7) !important;
}

74
src/theme.ts Normal file
View file

@ -0,0 +1,74 @@
import { createTheme, CSSVariablesResolver } from '@mantine/core';
export const theme = createTheme({
fontFamily: 'Inter, sans-serif',
headings: {
fontFamily: 'Inter, sans-serif',
},
white: '#fafafa',
black: '#101113',
colors: {
gray: [
'#f8f9fa',
'#f1f3f4',
'#e9ecef',
'#dee2e6',
'#ced4da',
'#adb5bd',
'#6c757d',
'#495057',
'#343a40',
'#212529',
],
dark: [
'#c1c2c5',
'#a6a7ab',
'#909296',
'#5c5f66',
'#373a40',
'#2c2e33',
'#25262b',
'#1a1b1e',
'#141517',
'#101113',
],
},
components: {
Tooltip: {
styles: {
tooltip: {
backgroundColor: 'var(--mantine-color-dark-6)',
color: 'var(--mantine-color-white)',
},
arrow: {
backgroundColor: 'var(--mantine-color-dark-6)',
},
},
},
Modal: {
styles: {
content: {
backgroundColor:
'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7))',
},
header: {
backgroundColor:
'light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-7))',
},
},
},
},
});
export const cssVariablesResolver: CSSVariablesResolver = () => ({
variables: {},
light: {
'--mantine-color-body': '#fafafa',
'--mantine-color-white': '#fafafa',
'--mantine-color-default-border': '#e5e7eb',
},
dark: {
'--mantine-color-body': '#0f0f0f',
'--mantine-color-default-border': '#2a2a2a',
},
});

View file

@ -152,6 +152,8 @@ export interface VersionInfo {
export interface AppAPI { export interface AppAPI {
showLogsFolder: () => Promise<void>; showLogsFolder: () => Promise<void>;
viewConfigFile: () => Promise<void>;
openPath: (path: string) => Promise<void>;
getVersion: () => Promise<string>; getVersion: () => Promise<string>;
getVersionInfo: () => Promise<VersionInfo>; getVersionInfo: () => Promise<VersionInfo>;
minimizeWindow: () => void; minimizeWindow: () => void;
@ -165,7 +167,6 @@ export interface AppAPI {
openExternal: (url: string) => Promise<void>; openExternal: (url: string) => Promise<void>;
openPerformanceManager: () => Promise<{ openPerformanceManager: () => Promise<{
success: boolean; success: boolean;
app?: string;
error?: string; error?: string;
}>; }>;
checkForUpdates: () => Promise<boolean>; checkForUpdates: () => Promise<boolean>;