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
80 changes: 79 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-shadow */
import { app, ipcMain, globalShortcut, desktopCapturer } from "electron";
import { app, ipcMain, globalShortcut, desktopCapturer, session } from "electron";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { WindowManager } from "./window-manager";
import { MenuManager } from "./menu-manager";
Expand All @@ -8,6 +8,57 @@ let windowManager: WindowManager;
let menuManager: MenuManager;
let isQuitting = false;

interface BackendAuthPayload {
baseUrl: string;
basicAuth: {
enabled: boolean;
username: string;
password: string;
};
}

interface BackendAuthState {
hostname: string;
port: string;
header?: string;
}

const getDefaultPort = (protocol: string) => (protocol === 'https:' || protocol === 'wss:' ? '443' : '80');

let backendAuthState: BackendAuthState | null = null;

const matchesBackend = (targetUrl: string) => {
if (!backendAuthState) return false;
try {
const parsed = new URL(targetUrl);
const port = parsed.port || getDefaultPort(parsed.protocol);
return parsed.hostname === backendAuthState.hostname && port === backendAuthState.port;
} catch (error) {
return false;
}
};

const updateBackendAuthState = (payload: BackendAuthPayload) => {
if (!payload?.baseUrl) {
backendAuthState = null;
return;
}

try {
const parsed = new URL(payload.baseUrl);
const port = parsed.port || getDefaultPort(parsed.protocol);
backendAuthState = {
hostname: parsed.hostname,
port,
header: payload.basicAuth?.enabled
? `Basic ${Buffer.from(`${payload.basicAuth.username}:${payload.basicAuth.password}`, 'utf-8').toString('base64')}`
: undefined,
};
} catch (error) {
backendAuthState = null;
}
};

function setupIPC(): void {
ipcMain.handle("get-platform", () => process.platform);

Expand Down Expand Up @@ -71,6 +122,32 @@ function setupIPC(): void {
const sources = await desktopCapturer.getSources({ types: ['screen'] });
return sources[0].id;
});

ipcMain.on('update-backend-auth', (_event, payload: BackendAuthPayload) => {
updateBackendAuthState(payload);
});
}

function setupBackendAuthInterceptor(): void {
const defaultSession = session?.defaultSession;
if (!defaultSession) {
return;
}

defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
if (backendAuthState && matchesBackend(details.url)) {
const requestHeaders = { ...details.requestHeaders };
if (backendAuthState.header) {
requestHeaders.Authorization = backendAuthState.header;
} else {
delete requestHeaders.Authorization;
}
callback({ requestHeaders });
return;
}

callback({ requestHeaders: details.requestHeaders });
});
}

