Compare commits

...
Author SHA1 Message Date
bflatastic dc892076d0 app status and update notifications 2026-04-06 22:32:23 +07:00
5 changed files with 234 additions and 2 deletions
@@ -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<ApiResponse<VersionInfoResponse>> {
const endpoint = `${this.getApiSchemeAndHost()}/v1/artcraft/version_info/${platform}/${versionString}`;
return this.get<VersionInfoResponse>({ endpoint })
.then((data) => ({
success: true,
data,
}))
.catch((err) => ({
success: false,
errorMessage: err.message,
}));
}
}
@@ -17,3 +17,4 @@ export * from "./UserBookmarksApi";
export * from "./UsersApi";
export * from "./VideoApi";
export * from "./WeightsApi";
export * from "./VersionApi";
@@ -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<NewVersionInfo | null>(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) => (
<div
className="cursor-pointer"
onClick={() => {
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}
</div>
),
{
duration: 10000,
icon: (
<FontAwesomeIcon
icon={faArrowAltUp}
className="text-blue-600"
/>
),
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: (
<FontAwesomeIcon
icon={faTriangleExclamation}
className="text-red-700"
/>
),
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 (
<Tooltip
content={`Update: ${updateInfo.version_label} - ${updateInfo.features_description}`}
position="bottom"
delay={300}
>
<Button
variant="primary"
className="h-[38px] transition-all duration-300 hover:animate-none hover:shadow-none"
onClick={() => {
if (updateInfo.direct_download_link) {
OpenUrl(updateInfo.direct_download_link);
} else {
OpenUrl(updateInfo.website_download_link);
}
gtagEvent("click_update_button");
}}
>
<FontAwesomeIcon icon={faArrowAltUp} className="animate-bounce" />
Update
</Button>
</Tooltip>
);
};
@@ -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) => {
)}
</div>
<AppStatusCheck />
<div className="flex justify-end gap-2" data-tauri-drag-region>
<div className="no-drag flex items-center gap-1.5">
{isCreditsLoading ? (
@@ -607,7 +610,10 @@ export const TopBar = ({ pageName }: Props) => {
position="bottom"
align="center"
triggerIcon={
<FontAwesomeIcon icon={faCoins} className="text-primary" />
<FontAwesomeIcon
icon={faCoins}
className="text-primary"
/>
}
triggerLabel={
<span className="whitespace-nowrap text-sm font-medium">
@@ -8,6 +8,7 @@ export type TabId =
| "EDIT"
| "IMAGE"
| "APPS"
| "EDIT"
| "VIDEO_FRAME_EXTRACTOR"
| "VIDEO_WATERMARK_REMOVAL"
| "IMAGE_WATERMARK_REMOVAL"