mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
real progress bar (#490)
This commit is contained in:
parent
518a2d1f70
commit
8d0378656a
8 changed files with 153 additions and 32 deletions
|
@ -46,7 +46,7 @@ let macWindowClosed = false;
|
|||
|
||||
setupTitlebar();
|
||||
|
||||
initializeHandlers();
|
||||
initializeHandlers({ notifyMainWindow });
|
||||
|
||||
function createWindow() {
|
||||
const windowState = windowStateManager({
|
||||
|
@ -193,3 +193,9 @@ app.on("open-url", (event, url) => {
|
|||
createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
function notifyMainWindow(channel: string, data: unknown) {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send(channel, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,111 @@ import fs from "fs";
|
|||
import path from "path";
|
||||
import initializeTeaCli from "./initialize";
|
||||
|
||||
import { app } from "electron";
|
||||
import * as log from "electron-log";
|
||||
import { MainWindowNotifier } from "./types";
|
||||
|
||||
const destinationDirectory = getGuiPath();
|
||||
|
||||
export const cliBinPath = path.join(destinationDirectory, "tea");
|
||||
|
||||
export async function installPackage(full_name: string) {
|
||||
export async function installPackage(
|
||||
full_name: string,
|
||||
version: string,
|
||||
notifyMainWindow: MainWindowNotifier
|
||||
) {
|
||||
const teaVersion = await initializeTeaCli();
|
||||
const progressNotifier = newInstallProgressNotifier(full_name, notifyMainWindow);
|
||||
|
||||
if (!teaVersion) throw new Error("no tea");
|
||||
log.info(`installing package ${full_name}`);
|
||||
await asyncExec(`cd ${destinationDirectory} && ./tea --env=false --sync +${full_name} true`);
|
||||
|
||||
const qualifedPackage = `${full_name}@${version}`;
|
||||
|
||||
log.info(`installing package ${qualifedPackage}`);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
// tea requires HOME to be set.
|
||||
const opts = { env: { HOME: app.getPath("home"), NO_COLOR: "1" } };
|
||||
|
||||
const child = spawn(
|
||||
`${destinationDirectory}/tea`,
|
||||
["--env=false", "--sync", "--json", `+${qualifedPackage}`],
|
||||
opts
|
||||
);
|
||||
|
||||
child.stdout.on("data", (data) => {
|
||||
stdout += data.toString().trim();
|
||||
});
|
||||
|
||||
child.stderr.on("data", (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString().trim());
|
||||
progressNotifier(msg);
|
||||
} catch (err) {
|
||||
//swallow it
|
||||
}
|
||||
|
||||
stderr += data.toString().trim();
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
console.log("stdout:", stdout);
|
||||
if (code !== 0) {
|
||||
reject(new Error("tea exited with non-zero code: " + code));
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", () => {
|
||||
reject(new Error(stderr));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// This is hacky and kind of complex because of the output we get from the CLI. When the CLI
|
||||
// gives better output this definitely should get looked at.
|
||||
function newInstallProgressNotifier(full_name: string, notifyMainWindow: MainWindowNotifier) {
|
||||
// the install progress is super spammy, only send every 10th update
|
||||
let counter = 0;
|
||||
|
||||
// the totall number of packages to install - this is set by the "resolved" message
|
||||
let numberOfPackages = 1;
|
||||
|
||||
// the current package number - this is incremented by the "installed" or "downloaded" message
|
||||
let currentPackageNumber = 0;
|
||||
|
||||
return function (msg: any) {
|
||||
if (msg.status !== "downloading") {
|
||||
log.info("cli:", msg);
|
||||
}
|
||||
|
||||
if (msg.status === "resolved") {
|
||||
numberOfPackages = msg.pkgs?.length ?? 1;
|
||||
log.info(`installing ${numberOfPackages} packages`);
|
||||
} else if (msg.status === "downloading") {
|
||||
counter++;
|
||||
if (counter % 10 !== 0) return;
|
||||
|
||||
const { received = 0, "content-size": contentSize = 0 } = msg;
|
||||
if (contentSize > 0) {
|
||||
// how many total pacakges are completed
|
||||
const overallProgress = (currentPackageNumber / numberOfPackages) * 100;
|
||||
// how much of the current package is completed
|
||||
const packageProgress = (received / contentSize) * 100;
|
||||
// progress is the total packages completed plus the percentage of the current package
|
||||
const progress = (overallProgress + packageProgress / numberOfPackages).toFixed(2);
|
||||
notifyMainWindow("install-progress", { full_name, progress });
|
||||
}
|
||||
} else if (msg.status === "installed") {
|
||||
currentPackageNumber++;
|
||||
const progress = ((currentPackageNumber / numberOfPackages) * 100).toFixed(2);
|
||||
notifyMainWindow("install-progress", { full_name, progress });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function openTerminal(cmd: string) {
|
||||
|
|
|
@ -4,6 +4,10 @@ import * as log from "electron-log";
|
|||
import semver from "semver";
|
||||
import { cliBinPath, asyncExec } from "./cli";
|
||||
import { createInitialSessionFile } from "./auth";
|
||||
import semverCompare from "semver/functions/compare";
|
||||
|
||||
// Versions before this do not support the --json flag
|
||||
const MINIMUM_TEA_VERSION = "0.28.3";
|
||||
|
||||
const destinationDirectory = getGuiPath();
|
||||
|
||||
|
@ -14,8 +18,9 @@ const binaryUrl = "https://tea.xyz/$(uname)/$(uname -m)";
|
|||
|
||||
export async function initializeTeaCli(): Promise<string> {
|
||||
try {
|
||||
let version = "";
|
||||
let binCheck = "";
|
||||
let needsUpdate = false;
|
||||
|
||||
// Create the destination directory if it doesn't exist
|
||||
if (!fs.existsSync(destinationDirectory)) {
|
||||
fs.mkdirSync(destinationDirectory, { recursive: true });
|
||||
|
@ -23,10 +28,19 @@ export async function initializeTeaCli(): Promise<string> {
|
|||
|
||||
const curlCommand = `curl -L -o "${cliBinPath}" "${binaryUrl}"`;
|
||||
|
||||
if (fs.existsSync(cliBinPath)) {
|
||||
const exists = fs.existsSync(cliBinPath);
|
||||
if (exists) {
|
||||
log.info("binary tea already exists at", cliBinPath);
|
||||
binCheck = await asyncExec(`cd ${destinationDirectory} && ./tea --version`);
|
||||
} else {
|
||||
const teaVersion = binCheck.toString().split(" ")[1];
|
||||
|
||||
if (semverCompare(teaVersion, MINIMUM_TEA_VERSION) < 0) {
|
||||
log.info("binary tea version is too old, updating");
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!exists || needsUpdate) {
|
||||
try {
|
||||
await asyncExec(curlCommand);
|
||||
log.info("Binary downloaded and saved to", cliBinPath);
|
||||
|
@ -37,7 +51,8 @@ export async function initializeTeaCli(): Promise<string> {
|
|||
log.error("Error setting-up tea binary:", error);
|
||||
}
|
||||
}
|
||||
version = binCheck.toString().split(" ")[1];
|
||||
|
||||
const version = binCheck.toString().split(" ")[1];
|
||||
log.info("binary tea version:", version);
|
||||
return semver.valid(version.trim()) ? version : "";
|
||||
} catch (error) {
|
||||
|
|
|
@ -12,13 +12,19 @@ import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
|
|||
|
||||
import { loadPackageCache, writePackageCache } from "./package";
|
||||
import { nanoid } from "nanoid";
|
||||
let teaProtocolPath = ""; // this should be empty string
|
||||
import { MainWindowNotifier } from "./types";
|
||||
|
||||
export type HandlerOptions = {
|
||||
// A function to call back to the current main
|
||||
notifyMainWindow: MainWindowNotifier;
|
||||
};
|
||||
|
||||
let teaProtocolPath = ""; // this should be empty string
|
||||
export const setProtocolPath = (path: string) => {
|
||||
teaProtocolPath = path;
|
||||
};
|
||||
|
||||
export default function initializeHandlers() {
|
||||
export default function initializeHandlers({ notifyMainWindow }: HandlerOptions) {
|
||||
ipcMain.handle("get-installed-packages", async () => {
|
||||
try {
|
||||
log.info("getting installed packages");
|
||||
|
@ -52,11 +58,9 @@ export default function initializeHandlers() {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("install-package", async (_, data) => {
|
||||
ipcMain.handle("install-package", async (_, { full_name, version }) => {
|
||||
try {
|
||||
log.info("installing package:", data.full_name);
|
||||
const result = await installPackage(data.full_name);
|
||||
return result;
|
||||
return await installPackage(full_name, version, notifyMainWindow);
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return error;
|
||||
|
@ -123,7 +127,6 @@ export default function initializeHandlers() {
|
|||
});
|
||||
|
||||
ipcMain.handle("set-badge-count", async (_, { count }) => {
|
||||
console.log("set badge count:", count);
|
||||
if (count) {
|
||||
app.dock.setBadge(count.toString());
|
||||
} else {
|
||||
|
|
1
modules/desktop/electron/libs/types.ts
Normal file
1
modules/desktop/electron/libs/types.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export type MainWindowNotifier = (channel: string, data: unknown) => void;
|
|
@ -23,7 +23,6 @@ import {
|
|||
|
||||
import * as mock from "./native-mock";
|
||||
import { PackageStates, type InstalledPackage } from "./types";
|
||||
import { installPackageCommand } from "./native/cli";
|
||||
|
||||
import { get as apiGet } from "$libs/v1-client";
|
||||
import axios from "axios";
|
||||
|
@ -94,8 +93,16 @@ export async function getPackageReviews(full_name: string): Promise<Review[]> {
|
|||
export async function installPackage(pkg: GUIPackage, version?: string) {
|
||||
const latestVersion = pkg.version;
|
||||
const specificVersion = version || latestVersion;
|
||||
|
||||
log.info(`installing package: ${pkg.name} version: ${specificVersion}`);
|
||||
await installPackageCommand(pkg.full_name + (specificVersion ? `@${specificVersion}` : ""));
|
||||
const res = await ipcRenderer.invoke("install-package", {
|
||||
full_name: pkg.full_name,
|
||||
version: specificVersion
|
||||
});
|
||||
|
||||
if (res instanceof Error) {
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncPantry() {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
const { ipcRenderer } = window.require("electron");
|
||||
|
||||
export async function installPackageCommand(full_name: string) {
|
||||
const res = await ipcRenderer.invoke("install-package", { full_name });
|
||||
if (res instanceof Error) {
|
||||
throw res;
|
||||
}
|
||||
}
|
|
@ -13,7 +13,8 @@ import {
|
|||
loadPackageCache,
|
||||
writePackageCache,
|
||||
syncPantry,
|
||||
cacheImageURL
|
||||
cacheImageURL,
|
||||
listenToChannel
|
||||
} from "@native";
|
||||
|
||||
import { getReadme, getContributors, getRepoAsPackage } from "$libs/github";
|
||||
|
@ -166,7 +167,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
};
|
||||
|
||||
const installPkg = async (pkg: GUIPackage, version?: string) => {
|
||||
let fakeTimer: NodeJS.Timer | null = null;
|
||||
const originalState = pkg.state;
|
||||
const versionToInstall = version || pkg.version;
|
||||
|
||||
|
@ -178,10 +178,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
|
||||
updatePackage(pkg.full_name, { state });
|
||||
|
||||
fakeTimer = withFakeLoader(pkg, (progress) => {
|
||||
updatePackage(pkg.full_name, { install_progress_percentage: progress });
|
||||
});
|
||||
|
||||
await installPackage(pkg, versionToInstall);
|
||||
trackInstall(pkg.full_name);
|
||||
|
||||
|
@ -206,7 +202,6 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
type: NotificationType.ERROR
|
||||
});
|
||||
} finally {
|
||||
fakeTimer && clearTimeout(fakeTimer);
|
||||
updatePackage(pkg.full_name, { install_progress_percentage: 100 });
|
||||
}
|
||||
};
|
||||
|
@ -268,6 +263,14 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
}
|
||||
};
|
||||
|
||||
listenToChannel("install-progress", (data: any) => {
|
||||
const { full_name, progress } = data;
|
||||
if (!full_name) {
|
||||
return;
|
||||
}
|
||||
updatePackage(full_name, { install_progress_percentage: progress });
|
||||
});
|
||||
|
||||
return {
|
||||
packageList,
|
||||
syncProgress,
|
||||
|
@ -290,6 +293,7 @@ To read more about this package go to [${guiPkg.homepage}](${guiPkg.homepage}).
|
|||
};
|
||||
}
|
||||
|
||||
// This is only used for uninstall now
|
||||
export const withFakeLoader = (
|
||||
pkg: GUIPackage,
|
||||
callback: (progress: number) => void
|
||||
|
|
Loading…
Reference in a new issue