app.whenReady().then(() => {
Expand Down Expand Up @@ -110,6 +187,7 @@ app.whenReady().then(() => {
// }

setupIPC();
setupBackendAuthInterceptor();

app.on("activate", () => {
const window = windowManager.getWindow();
Expand Down
4 changes: 4 additions & 0 deletions src/preload/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ declare global {
setMode: (mode: 'window' | 'pet') => void
getConfigFiles: () => Promise<any>
updateConfigFiles: (files: any[]) => void
updateBackendAuth: (config: {
baseUrl: string
basicAuth: { enabled: boolean; username: string; password: string }
}) => void
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ const api = {
updateConfigFiles: (files: ConfigFile[]) => {
ipcRenderer.send('update-config-files', files);
},
updateBackendAuth: (config: {
baseUrl: string
basicAuth: { enabled: boolean; username: string; password: string }
}) => {
ipcRenderer.send('update-backend-auth', config);
},
};

if (process.contextIsolated) {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/canvas/background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ const Background = memo(({ children }: { children?: React.ReactNode }) => {

Background.displayName = 'Background';

export default Background;
export default Background;
9 changes: 9 additions & 0 deletions src/renderer/src/components/sidebar/setting/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ interface InputFieldProps {
onChange: (value: string) => void
placeholder?: string
help?: string
error?: string
disabled?: boolean
type?: string
}

// Reusable Components
Expand Down Expand Up @@ -187,6 +190,9 @@ export function InputField({
onChange,
placeholder,
help,
error,
disabled = false,
type = 'text',
}: InputFieldProps): JSX.Element {
return (
<Field
Expand All @@ -197,12 +203,15 @@ export function InputField({
{help && <HelpIcon content={help} />}
</Flex>
}
errorText={error}
>
<Input
{...settingStyles.general.input}
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
type={type}
disabled={disabled}
/>
</Field>
);
Expand Down
83 changes: 76 additions & 7 deletions src/renderer/src/components/sidebar/setting/general.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useWebSocket } from "@/context/websocket-context";
import { SelectField, SwitchField, InputField } from "./common";

interface GeneralProps {
onSave?: (callback: () => void) => () => void;
onSave?: (callback: () => boolean | void) => () => void;
onCancel?: (callback: () => void) => () => void;
}

Expand All @@ -26,11 +26,37 @@ const useCollections = () => {
});

const backgrounds = createListCollection({
items:
backgroundFiles?.map((filename) => ({
label: String(filename),
value: `/bg/${filename}`,
})) || [],
items: (backgroundFiles ?? [])
.map((raw) => {
const entry = raw as unknown as { name?: string; url?: string } | string | null | undefined;
if (entry == null) {
return null;
}
if (typeof entry === 'string') {
const trimmed = entry.trim();
if (!trimmed) {
return null;
}
const label = trimmed.split('/').filter(Boolean).pop() ?? trimmed;
return {
label: label || trimmed,
value: trimmed,
};
}
const name = entry.name?.trim();
const url = entry.url?.trim();
const fallbackValue = name || url || '';
if (!fallbackValue) {
return null;
}
const labelSource = name || url;
const label = labelSource?.split('/').filter(Boolean).pop() ?? labelSource;
return {
label: label || fallbackValue,
value: name || fallbackValue,
};
})
.filter((item): item is { label: string; value: string } => Boolean(item?.value)),
});

const characterPresets = createListCollection({
Expand All @@ -51,7 +77,18 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element {
const { t, i18n } = useTranslation();
const bgUrlContext = useBgUrl();
const { confName, setConfName } = useConfig();
const { wsUrl, setWsUrl, baseUrl, setBaseUrl } = useWebSocket();
const {
wsUrl,
setWsUrl,
baseUrl,
setBaseUrl,
basicAuthEnabled,
setBasicAuthEnabled,
basicAuthUsername,
setBasicAuthUsername,
basicAuthPassword,
setBasicAuthPassword,
} = useWebSocket();
const collections = useCollections();

const {
Expand All @@ -61,6 +98,7 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element {
handleCharacterPresetChange,
showSubtitle,
setShowSubtitle,
basicAuthErrors,
} = useGeneralSettings({
bgUrlContext,
confName,
Expand All @@ -71,6 +109,12 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element {
onBaseUrlChange: setBaseUrl,
onSave,
onCancel,
basicAuthEnabled,
basicAuthUsername,
basicAuthPassword,
onBasicAuthEnabledChange: setBasicAuthEnabled,
onBasicAuthUsernameChange: setBasicAuthUsername,
onBasicAuthPasswordChange: setBasicAuthPassword,
});

if (settings.language[0] !== i18n.language) {
Expand Down Expand Up @@ -140,6 +184,31 @@ function General({ onSave, onCancel }: GeneralProps): JSX.Element {
placeholder="Enter Base URL"
/>

<SwitchField
label={t("settings.general.basicAuthEnabled")}
checked={settings.basicAuthEnabled}
onChange={(checked) => handleSettingChange("basicAuthEnabled", checked)}
/>

<InputField
label={t("settings.general.basicAuthUsername")}
value={settings.basicAuthUsername}
onChange={(value) => handleSettingChange("basicAuthUsername", value)}
placeholder={t("settings.general.basicAuthUsernamePlaceholder")}
disabled={!settings.basicAuthEnabled}
error={settings.basicAuthEnabled ? basicAuthErrors.username : undefined}
/>

<InputField
label={t("settings.general.basicAuthPassword")}
value={settings.basicAuthPassword}
onChange={(value) => handleSettingChange("basicAuthPassword", value)}
placeholder={t("settings.general.basicAuthPasswordPlaceholder")}
disabled={!settings.basicAuthEnabled}
error={settings.basicAuthEnabled ? basicAuthErrors.password : undefined}
type="password"
/>

<InputField
label={t("settings.general.imageCompressionQuality")}
value={settings.imageCompressionQuality.toString()}
Expand Down
23 changes: 18 additions & 5 deletions src/renderer/src/components/sidebar/setting/setting-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ interface SettingUIProps {

function SettingUI({ open, onClose }: SettingUIProps): JSX.Element {
const { t } = useTranslation();
const [saveHandlers, setSaveHandlers] = useState<(() => void)[]>([]);
const [saveHandlers, setSaveHandlers] = useState<(() => boolean | void | Promise<boolean | void>)[]>([]);
const [cancelHandlers, setCancelHandlers] = useState<(() => void)[]>([]);
const [activeTab, setActiveTab] = useState('general');

const handleSaveCallback = useCallback((handler: () => void) => {
const handleSaveCallback = useCallback((handler: () => boolean | void | Promise<boolean | void>) => {
setSaveHandlers((prev) => [...prev, handler]);
return (): void => {
setSaveHandlers((prev) => prev.filter((h) => h !== handler));
Expand All @@ -49,9 +49,22 @@ function SettingUI({ open, onClose }: SettingUIProps): JSX.Element {
};
}, []);

const handleSave = useCallback((): void => {
saveHandlers.forEach((handler) => handler());
onClose();
const handleSave = useCallback(async (): Promise<void> => {
let hasError = false;
for (const handler of saveHandlers) {
try {
const result = await handler();
if (result === false) {
hasError = true;
}
} catch (error) {
console.error('Settings save handler failed', error);
hasError = true;
}
}
if (!hasError) {
onClose();
}
}, [saveHandlers, onClose]);

const handleCancel = useCallback((): void => {
Expand Down
14 changes: 13 additions & 1 deletion src/renderer/src/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Box, Button, Menu } from '@chakra-ui/react';
import {
FiSettings, FiClock, FiPlus, FiChevronLeft, FiUsers, FiLayers
} from 'react-icons/fi';
import { memo } from 'react';
import { memo, useEffect } from 'react';
import { sidebarStyles } from './sidebar-styles';
import SettingUI from './setting/setting-ui';
import ChatHistoryPanel from './chat-history-panel';
Expand All @@ -12,6 +12,7 @@ import HistoryDrawer from './history-drawer';
import { useSidebar } from '@/hooks/sidebar/use-sidebar';
import GroupDrawer from './group-drawer';
import { ModeType } from '@/context/mode-context';
import { OPEN_SETTINGS_EVENT } from '@/constants/events';

// Type definitions
interface SidebarProps {
Expand Down Expand Up @@ -146,6 +147,17 @@ function Sidebar({ isCollapsed = false, onToggle }: SidebarProps): JSX.Element {
isElectron,
} = useSidebar();

useEffect(() => {
const handleOpenSettings = () => {
onSettingsOpen();
};

window.addEventListener(OPEN_SETTINGS_EVENT, handleOpenSettings);
return () => {
window.removeEventListener(OPEN_SETTINGS_EVENT, handleOpenSettings);
};
}, [onSettingsOpen]);

return (
<Box {...sidebarStyles.sidebar.container(isCollapsed)}>
<ToggleButton isCollapsed={isCollapsed} onToggle={onToggle} />
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/src/constants/backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const DEFAULT_BACKEND_WS_URL = 'ws://127.0.0.1:12393/client-ws';
export const DEFAULT_BACKEND_BASE_URL = 'http://127.0.0.1:12393';
export const DEFAULT_BASIC_AUTH_USERNAME = 'admin';
export const DEFAULT_BASIC_AUTH_PASSWORD = 'change-me';
3 changes: 3 additions & 0 deletions src/renderer/src/constants/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const OPEN_SETTINGS_EVENT = 'app-open-settings';
export const AUTH_STATUS_EVENT = 'app-auth-status';

Loading