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:
ABevier 2023-04-21 22:42:00 -04:00 committed by GitHub
parent 3fd5012d8c
commit 544cf09b7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 236 additions and 102 deletions

View file

@ -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 });
});

View file

@ -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);
}
});
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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" };
}
};

View file

@ -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" };
};

View file

@ -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();

View file

@ -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: () => {

View file

@ -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),

View 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
};
}

View file

@ -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;
};

View file

@ -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 {

View 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