feat: prototype big picture mode

This commit is contained in:
DecDuck
2025-09-23 18:05:35 +10:00
parent 864640d6ae
commit dbf9c8e8e5
62 changed files with 10306 additions and 214 deletions

View File

@ -0,0 +1,3 @@
import type { AppState } from "~/types";
export const useAppState = () => useState<AppState | undefined>("state");

View File

@ -0,0 +1,32 @@
import type { RouteLocationNormalized } from "vue-router";
import type { NavigationItem } from "~/types";
export const useCurrentNavigationIndex = (
navigation: Array<NavigationItem>
) => {
const router = useRouter();
const route = useRoute();
const currentNavigation = ref(-1);
function calculateCurrentNavIndex(to: RouteLocationNormalized) {
const validOptions = navigation
.map((e, i) => ({ ...e, index: i }))
.filter((e) => to.fullPath.startsWith(e.prefix));
const bestOption = validOptions
.sort((a, b) => b.route.length - a.route.length)
.at(0);
return bestOption?.index ?? -1;
}
currentNavigation.value = calculateCurrentNavIndex(route);
router.afterEach((to) => {
currentNavigation.value = calculateCurrentNavIndex(to);
});
return {currentNavigation, recalculateNavigation: () => {
currentNavigation.value = calculateCurrentNavIndex(route);
}};
};

View File

@ -0,0 +1,36 @@
import { listen } from "@tauri-apps/api/event";
import type { DownloadableMetadata } from "~/types";
export type QueueState = {
queue: Array<{
meta: DownloadableMetadata;
status: string;
progress: number | null;
current: number;
max: number;
}>;
status: string;
};
export type StatsState = {
speed: number; // Bytes per second
time: number; // Seconds,
};
export const useQueueState = () =>
useState<QueueState>("queue", () => ({ queue: [], status: "Unknown" }));
export const useStatsState = () =>
useState<StatsState>("stats", () => ({ speed: 0, time: 0 }));
listen("update_queue", (event) => {
const queue = useQueueState();
queue.value = event.payload as QueueState;
});
listen("update_stats", (event) => {
const stats = useStatsState();
stats.value = event.payload as StatsState;
});
export const useDownloadHistory = () => useState<Array<number>>('history', () => []);

View File

@ -0,0 +1,74 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types";
const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } =
{};
const gameStatusRegistry: { [key: string]: Ref<GameStatus> } = {};
type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } };
export type SerializedGameStatus = [
{ type: GameStatusEnum },
OptionGameStatus | null
];
export const parseStatus = (status: SerializedGameStatus): GameStatus => {
if (status[0]) {
return {
type: status[0].type,
};
} else if (status[1]) {
const [[gameStatus, options]] = Object.entries(status[1]);
return {
type: gameStatus as GameStatusEnum,
...options,
};
} else {
throw new Error("No game status");
}
};
export const useGame = async (gameId: string) => {
if (!gameRegistry[gameId]) {
const data: {
game: Game;
status: SerializedGameStatus;
version?: GameVersion;
} = await invoke("fetch_game", {
gameId,
});
gameRegistry[gameId] = { game: data.game, version: data.version };
if (!gameStatusRegistry[gameId]) {
gameStatusRegistry[gameId] = ref(parseStatus(data.status));
listen(`update_game/${gameId}`, (event) => {
console.log(event);
const payload: {
status: SerializedGameStatus;
version?: GameVersion;
} = event.payload as any;
gameStatusRegistry[gameId].value = parseStatus(payload.status);
/**
* I am not super happy about this.
*
* This will mean that we will still have a version assigned if we have a game installed then uninstall it.
* It is necessary because a flag to check if we should overwrite seems excessive, and this function gets called
* on transient state updates.
*/
if (payload.version) {
gameRegistry[gameId].version = payload.version;
}
});
}
}
const game = gameRegistry[gameId];
const status = gameStatusRegistry[gameId];
return { ...game, status };
};
export type FrontendGameConfiguration = {
launchString: string;
};

View File

@ -0,0 +1,9 @@
import { type DownloadableMetadata, DownloadableType } from '~/types'
export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
return {
id: gameId,
version,
downloadType: DownloadableType.Game
}
}

View File

@ -0,0 +1,93 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { data } from "autoprefixer";
import { AppStatus, type AppState } from "~/types";
export function setupHooks() {
const router = useRouter();
const state = useAppState();
listen("auth/processing", (event) => {
router.push("/auth/processing");
});
listen("auth/failed", (event) => {
router.push(
`/auth/failed?error=${encodeURIComponent(event.payload as string)}`
);
});
listen("auth/finished", async (event) => {
router.push("/library");
state.value = JSON.parse(await invoke("fetch_state"));
});
listen("download_error", (event) => {
createModal(
ModalType.Notification,
{
title: "Drop encountered an error while downloading",
description: `Drop encountered an error while downloading your game: "${(
event.payload as unknown as string
).toString()}"`,
buttonText: "Close",
},
(e, c) => c()
);
});
// This is for errors that (we think) aren't our fault
listen("launch_external_error", (event) => {
createModal(
ModalType.Confirmation,
{
title: "Did something go wrong?",
description:
"Drop detected that something might've gone wrong with launching your game. Do you want to open the log directory?",
buttonText: "Open",
},
async (e, c) => {
if (e == "confirm") {
await invoke("open_process_logs", { gameId: event.payload });
}
c();
}
);
});
/*
document.addEventListener("contextmenu", (event) => {
event.target?.dispatchEvent(new Event("contextmenu"));
event.preventDefault();
});
*/
}
export function initialNavigation(state: ReturnType<typeof useAppState>) {
if (!state.value)
throw createError({
statusCode: 500,
statusMessage: "App state not valid",
fatal: true,
});
const router = useRouter();
switch (state.value.status) {
case AppStatus.NotConfigured:
router.push({ path: "/setup" });
break;
case AppStatus.SignedOut:
router.push("/auth");
break;
case AppStatus.SignedInNeedsReauth:
router.push("/auth/signedout");
break;
case AppStatus.ServerUnavailable:
router.push("/error/serverunavailable");
break;
default:
router.push("/library");
}
}

View File

@ -0,0 +1,5 @@
import { convertFileSrc } from "@tauri-apps/api/core";
export const useObject = (id: string) => {
return convertFileSrc(id, "object");
};