From dc892076d094e2520d5f7aa29042389ef6cb64cc Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 26 Feb 2026 15:15:41 +0700 Subject: [PATCH] app status and update notifications --- .../app/src/Classes/ApiManager/VersionApi.ts | 40 ++++ .../app/src/Classes/ApiManager/index.ts | 1 + .../signaled/TopBar/AppStatusCheck.tsx | 184 ++++++++++++++++++ .../src/components/signaled/TopBar/TopBar.tsx | 10 +- .../artcraft/app/src/pages/Stores/TabState.ts | 1 + 5 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 frontend/apps/artcraft/app/src/Classes/ApiManager/VersionApi.ts create mode 100644 frontend/apps/artcraft/app/src/components/signaled/TopBar/AppStatusCheck.tsx diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/VersionApi.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/VersionApi.ts new file mode 100644 index 0000000000..03640751a2 --- /dev/null +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/VersionApi.ts @@ -0,0 +1,40 @@ +import { ApiManager, ApiResponse } from "./ApiManager"; + +export interface NewVersionInfo { + version_label: string; + features_description: string; + direct_download_link?: string; + website_download_link: string; +} + +export interface VersionInfoResponse { + is_up_to_date: boolean; + new_version?: NewVersionInfo; +} + +export class VersionApi extends ApiManager { + private static instance: VersionApi; + + public static getInstance(): VersionApi { + if (!VersionApi.instance) { + VersionApi.instance = new VersionApi(); + } + return VersionApi.instance; + } + + public getVersionInfo( + platform: string, + versionString: string, + ): Promise> { + const endpoint = `${this.getApiSchemeAndHost()}/v1/artcraft/version_info/${platform}/${versionString}`; + return this.get({ endpoint }) + .then((data) => ({ + success: true, + data, + })) + .catch((err) => ({ + success: false, + errorMessage: err.message, + })); + } +} diff --git a/frontend/apps/artcraft/app/src/Classes/ApiManager/index.ts b/frontend/apps/artcraft/app/src/Classes/ApiManager/index.ts index ef832f222c..f49a591475 100644 --- a/frontend/apps/artcraft/app/src/Classes/ApiManager/index.ts +++ b/frontend/apps/artcraft/app/src/Classes/ApiManager/index.ts @@ -17,3 +17,4 @@ export * from "./UserBookmarksApi"; export * from "./UsersApi"; export * from "./VideoApi"; export * from "./WeightsApi"; +export * from "./VersionApi"; diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppStatusCheck.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppStatusCheck.tsx new file mode 100644 index 0000000000..5587f29b27 --- /dev/null +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppStatusCheck.tsx @@ -0,0 +1,184 @@ +import { + faArrowAltUp, + faTriangleExclamation, +} from "@fortawesome/pro-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { gtagEvent } from "@storyteller/google-analytics"; +import { GetAppInfo, OpenUrl } from "@storyteller/tauri-api"; +import { useTauriPlatform } from "@storyteller/tauri-utils"; +import { Button } from "@storyteller/ui-button"; +import { Tooltip } from "@storyteller/ui-tooltip"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import { MiscApi, VersionApi, NewVersionInfo } from "~/Classes/ApiManager"; + +export const AppStatusCheck = () => { + const [updateInfo, setUpdateInfo] = useState(null); + const platform = useTauriPlatform(); + + useEffect(() => { + let active = true; + const checkAppStatus = async () => { + try { + const appInfo = await GetAppInfo(); + const version = appInfo.payload.artcraft_version; + const currentPlatform = + platform || appInfo.payload.os_platform || "windows"; + + let versionRes = await VersionApi.getInstance().getVersionInfo( + currentPlatform, + version, + ); + + // TODO: Remove this mock once the API is working! + if (!versionRes.success || !versionRes.data) { + console.log( + "API not working - injecting mock version info for testing", + ); + versionRes = { + success: true, + data: { + is_up_to_date: false, + new_version: { + version_label: "v1.5.0-beta", + features_description: + "New AI generation tools, UI polish, and bug fixes!", + direct_download_link: "https://getartcraft.com/download/direct", + website_download_link: "https://getartcraft.com/download", + }, + }, + }; + } + + if ( + active && + versionRes.success && + versionRes.data && + !versionRes.data.is_up_to_date && + versionRes.data.new_version + ) { + const newVersion = versionRes.data.new_version; + setUpdateInfo(newVersion); + + setTimeout(() => { + if (active) { + toast( + (t) => ( +
{ + toast.dismiss(t.id); + if (newVersion.direct_download_link) { + OpenUrl(newVersion.direct_download_link); + } else { + OpenUrl(newVersion.website_download_link); + } + gtagEvent("click_update_button"); + }} + > + Update available: {newVersion.version_label} +
+ ), + { + duration: 10000, + icon: ( + + ), + style: { + background: "#eff6ff", + color: "#1e3a8a", + border: "1px solid #bfdbfe", + }, + }, + ); + } + }, 1000); + } + + let outageRes = await new MiscApi().GetStatusAlertCheck(); + + // TODO: Remove this mock once the API is working! + if (!outageRes.success || !outageRes.data?.maybe_alert?.maybe_message) { + console.log( + "API not working - injecting mock outage info for testing", + ); + outageRes = { + success: true, + data: { + refresh_interval_millis: 60000, + maybe_alert: { + maybe_category: "outage", + maybe_message: + "We are currently experiencing a brief outage. The team is actively investigating.", + }, + }, + }; + } + + if ( + active && + outageRes.success && + outageRes.data?.maybe_alert?.maybe_message + ) { + // Add a small delay for the toast to ensure it doesn't get hidden by initial loads + setTimeout(() => { + if (active) { + toast(outageRes.data!.maybe_alert!.maybe_message!, { + duration: 15000, + icon: ( + + ), + + style: { + background: "#fef2f2", + color: "#991b1b", + border: "1px solid #f87171", + }, + }); + } + }, 1000); + } + } catch (err) { + console.error("Failed to check app status", err); + } + }; + checkAppStatus(); + return () => { + active = false; + }; + }, [platform]); + + if (!updateInfo) { + return null; + } + + return ( + + + + ); +}; diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx index 7dcd38eeaa..50dab2b06b 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TopBar.tsx @@ -76,6 +76,7 @@ import { AppsQuickMenu } from "./AppsQuickMenu"; import { SceneTitleInput } from "./SceneTitleInput"; import { TaskQueue } from "./TaskQueue"; import { UploadImagesButton } from "./UploadImagesButton"; +import { AppStatusCheck } from "./AppStatusCheck"; interface Props { pageName: string; @@ -166,7 +167,7 @@ export const TopBar = ({ pageName }: Props) => { Promise.all([ creditsStore.fetchFromServer(), subscriptionStore.fetchFromServer(), - ]) + ]); }, 1000); return () => clearTimeout(t); }, []); @@ -597,6 +598,8 @@ export const TopBar = ({ pageName }: Props) => { )} + +
{isCreditsLoading ? ( @@ -607,7 +610,10 @@ export const TopBar = ({ pageName }: Props) => { position="bottom" align="center" triggerIcon={ - + } triggerLabel={ diff --git a/frontend/apps/artcraft/app/src/pages/Stores/TabState.ts b/frontend/apps/artcraft/app/src/pages/Stores/TabState.ts index b59110a43d..623bd9cdd9 100644 --- a/frontend/apps/artcraft/app/src/pages/Stores/TabState.ts +++ b/frontend/apps/artcraft/app/src/pages/Stores/TabState.ts @@ -8,6 +8,7 @@ export type TabId = | "EDIT" | "IMAGE" | "APPS" + | "EDIT" | "VIDEO_FRAME_EXTRACTOR" | "VIDEO_WATERMARK_REMOVAL" | "IMAGE_WATERMARK_REMOVAL"