Compare commits

..

27 Commits

Author SHA1 Message Date
Tim Jaeryang Baek b83f790e75 Merge pull request #36 from open-webui/release
chore: actions
2025-07-29 16:17:07 +04:00
Timothy Jaeryang Baek 67c83f328d chore: actions 2025-07-29 16:16:51 +04:00
Timothy Jaeryang Baek 2a644345d4 chore: build 2025-07-29 16:11:08 +04:00
Timothy Jaeryang Baek 3f8fe6cfe1 doc: readme 2025-07-29 16:00:35 +04:00
Timothy Jaeryang Baek f2db94ed13 refac: wording 2025-07-29 15:50:24 +04:00
Timothy Jaeryang Baek db2e170291 refac 2025-07-29 15:32:38 +04:00
Timothy Jaeryang Baek 32224983cf refac 2025-07-29 15:27:40 +04:00
Timothy Jaeryang Baek cf011d4642 refac 2025-07-29 15:21:28 +04:00
Timothy Jaeryang Baek 96388c45c6 refac 2025-07-29 15:04:15 +04:00
Timothy Jaeryang Baek db1b59a813 refac 2025-07-29 14:41:33 +04:00
Timothy Jaeryang Baek 6d4ffea851 refac 2025-07-29 14:33:52 +04:00
Timothy Jaeryang Baek 238cce6dc8 refac 2025-07-29 14:31:03 +04:00
Timothy Jaeryang Baek b255f22fe7 refac 2025-07-29 14:30:09 +04:00
Timothy Jaeryang Baek d205c176ef refac 2025-07-29 14:19:48 +04:00
Timothy Jaeryang Baek 2a223e62b6 refac 2025-07-29 14:16:54 +04:00
Timothy Jaeryang Baek eb2ebd2c1d refac 2025-07-29 14:15:34 +04:00
Timothy Jaeryang Baek a6a944c847 refac 2025-07-29 14:14:34 +04:00
Timothy Jaeryang Baek 03fea4a7cc refac 2025-07-29 14:09:52 +04:00
Timothy Jaeryang Baek 6b159246d1 refac: styling 2025-07-29 13:44:42 +04:00
Timothy Jaeryang Baek 398dc3128e refac 2025-07-29 13:39:05 +04:00
Timothy Jaeryang Baek d991908205 refac 2025-07-29 13:34:49 +04:00
Timothy Jaeryang Baek 2528096e07 fix: windows utf-8 issue 2025-07-29 13:32:38 +04:00
Timothy Jaeryang Baek aeba0e7c22 refac: logging 2025-07-29 13:23:30 +04:00
Timothy Jaeryang Baek ffbbb8f5b2 refac 2025-07-29 13:14:06 +04:00
Timothy Jaeryang Baek 119409a2dc refac 2025-07-29 13:08:39 +04:00
Timothy Jaeryang Baek cb564555dc refac 2025-07-29 12:54:24 +04:00
Timothy Jaeryang Baek 00d739d0d0 refac 2025-07-29 12:51:54 +04:00
12 changed files with 382 additions and 142 deletions
+4 -4
View File
@@ -3,7 +3,7 @@ name: Build and Release Electron App (electron-builder)
on:
push:
branches:
- main
- release
pull_request:
jobs:
@@ -13,8 +13,8 @@ jobs:
strategy:
matrix:
include:
# - os: ubuntu-latest
# arch: x64
- os: ubuntu-latest
arch: x64
# - os: ubuntu-latest
# arch: arm64
- os: windows-latest
@@ -150,7 +150,7 @@ jobs:
release:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: github.event_name == 'push' && github.ref == 'refs/heads/release'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
+12 -27
View File
@@ -1,53 +1,38 @@
# Open WebUI Desktop 🌐
![App Demo](./demo.png)
**Open WebUI App** is the cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It brings the _full-featured Open WebUI experience_ directly to your device, effectively transforming it into a powerful server—without the complexities of manual setup.
**Open WebUI App** is the upcoming cross-platform desktop application for [Open WebUI](https://github.com/open-webui/open-webui). It brings the *full-featured Open WebUI experience* directly to your device, effectively transforming it into a powerful server—without the complexities of manual setup.
This project is currently in **alpha** and under active development. 🛠️ Expect frequent updates and potential changes as we refine the application.
This project is still in an **experimental phase** and under active development. 🛠️ Expect frequent updates and potential changes as we refine the application.
## Download 📥
Get the latest alpha release from our [releases page](https://github.com/open-webui/desktop/releases).
---
**Note**: An internet connection is required for initial setup, but afterwards the application can be used completely offline.
## Features
- **One-Click Installation**: Quickly and effortlessly install and set up Open WebUI with all its dependencies. This feature is fully functional and ready to make your setup a breeze.
- **Cross-Platform Support**: Compatible with Windows, macOS, and Linux to ensure broad accessibility.
---
- **Offline Capability**: After initial setup, use the application completely offline for enhanced privacy and reliability.
## Project Setup
### Install
```bash
$ npm install
npm install
```
### Development
```bash
$ npm run dev
npm run dev
```
### Build
```bash
# For windows
$ npm run build:win
npm run build:win
# For macOS
$ npm run build:mac
npm run build:mac
# For Linux
$ npm run build:linux
npm run build:linux
```
---
## License 📜
This project is licensed under the **Open WebUI Sustainable Use License**. For details, see [LICENSE](LICENSE).
---
## Stay Tuned! 🌟
We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved.
We're actively developing Open WebUI App. Follow [Open WebUI](https://github.com/open-webui/open-webui) for updates, and join the [community on Discord](https://discord.gg/5rJgQTnV4s) to stay involved.
+60 -13
View File
@@ -16,12 +16,9 @@ import {
} from "electron";
import path, { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import log from "electron-log";
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
import {
getLogFilePath,
checkUrlAndOpen,
getConfig,
getServerLog,
@@ -38,6 +35,12 @@ import {
uninstallPython,
} from "./utils";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
import icon from "../../resources/icon.png?asset";
import trayIconImage from "../../resources/assets/tray.png?asset";
// Main application logic
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
@@ -58,14 +61,16 @@ function createWindow(show = true): void {
minHeight: 400,
icon: path.join(__dirname, "assets/icon.png"),
show: false,
titleBarStyle: process.platform === "win32" ? "default" : "hidden",
trafficLightPosition: { x: 16, y: 16 },
autoHideMenuBar: true,
...(process.platform === "win32"
? {
frame: false,
frame: true,
}
: {}),
...(process.platform === "linux" ? { icon } : {}),
titleBarStyle: process.platform === "win32" ? "default" : "hidden",
trafficLightPosition: { x: 16, y: 16 },
...(process.platform !== "darwin" ? { titleBarOverlay: true } : {}),
webPreferences: {
preload: join(__dirname, "../preload/index.js"),
sandbox: false,
@@ -122,6 +127,13 @@ function createWindow(show = true): void {
uninstallHandler();
},
},
{
label: "Reset",
click: async () => {
await resetAppHandler();
},
},
],
});
const updatedMenu = Menu.buildFromTemplate(menuTemplate);
@@ -241,7 +253,7 @@ const uninstallHandler = async () => {
});
notification.show();
} catch (error) {
console.error("Uninstallation failed:", error);
log.error("Uninstallation failed:", error);
// Show error notification
const notification = new Notification({
title: "Open WebUI",
@@ -295,7 +307,7 @@ const startServerHandler = async () => {
// Show system notification
const notification = new Notification({
title: "Open WebUI",
body: "Server is now available and opened in your browser",
body: "Open WebUI is now available and opened in your browser",
});
notification.show();
@@ -307,7 +319,7 @@ const startServerHandler = async () => {
return true; // Indicate success
} catch (error) {
console.error("Failed to start server:", error);
log.error("Failed to start server:", error);
SERVER_STATUS = "failed";
mainWindow?.webContents.send("main:data", {
type: "status:server",
@@ -343,11 +355,38 @@ const stopServerHandler = async () => {
return true; // Indicate success
} catch (error) {
console.error("Failed to stop server:", error);
log.error("Failed to stop server:", error);
return false; // Indicate failure
}
};
const resetAppHandler = async () => {
try {
await stopServerHandler(); // Stop the server if running
SERVER_STATUS = null;
// wait a moment to ensure all processes are stopped
await new Promise((resolve) => setTimeout(resolve, 1000));
await resetApp(); // Reset the application state
// Show success notification
const notification = new Notification({
title: "Open WebUI",
body: "Application has been reset successfully.",
});
notification.show();
} catch (error) {
log.error("Failed to reset application:", error);
// Show error notification
const notification = new Notification({
title: "Open WebUI",
body: `Failed to reset application: ${error.message}`,
});
notification.show();
}
};
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit(); // Quit if another instance is already running
@@ -484,8 +523,16 @@ if (!gotTheLock) {
return SERVER_STATUS;
});
ipcMain.handle("app:info", async (event) => {
return {
version: app.getVersion(),
platform: process.platform,
arch: process.arch,
};
});
ipcMain.handle("app:reset", async (event) => {
return await resetApp();
return await resetAppHandler();
});
ipcMain.handle("get:config", async (event) => {
@@ -528,7 +575,7 @@ if (!gotTheLock) {
);
await installPackage("open-webui");
} catch (error) {
console.error("Failed to update package:", error);
log.error("Failed to update package:", error);
}
}
+226 -67
View File
@@ -12,6 +12,18 @@ import { app, shell, Notification } from "electron";
import { execFileSync, exec, spawn, execSync, execFile } from "child_process";
import log from "electron-log";
log.transports.file.resolvePathFn = () => getLogFilePath("main");
const serverLogger = log.create({ logId: "server" });
serverLogger.transports.file.resolvePath = () => getLogFilePath(`server`);
export const getLogFilePath = (name: string = "main"): string => {
const logDir = path.join(getUserDataPath(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
return path.join(logDir, `${name}.log`);
};
export const getAppPath = (): string => {
let appPath = app.getAppPath();
@@ -219,7 +231,7 @@ export const downloadFileWithProgress = async (
return downloadPath;
}
} catch (error) {
console.error("Download failed:", error);
log.error("Download failed:", error);
throw error;
}
};
@@ -237,7 +249,7 @@ export const getPythonDownloadPath = (): string => {
return downloadPath;
};
export const getPythonInstallationPath = (): string => {
export const getPythonInstallationDir = (): string => {
const installDir = path.join(app.getPath("userData"), "python");
if (!fs.existsSync(installDir)) {
@@ -276,7 +288,7 @@ const downloadPython = async (onProgress = null) => {
log.info(`✅ Python downloaded successfully to: ${result}`);
return result;
} catch (error) {
console.error(`❌ Download failed: ${error?.message}`);
log.error(`❌ Download failed: ${error?.message}`);
throw error;
}
};
@@ -299,7 +311,7 @@ const checkInternet = async () => {
};
export const installPython = async (
installationPath?: string
installationDir?: string
): Promise<boolean> => {
let pythonDownloadPath = getPythonDownloadPath();
if (!isPythonDownloaded()) {
@@ -314,9 +326,6 @@ export const installPython = async (
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
log.info(
`Downloading Python: ${progress.toFixed(2)}% (${downloaded} of ${total} bytes)`
);
});
}
if (!fs.existsSync(pythonDownloadPath)) {
@@ -324,8 +333,8 @@ export const installPython = async (
return false;
}
installationPath = installationPath || getPythonInstallationPath();
log.info(installationPath, pythonDownloadPath);
installationDir = installationDir || getPythonInstallationDir();
log.info(installationDir, pythonDownloadPath);
try {
const userDataPath = getUserDataPath();
@@ -339,13 +348,16 @@ export const installPython = async (
}
// Get the path to the installed Python binary
if (isPythonInstalled(installationPath)) {
const pythonPath = getPythonPath(installationPath);
if (isPythonInstalled(installationDir)) {
const pythonPath = getPythonPath(installationDir);
execFileSync(pythonPath, ["-m", "pip", "install", "uv"], {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
log.info("Successfully installed uv package");
@@ -367,14 +379,14 @@ export const getPythonExecutablePath = (envPath: string) => {
}
};
export const getPythonPath = (installationPath?: string) => {
export const getPythonPath = (installationDir?: string) => {
return path.normalize(
getPythonExecutablePath(installationPath || getPythonInstallationPath())
getPythonExecutablePath(installationDir || getPythonInstallationDir())
);
};
export const isPythonInstalled = (installationPath?: string) => {
const pythonPath = getPythonPath(installationPath);
export const isPythonInstalled = (installationDir?: string) => {
const pythonPath = getPythonPath(installationDir);
if (!fs.existsSync(pythonPath)) {
log.error("Python binary not found in install path");
@@ -387,6 +399,9 @@ export const isPythonInstalled = (installationPath?: string) => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
log.info("Installed Python Version:", pythonVersion.trim());
@@ -398,14 +413,17 @@ export const isPythonInstalled = (installationPath?: string) => {
}
};
export const isUvInstalled = (installationPath?: string) => {
const pythonPath = getPythonPath(installationPath);
export const isUvInstalled = (installationDir?: string) => {
const pythonPath = getPythonPath(installationDir);
try {
// Check if uv is installed by running the command
const result = execFileSync(pythonPath, ["-m", "uv", "--version"], {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
@@ -420,16 +438,17 @@ export const isUvInstalled = (installationPath?: string) => {
}
};
export const uninstallPython = (installationPath?: string): boolean => {
installationPath = installationPath || getPythonInstallationPath();
export const uninstallPython = (installationDir?: string): boolean => {
installationDir = installationDir || getPythonInstallationDir();
if (!fs.existsSync(installationPath)) {
if (!fs.existsSync(installationDir)) {
log.error("Python installation not found");
return false;
}
try {
fs.rmSync(installationPath, { recursive: true });
fs.rmSync(installationDir, { recursive: true, force: true });
log.info("Python installation removed successfully:", installationDir);
} catch (error) {
log.error("Failed to remove Python installation", error);
return false;
@@ -449,6 +468,39 @@ export const uninstallPython = (installationPath?: string): boolean => {
export const resetApp = async (): Promise<void> => {
await uninstallPython();
log.info("Uninstalled Python environment");
// remove config file
const configPath = path.join(getUserDataPath(), "config.json");
if (fs.existsSync(configPath)) {
try {
fs.unlinkSync(configPath);
log.info("Removed config file:", configPath);
} catch (error) {
log.error("Failed to remove config file:", error);
}
}
// remove secret key file
const secretKeyPath = path.join(getOpenWebUIDataPath(), ".key");
if (fs.existsSync(secretKeyPath)) {
try {
fs.unlinkSync(secretKeyPath);
log.info("Removed secret key file:", secretKeyPath);
} catch (error) {
log.error("Failed to remove secret key file:", error);
}
}
// remove /data folder
const dataPath = getOpenWebUIDataPath();
if (fs.existsSync(dataPath)) {
try {
fs.rmSync(dataPath, { recursive: true, force: true });
log.info("Removed data directory:", dataPath);
} catch (error) {
log.error("Failed to remove data directory:", error);
}
}
};
////////////////////////////////////////////////
@@ -518,6 +570,9 @@ export const installPackage = (
{
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
@@ -568,6 +623,9 @@ export const isPackageInstalled = (packageName: string): boolean => {
encoding: "utf-8",
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
}
);
@@ -610,16 +668,15 @@ export const startServer = async (
const pythonPath = getPythonPath();
log.info(`Using Python at: ${pythonPath}`);
const openWebUIPath = path.join(path.dirname(pythonPath), "open-webui");
let commandArgs: string[];
commandArgs = ["-m", "uv", "run", "open-webui", "serve", "--host", host];
const dataDir = path.join(app.getPath("userData"), "data");
const dataDir = getOpenWebUIDataPath();
const secretKey = getSecretKey();
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
process.env.DATA_DIR = dataDir;
process.env.WEBUI_SECRET_KEY = secretKey;
@@ -645,7 +702,12 @@ export const startServer = async (
const childProcess = spawn(pythonPath, commandArgs, {
detached: process.platform !== "win32",
stdio: ["ignore", "pipe", "pipe"],
env: { ...process.env },
env: {
...process.env,
...(process.platform === "win32"
? { PYTHONIOENCODING: "utf-8" }
: {}),
},
});
if (!childProcess.pid) {
@@ -658,24 +720,23 @@ export const startServer = async (
const appendLog = (source: string) => (data: Buffer) => {
const logLine = data.toString().trim();
const tag = `[${source}][PID:${childProcess.pid}]:`;
logLines.push(`${tag} ${logLine}`);
// (Optional) also log to main process console
// log.info(`${tag} ${logLine}`);
const line = `[${source}][PID:${childProcess.pid}]: ${logLine}`;
logLines.push(line);
serverLogger.info(line); // Log to console
};
childProcess.stdout?.on("data", appendLog("stdout"));
childProcess.stderr?.on("data", appendLog("stderr"));
childProcess.on("close", (code, signal) => {
logLines.push(
`[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`
);
const line = `[process][PID:${childProcess.pid}] Exited with code ${code} signal ${signal}`;
serverLogger.info(line);
logLines.push(line);
serverPIDs.delete(childProcess.pid);
// Note: we keep the logs available until manually cleared
});
childProcess.on("error", (err) => {
logLines.push(
`[process][PID:${childProcess.pid}] Error: ${err.message}`
);
const line = `[process][PID:${childProcess.pid}] Error: ${err.message}`;
serverLogger.error(line);
logLines.push(line);
});
// Compute URL directly, do not try to parse logs
@@ -688,50 +749,148 @@ export const startServer = async (
};
/**
* Terminates all server processes.
* Terminates all server processes with maximum reliability.
*/
export async function stopAllServers(): Promise<void> {
log.info("Stopping all servers...");
for (const pid of Array.from(serverPIDs)) {
try {
terminateProcessTree(pid);
serverPIDs.delete(pid); // Remove from tracking set after termination
const pidsToStop = Array.from(serverPIDs);
if (pidsToStop.length === 0) {
log.info("No servers to stop.");
return;
}
// First pass: attempt graceful termination
for (const pid of pidsToStop) {
await terminateProcessTree(pid, false);
}
// Wait a moment for graceful shutdown
await sleep(2000);
// Second pass: force kill any remaining processes
for (const pid of pidsToStop) {
await terminateProcessTree(pid, true);
}
// Final verification and cleanup
for (const pid of pidsToStop) {
if (!isProcessRunning(pid)) {
serverPIDs.delete(pid);
serverLogs.delete(pid);
} catch (error) {
console.error(`Error stopping server with PID ${pid}:`, error);
} else {
log.warn(
`Process ${pid} may still be running after termination attempts`
);
}
}
log.info("All servers stopped successfully.");
log.info(
`Stopped ${pidsToStop.length - serverPIDs.size}/${pidsToStop.length} servers successfully.`
);
}
/**
* Kills a process tree by PID.
* Kills a process tree by PID with retry logic.
*/
function terminateProcessTree(pid: number): void {
if (process.platform === "win32") {
async function terminateProcessTree(
pid: number,
forceKill: boolean = false
): Promise<void> {
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
execSync(`taskkill /PID ${pid} /T /F`);
log.info(
`Terminated server process tree (PID: ${pid}) on Windows.`
);
if (process.platform === "win32") {
await terminateWindows(pid, forceKill);
} else {
await terminateUnix(pid, forceKill);
}
// Verify termination
if (!isProcessRunning(pid)) {
log.info(`Successfully terminated process tree (PID: ${pid})`);
return;
}
} catch (error) {
console.error(
`Failed to terminate process tree (PID: ${pid}):`,
log.warn(
`Attempt ${attempt}/${maxRetries} failed for PID ${pid}:`,
error
);
}
} else {
try {
process.kill(-pid, "SIGKILL");
log.info(
`Terminated server process tree (PID: ${pid}) on Unix-like OS.`
);
} catch (error) {
console.error(
`Failed to terminate process tree (PID: ${pid}):`,
error
);
if (attempt < maxRetries) {
await sleep(1000); // Wait before retry
}
}
log.error(
`Failed to terminate process tree (PID: ${pid}) after ${maxRetries} attempts`
);
}
/**
* Terminate process on Windows.
*/
async function terminateWindows(
pid: number,
forceKill: boolean
): Promise<void> {
const commands = forceKill
? [`taskkill /PID ${pid} /T /F`]
: [`taskkill /PID ${pid} /T`, `taskkill /PID ${pid} /T /F`];
for (const cmd of commands) {
try {
execSync(cmd, { timeout: 5000, stdio: "ignore" });
await sleep(500); // Brief pause between commands
} catch (error) {
log.error(`Failed to terminate process (PID: ${pid}):`, error);
}
}
}
/**
* Terminate process on Unix-like systems.
*/
async function terminateUnix(pid: number, forceKill: boolean): Promise<void> {
const signals = forceKill ? ["SIGKILL"] : ["SIGTERM", "SIGKILL"];
for (const signal of signals) {
try {
// Kill process group (negative PID)
process.kill(-pid, signal);
await sleep(500);
// Also try individual process if group kill fails
if (isProcessRunning(pid)) {
process.kill(pid, signal);
await sleep(500);
}
} catch (error) {
log.error(`Failed to terminate process (PID: ${pid}):`, error);
}
}
}
/**
* Check if a process is still running.
*/
function isProcessRunning(pid: number): boolean {
try {
// Sending signal 0 checks if process exists without killing it
process.kill(pid, 0);
return true;
} catch (error) {
return false;
}
}
/**
* Simple sleep utility.
*/
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
@@ -746,7 +905,7 @@ export const checkUrlAndOpen = async (
url: string,
callback: Function = async () => {}
) => {
const maxAttempts = 150; // 5 minutes with 2-second intervals
const maxAttempts = 1800; // 60 minutes with 2-second intervals
const interval = 2000; // 2 seconds
let attempts = 0;
@@ -793,7 +952,7 @@ export const checkUrlAndOpen = async (
// Start polling in the background (don't await)
pollUrl().catch((error) => {
console.error("Error in URL polling:", error);
log.error("Error in URL polling:", error);
});
};
@@ -806,7 +965,7 @@ export const getConfig = async (): Promise<Record<string, any>> => {
}
return {};
} catch (error) {
console.error("Error reading config:", error);
log.error("Error reading config:", error);
throw error;
}
};
@@ -819,7 +978,7 @@ export const setConfig = async (config: Record<string, any>): Promise<void> => {
JSON.stringify(config, null, 2)
);
} catch (error) {
console.error("Error writing config:", error);
log.error("Error writing config:", error);
throw error;
}
};
+10
View File
@@ -54,6 +54,16 @@ const api = {
await ipcRenderer.invoke("open:browser", { url });
},
getAppInfo: async () => {
if (!isLocalSource()) {
throw new Error(
"Access restricted: This operation is only allowed in a local environment."
);
}
return await ipcRenderer.invoke("app:info");
},
getVersion: async () => {
if (!isLocalSource()) {
throw new Error(
+3 -2
View File
@@ -6,11 +6,12 @@
import Installation from "./lib/components/Installation.svelte";
import Loading from "./lib/components/Loading.svelte";
import { info, config } from "./lib/stores";
import { info, config, appInfo } from "./lib/stores";
let installed = $state(false);
onMount(async () => {
appInfo.set(await window?.electronAPI?.getAppInfo());
config.set(await window?.electronAPI?.getConfig());
const pythonStatus = await window?.electronAPI?.getPythonStatus();
@@ -57,7 +58,7 @@
{:else if installed === false}
<Installation bind:installed />
{:else}
<Controls />
<Controls bind:installed />
{/if}
</main>
+37 -1
View File
@@ -154,7 +154,6 @@ html {
.scrollbar-hidden:hover::-webkit-scrollbar-thumb {
visibility: visible;
}
.scrollbar-hidden::-webkit-scrollbar-thumb {
visibility: hidden;
}
@@ -162,3 +161,40 @@ html {
.scrollbar-hidden::-webkit-scrollbar-corner {
display: none;
}
.scrollbar-none::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.scrollbar-none::-webkit-scrollbar-corner {
display: none;
}
.scrollbar-none {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
::-webkit-scrollbar-thumb {
--tw-border-opacity: 1;
background-color: rgba(215, 215, 215, 0.8);
border-color: rgba(255, 255, 255, var(--tw-border-opacity));
border-radius: 9999px;
border-width: 1px;
}
/* Dark theme scrollbar styles */
.dark ::-webkit-scrollbar-thumb {
background-color: rgba(67, 67, 67, 0.8); /* Darker color for dark theme */
border-color: rgba(0, 0, 0, var(--tw-border-opacity));
}
::-webkit-scrollbar {
height: 0.6rem;
width: 0.4rem;
}
::-webkit-scrollbar-track {
background-color: transparent;
border-radius: 9999px;
}
@@ -1,11 +1,13 @@
<script lang="ts">
import { onMount } from "svelte";
import { appInfo, info } from "../stores";
import Launching from "./Launching.svelte";
import logoImage from "../assets/images/splash.png";
import General from "./Controls/General.svelte";
import About from "./Controls/About.svelte";
import { info } from "../stores";
let { installed = $bindable(false) } = $props();
let selectedTab = $state("general");
@@ -17,7 +19,9 @@
class="flex flex-col w-full h-full relative text-gray-850 dark:text-gray-100"
>
<div
class="pt-3 pb-1.5 pl-22 pr-4 w-full drag-region flex flex-row gap-3 items-center justify-between"
class="pt-3 pb-1.5 {$appInfo?.platform === 'darwin'
? 'pl-22 pr-4'
: 'px-4'} w-full drag-region flex flex-row gap-3 items-center justify-between"
>
<div class=" font-medium">Controls</div>
@@ -104,7 +108,7 @@
class="flex-1 pt-1 sm:mt-0 overflow-y-scroll pr-1 scrollbar-hidden"
>
{#if selectedTab === "general"}
<General info={$info} />
<General bind:installed info={$info} />
{:else if selectedTab === "about"}
<About />
{/if}
@@ -7,7 +7,7 @@
import Spinner from "../common/Spinner.svelte";
import ConfirmDialog from "../common/ConfirmDialog.svelte";
let { info } = $props();
let { info, installed = $bindable(false) } = $props();
let config = $state(null);
@@ -54,12 +54,14 @@
confirmLabel="Reset"
onConfirm={async () => {
try {
installed = null;
config = null;
await window.electronAPI.resetApp();
toast.success("App has been reset successfully.");
// refresh the page to apply changes
window.location.reload();
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
toast.error("Failed to reset the app");
}
@@ -11,20 +11,12 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
let { installed = $bindable() } = $props();
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let mounted = $state(false);
let currentTime = Date.now();
@@ -57,10 +49,12 @@
(await window.electronAPI.getPythonStatus()) &&
(await window.electronAPI.getPackageStatus())
) {
// Notify the user that the installation is complete
// Start the server if it's not already running
if (!(await window.electronAPI.getServerStatus())) {
await window.electronAPI.startServer();
}
// Notify the user that the installation is complete
await window.electronAPI.notification(
"Installation Complete",
"Open WebUI is now ready to use."
@@ -218,7 +212,7 @@
{`Continue`}
</div>
<!-- <button
<div
class="text-xs mt-3 text-gray-500 cursor-pointer"
in:fly={{
delay: 500,
@@ -226,8 +220,16 @@
y: 10,
}}
>
To connect to an existing server, click here.
</button> -->
By continuing, you agree to our
<button
class="underline"
onclick={() => {
window.electronAPI.openInBrowser(
"https://github.com/open-webui/desktop/blob/main/LICENSE"
);
}}>license agreement</button
>.
</div>
{/if}
</div>
</div>
@@ -7,19 +7,11 @@
import galaxyImage from "../assets/images/galaxy.jpg";
import greenImage from "../assets/images/green.jpg";
import adamImage from "../assets/images/adam.jpg";
import earthImage from "../assets/images/earth.jpg";
import nasaImage from "../assets/images/nasa.jpg";
import neomImage from "../assets/images/neom.jpg";
import { fly } from "svelte/transition";
let images = [
galaxyImage,
greenImage,
adamImage,
earthImage,
nasaImage,
neomImage,
];
let images = [galaxyImage, greenImage, adamImage, nasaImage, neomImage];
let startTime = $state(null);
let currentTime = $state(null);
+2
View File
@@ -1,4 +1,6 @@
import { writable } from "svelte/store";
export const appInfo = writable(null);
export const info = writable(null);
export const config = writable(null);