mirror of
https://github.com/ivabus/gui
synced 2025-04-23 14:07:14 +03:00
Auto updates on the top bar (#488)
* Auto updates on the top bar --------- Co-authored-by: neil molina <neil@neils-MacBook-Pro.local>
This commit is contained in:
parent
3fd5012d8c
commit
544cf09b7c
15 changed files with 236 additions and 102 deletions
|
@ -2,51 +2,60 @@ import { type AppUpdater, autoUpdater } from "electron-updater";
|
|||
import * as log from "electron-log";
|
||||
import { BrowserWindow } from "electron";
|
||||
|
||||
type AutoUpdateStatus = {
|
||||
status: "up-to-date" | "available" | "ready";
|
||||
version?: string;
|
||||
};
|
||||
|
||||
autoUpdater.logger = log;
|
||||
|
||||
let window: BrowserWindow;
|
||||
// keep the last status to resend to the window when it's opened becuase the store is destroyed when the window is closed
|
||||
let lastStatus: AutoUpdateStatus = { status: "up-to-date" };
|
||||
|
||||
export const getUpdater = () => autoUpdater;
|
||||
|
||||
export function checkUpdater(mainWindow: BrowserWindow): AppUpdater {
|
||||
window = mainWindow;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
return autoUpdater;
|
||||
}
|
||||
|
||||
function sendStatusToWindow(text: string, params?: { [key: string]: any }) {
|
||||
log.info(text);
|
||||
window?.webContents.send("message", text, params || {});
|
||||
// The auto update runs in the background so the window might not be open when the status changes
|
||||
// When the update store gets created as part of the window it will request the latest status.
|
||||
export function getAutoUpdateStatus() {
|
||||
return lastStatus;
|
||||
}
|
||||
|
||||
function sendStatusToWindow(status: AutoUpdateStatus) {
|
||||
lastStatus = status;
|
||||
window?.webContents.send("app-update-status", status);
|
||||
}
|
||||
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
log.info("checking for tea gui update");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
sendStatusToWindow(
|
||||
`A new tea gui(${info.version}) is being downloaded. Please don't close the app.`,
|
||||
{
|
||||
i18n_key: "notification.gui-downloading",
|
||||
version: info.version
|
||||
}
|
||||
);
|
||||
sendStatusToWindow({ status: "available" });
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
log.info("no update for tea gui");
|
||||
sendStatusToWindow({ status: "up-to-date" });
|
||||
});
|
||||
|
||||
autoUpdater.on("error", (err) => {
|
||||
log.error("auto update:", err);
|
||||
});
|
||||
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
let log_message = "Download speed: " + progressObj.bytesPerSecond;
|
||||
log_message = log_message + " - Downloaded " + progressObj.percent + "%";
|
||||
log_message = log_message + " (" + progressObj.transferred + "/" + progressObj.total + ")";
|
||||
log.info("tea gui:", log_message);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
sendStatusToWindow(`A new tea gui(${info.version}) is available. Relaunch the app to update.`, {
|
||||
i18n_key: "notification.gui-downloaded",
|
||||
version: info.version,
|
||||
action: "relaunch"
|
||||
});
|
||||
sendStatusToWindow({ status: "ready", version: info.version });
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { installPackage, openTerminal, syncPantry } from "./cli";
|
|||
|
||||
import initializeTeaCli from "./initialize";
|
||||
|
||||
import { getUpdater } from "./auto-updater";
|
||||
import { getAutoUpdateStatus, getUpdater } from "./auto-updater";
|
||||
|
||||
import { loadPackageCache, writePackageCache } from "./package";
|
||||
import { nanoid } from "nanoid";
|
||||
|
@ -86,6 +86,7 @@ export default function initializeHandlers() {
|
|||
|
||||
ipcMain.handle("relaunch", async () => {
|
||||
try {
|
||||
log.info("relaunching app");
|
||||
const autoUpdater = getUpdater();
|
||||
await autoUpdater.quitAndInstall();
|
||||
} catch (error) {
|
||||
|
@ -192,4 +193,13 @@ export default function initializeHandlers() {
|
|||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("get-auto-update-status", async () => {
|
||||
try {
|
||||
log.info("getting auto update status");
|
||||
return getAutoUpdateStatus();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { navStore } from "$libs/stores";
|
||||
import { submitLogs } from "@native";
|
||||
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
||||
|
||||
let submittedMessage = "";
|
||||
const onSubmitLogs = async () => {
|
||||
if (submittedMessage !== "syncing...") {
|
||||
submittedMessage = "syncing...";
|
||||
const msg = await submitLogs();
|
||||
submittedMessage = msg;
|
||||
}
|
||||
};
|
||||
|
||||
const hidePopup = () => {
|
||||
navStore.sideNavOpen.set(false);
|
||||
};
|
||||
</script>
|
||||
|
||||
<nav class="w-full p-2 text-sm" use:mouseLeaveDelay={2000} on:leave_delay={() => hidePopup()}>
|
||||
<button
|
||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
language
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
docs
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
update tea
|
||||
</button>
|
||||
<hr />
|
||||
<button
|
||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||
class:animate-pulse={submittedMessage === "syncing..."}
|
||||
on:click={onSubmitLogs}
|
||||
>
|
||||
submit logs
|
||||
</button>
|
||||
{#if submittedMessage}
|
||||
<p class="text-gray mt-2 text-xs">{submittedMessage}</p>
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
hr {
|
||||
border: 1px solid #272626;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import { shellOpenExternal, submitLogs } from "@native";
|
||||
import mouseLeaveDelay from "@tea/ui/lib/mouse-leave-delay";
|
||||
import UpdateButton from "./update-button.svelte";
|
||||
import { appUpdateStore } from "$libs/stores";
|
||||
const { updateStatus } = appUpdateStore;
|
||||
|
||||
const hidePopup = () => {
|
||||
isOpen = false;
|
||||
};
|
||||
|
||||
const preventDoubleClick = (evt: MouseEvent) => evt.stopPropagation();
|
||||
|
||||
$: isOpen = false;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="relative"
|
||||
use:mouseLeaveDelay={2000}
|
||||
on:leave_delay={() => hidePopup()}
|
||||
on:dblclick={preventDoubleClick}
|
||||
>
|
||||
<button
|
||||
class="border-gray group flex h-[28px] w-[28px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
||||
class:circle-badge={$updateStatus.status === "available" || $updateStatus.status === "ready"}
|
||||
on:click={() => (isOpen = !isOpen)}
|
||||
>
|
||||
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
|
||||
</button>
|
||||
|
||||
<nav
|
||||
class="menu border-gray absolute w-full border bg-black p-2 text-xs transition-all"
|
||||
class:invisible={!isOpen}
|
||||
class:visible={isOpen}
|
||||
>
|
||||
<!-- TODO: what is this supposed to do? -->
|
||||
<!-- <button
|
||||
class="hover:bg-gray outline-gray focus:bg-secondary h-8 w-full p-1 text-left outline-1 transition-all hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
language
|
||||
</button>
|
||||
<hr /> -->
|
||||
<button
|
||||
class="hover:bg-gray outline-gray h-7 w-full p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||
on:click={() => shellOpenExternal("https://docs.tea.xyz")}
|
||||
>
|
||||
docs
|
||||
</button>
|
||||
<hr />
|
||||
<UpdateButton />
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
hr {
|
||||
border: 1px solid #272626;
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
.menu {
|
||||
top: calc(100% + 4px);
|
||||
left: calc(50% - 80px);
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.circle-badge::after {
|
||||
content: "1";
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
background: #ff4100;
|
||||
font-size: 10px;
|
||||
top: -7px;
|
||||
right: -7px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<script lang="ts">
|
||||
import Spinner from "@tea/ui/spinner/spinner.svelte";
|
||||
import { relaunch } from "@native";
|
||||
import { appUpdateStore } from "$libs/stores";
|
||||
|
||||
const { updateStatus } = appUpdateStore;
|
||||
</script>
|
||||
|
||||
{#if $updateStatus.status === "up-to-date"}
|
||||
<div
|
||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
<div>up to date</div>
|
||||
<i class="installed-text icon-check-circle-o flex text-[#00ffd0]" />
|
||||
</div>
|
||||
{:else if $updateStatus.status === "available"}
|
||||
<div
|
||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||
>
|
||||
<div>fetching update</div>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else if $updateStatus.status === "ready"}
|
||||
<button
|
||||
class="hover:bg-gray outline-gray flex h-7 w-full items-center justify-between p-1 text-left outline-1 hover:bg-opacity-25 hover:outline"
|
||||
on:click={relaunch}
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="circle-badge mr-2">1</div>
|
||||
<div>update</div>
|
||||
</div>
|
||||
<div class="rounded-sm bg-white px-2 text-[10px] leading-[12px] text-black">
|
||||
v{$updateStatus.version}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.circle-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
background: #ff4100;
|
||||
font-size: 10px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { shellOpenExternal, submitLogs } from "@native";
|
||||
import { navStore } from "$libs/stores";
|
||||
import LoginButton from "./login-button.svelte";
|
||||
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
|
||||
const { sideNavOpen } = navStore;
|
||||
|
||||
const toggleSideNav = () => {
|
||||
sideNavOpen.update((v) => !v);
|
||||
};
|
||||
import ToolTip from "@tea/ui/tool-tip/tool-tip.svelte";
|
||||
import SettingsMenu from "$components/settings-menu/settings-menu.svelte";
|
||||
|
||||
const submitBugReport = async () => {
|
||||
const logId = await submitLogs();
|
||||
|
@ -20,7 +15,6 @@
|
|||
|
||||
<div class="mr-1 flex h-full items-center justify-end gap-2 p-2">
|
||||
<button
|
||||
|
||||
class="border-gray group flex h-[28px] w-[28px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
||||
on:click={() => submitBugReport()}
|
||||
on:dblclick={preventDoubleClick}
|
||||
|
@ -32,13 +26,6 @@
|
|||
</div>
|
||||
</ToolTip>
|
||||
</button>
|
||||
<!-- Add this back when dropdown is ready -->
|
||||
<!-- <button
|
||||
class="border-gray group flex h-[28px] w-[28px] items-center justify-center rounded-sm border hover:bg-[#e1e1e1]"
|
||||
on:click={toggleSideNav}
|
||||
on:dblclick={preventDoubleClick}
|
||||
>
|
||||
<div class="icon-gear text-l text-gray flex group-hover:text-black" />
|
||||
</button> -->
|
||||
<SettingsMenu />
|
||||
<LoginButton />
|
||||
</div>
|
||||
|
|
|
@ -12,7 +12,14 @@
|
|||
*/
|
||||
|
||||
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
|
||||
import { type GUIPackage, type DeviceAuth, type Session, AuthStatus, type Packages } from "./types";
|
||||
import {
|
||||
type GUIPackage,
|
||||
type DeviceAuth,
|
||||
type Session,
|
||||
AuthStatus,
|
||||
type Packages,
|
||||
type AutoUpdateStatus
|
||||
} from "./types";
|
||||
|
||||
import * as mock from "./native-mock";
|
||||
import { PackageStates, type InstalledPackage } from "./types";
|
||||
|
@ -191,8 +198,8 @@ export const openTerminal = (cmd: string) => {
|
|||
|
||||
export const shellOpenExternal = (link: string) => shell.openExternal(link);
|
||||
|
||||
export const listenToChannel = (channel: string, callback: (msg: string, ...args: any) => void) => {
|
||||
ipcRenderer.on(channel, (_: any, message: string, ...args: any[]) => callback(message, ...args));
|
||||
export const listenToChannel = (channel: string, callback: (data: any) => void) => {
|
||||
ipcRenderer.on(channel, (_: any, data: any) => callback(data));
|
||||
};
|
||||
|
||||
export const relaunch = () => ipcRenderer.invoke("relaunch");
|
||||
|
@ -270,3 +277,12 @@ export const cacheImageURL = async (url: string): Promise<string | undefined> =>
|
|||
log.error("Failed to cache image:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
||||
try {
|
||||
return await ipcRenderer.invoke("get-auto-update-status");
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
return { status: "up-to-date" };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* * make cors work with api.tea.xyz/v1
|
||||
*/
|
||||
import type { Package, Review, AirtablePost, Bottle } from "@tea/ui/types";
|
||||
import type { GUIPackage, Course, Category, Session, Packages } from "./types";
|
||||
import type { GUIPackage, Course, Category, Session, Packages, AutoUpdateStatus } from "./types";
|
||||
import { PackageStates } from "./types";
|
||||
import { loremIpsum } from "lorem-ipsum";
|
||||
import _ from "lodash";
|
||||
|
@ -383,3 +383,7 @@ export const topbarDoubleClick = async () => {
|
|||
export const cacheImageURL = async (url: string): Promise<string | undefined> => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getAutoUpdateStatus = async (): Promise<AutoUpdateStatus> => {
|
||||
return { status: "up-to-date" };
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ import initAuthStore from "./stores/auth";
|
|||
import initNavStore from "./stores/nav";
|
||||
import initPackagesStore from "./stores/pkgs";
|
||||
import initNotificationStore from "./stores/notifications";
|
||||
import initAppUpdateStore from "./stores/update";
|
||||
|
||||
export const featuredPackages = writable<Package[]>([]);
|
||||
|
||||
|
@ -135,3 +136,5 @@ export const authStore = initAuthStore();
|
|||
export const navStore = initNavStore();
|
||||
|
||||
export const notificationStore = initNotificationStore();
|
||||
|
||||
export const appUpdateStore = initAppUpdateStore();
|
||||
|
|
|
@ -4,7 +4,6 @@ import { goto } from "$app/navigation";
|
|||
const log = window.require("electron-log");
|
||||
|
||||
export default function initNavStore() {
|
||||
const sideNavOpen = writable<boolean>(false);
|
||||
const historyStore = writable<string[]>(["/"]);
|
||||
const showWelcome = writable<boolean>(false);
|
||||
|
||||
|
@ -23,7 +22,6 @@ export default function initNavStore() {
|
|||
return {
|
||||
showWelcome,
|
||||
historyStore,
|
||||
sideNavOpen,
|
||||
prevPath: prevPathStore,
|
||||
nextPath: nextPathStore,
|
||||
next: () => {
|
||||
|
|
|
@ -14,7 +14,9 @@ export default function initNotificationStore() {
|
|||
update((notifications) => notifications.filter((n) => n.id != id));
|
||||
};
|
||||
|
||||
listenToChannel("message", (message: string, params: { [key: string]: string }) => {
|
||||
listenToChannel("message", (data: any) => {
|
||||
const { message, params }: { message: string; params: { [key: string]: string } } = data;
|
||||
|
||||
update((value) => {
|
||||
const newNotification: Notification = {
|
||||
id: nanoid(4),
|
||||
|
|
21
modules/desktop/src/libs/stores/update.ts
Normal file
21
modules/desktop/src/libs/stores/update.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { AutoUpdateStatus } from "$libs/types";
|
||||
import { getAutoUpdateStatus, listenToChannel } from "@native";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export default function initAppUpdateStore() {
|
||||
const updateStatus = writable<AutoUpdateStatus>({ status: "up-to-date" });
|
||||
|
||||
getAutoUpdateStatus().then((status: AutoUpdateStatus) => {
|
||||
updateStatus.update(() => status);
|
||||
});
|
||||
|
||||
listenToChannel("app-update-status", (status: AutoUpdateStatus) => {
|
||||
if (status.status) {
|
||||
updateStatus.update(() => status);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
updateStatus
|
||||
};
|
||||
}
|
|
@ -74,3 +74,8 @@ export enum SideMenuOptions {
|
|||
}
|
||||
|
||||
export type InstalledPackage = Required<Pick<GUIPackage, "full_name" | "installed_versions">>;
|
||||
|
||||
export type AutoUpdateStatus = {
|
||||
status: "up-to-date" | "available" | "ready";
|
||||
version?: string;
|
||||
};
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
import { navigating } from "$app/stores";
|
||||
import { afterNavigate } from "$app/navigation";
|
||||
import TopBar from "$components/top-bar/top-bar.svelte";
|
||||
import PopoutMenu from "$components/popout-menu/popout-menu.svelte";
|
||||
import { navStore, packagesStore, searchStore } from "$libs/stores";
|
||||
import { listenToChannel } from "@native";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
@ -16,7 +15,7 @@
|
|||
|
||||
let view: HTMLElement;
|
||||
|
||||
const { sideNavOpen, setNewPath } = navStore;
|
||||
const { setNewPath } = navStore;
|
||||
const { searching } = searchStore;
|
||||
|
||||
$: if ($navigating) view.scrollTop = 0;
|
||||
|
@ -68,11 +67,6 @@
|
|||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{#if $sideNavOpen}
|
||||
<aside class="border-gray fixed z-50 rounded-sm border bg-black transition-all">
|
||||
<PopoutMenu />
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
#main-layout {
|
||||
|
|
8
modules/ui/src/spinner/spinner.svelte
Normal file
8
modules/ui/src/spinner/spinner.svelte
Normal file
|
@ -0,0 +1,8 @@
|
|||
<svg class="-ml-1 h-5 w-5 animate-spin text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 356 B |
Loading…
Reference in a new issue