diff --git a/desktop/.gitmodules b/desktop/.gitmodules new file mode 100644 index 00000000..5b830403 --- /dev/null +++ b/desktop/.gitmodules @@ -0,0 +1,3 @@ +[submodule "drop-base"] + path = drop-base + url = https://github.com/drop-oss/drop-base diff --git a/desktop/components/GameOptions/Launch.vue b/desktop/components/GameOptions/Launch.vue new file mode 100644 index 00000000..054dac26 --- /dev/null +++ b/desktop/components/GameOptions/Launch.vue @@ -0,0 +1,31 @@ + + + Launch string template + + + + + Override the launch string. Passed to system's default shell, and replaces + "{}" with the command to start the game. + Leaving it blank will cause the game not to start. + + + + + diff --git a/desktop/components/GameOptionsModal.vue b/desktop/components/GameOptionsModal.vue new file mode 100644 index 00000000..19ff39a1 --- /dev/null +++ b/desktop/components/GameOptionsModal.vue @@ -0,0 +1,122 @@ + + + + + + + + (currentTabIndex = tabIdx)" + :class="[ + tabIdx == currentTabIndex + ? 'bg-zinc-800 text-zinc-100' + : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100', + 'transition w-full group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold', + ]" + > + + {{ tab.name }} + + + + + + + + + + + + + + + + {{ saveError }} + + + + + + + save()" + :loading="saveLoading" + type="submit" + class="ml-2 w-full sm:w-fit" + > + Save + + (open = false)" + type="button" + class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto" + ref="cancelButtonRef" + > + Cancel + + + + + + diff --git a/desktop/components/GameStatusButton.vue b/desktop/components/GameStatusButton.vue index 36468508..97317214 100644 --- a/desktop/components/GameStatusButton.vue +++ b/desktop/components/GameStatusButton.vue @@ -1,33 +1,75 @@ + - buttonActions[props.status.type]()" :class="[ - styles[props.status.type], - showDropdown ? 'rounded-l-md' : 'rounded-md', - 'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', - ]"> - + buttonActions[props.status.type]()" + :class="[ + styles[props.status.type], + showDropdown ? 'rounded-l-md' : 'rounded-md', + 'inline-flex uppercase font-display items-center gap-x-2 px-4 py-3 text-md font-semibold shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', + ]" + > + {{ buttonNames[props.status.type] }} - + - + - + + class="absolute right-0 z-[500] mt-2 w-32 origin-top-right rounded-md bg-zinc-900 shadow-lg ring-1 ring-zinc-100/5 focus:outline-none" + > - emit('uninstall')" - :class="[active ? 'bg-zinc-800 text-zinc-100 outline-none' : 'text-zinc-400', 'w-full block px-4 py-2 text-sm inline-flex justify-between']"> + emit('options')" + :class="[ + active + ? 'bg-zinc-800 text-zinc-100 outline-none' + : 'text-zinc-400', + 'w-full block px-4 py-2 text-sm inline-flex justify-between', + ]" + > + Options + + + + + emit('uninstall')" + :class="[ + active + ? 'bg-zinc-800 text-zinc-100 outline-none' + : 'text-zinc-400', + 'w-full block px-4 py-2 text-sm inline-flex justify-between', + ]" + > Uninstall @@ -45,13 +87,13 @@ import { ChevronDownIcon, PlayIcon, QueueListIcon, - TrashIcon, WrenchIcon, } from "@heroicons/vue/20/solid"; import type { Component } from "vue"; import { GameStatusEnum, type GameStatus } from "~/types.js"; -import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue' +import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue"; +import { Cog6ToothIcon, TrashIcon } from "@heroicons/vue/24/outline"; const props = defineProps<{ status: GameStatus }>(); const emit = defineEmits<{ @@ -60,19 +102,32 @@ const emit = defineEmits<{ (e: "queue"): void; (e: "uninstall"): void; (e: "kill"): void; + (e: "options"): void; }>(); -const showDropdown = computed(() => props.status.type === GameStatusEnum.Installed || props.status.type === GameStatusEnum.SetupRequired); +const showDropdown = computed( + () => + props.status.type === GameStatusEnum.Installed || + props.status.type === GameStatusEnum.SetupRequired +); const styles: { [key in GameStatusEnum]: string } = { - [GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500", - [GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", - [GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", - [GameStatusEnum.SetupRequired]: "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500", - [GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500", - [GameStatusEnum.Updating]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", - [GameStatusEnum.Uninstalling]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", - [GameStatusEnum.Running]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700" + [GameStatusEnum.Remote]: + "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 hover:bg-blue-500", + [GameStatusEnum.Queued]: + "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", + [GameStatusEnum.Downloading]: + "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", + [GameStatusEnum.SetupRequired]: + "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600 hover:bg-yellow-500", + [GameStatusEnum.Installed]: + "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600 hover:bg-green-500", + [GameStatusEnum.Updating]: + "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", + [GameStatusEnum.Uninstalling]: + "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", + [GameStatusEnum.Running]: + "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700 hover:bg-zinc-700", }; const buttonNames: { [key in GameStatusEnum]: string } = { @@ -83,7 +138,7 @@ const buttonNames: { [key in GameStatusEnum]: string } = { [GameStatusEnum.Installed]: "Play", [GameStatusEnum.Updating]: "Updating", [GameStatusEnum.Uninstalling]: "Uninstalling", - [GameStatusEnum.Running]: "Stop" + [GameStatusEnum.Running]: "Stop", }; const buttonIcons: { [key in GameStatusEnum]: Component } = { @@ -94,7 +149,7 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = { [GameStatusEnum.Installed]: PlayIcon, [GameStatusEnum.Updating]: ArrowDownTrayIcon, [GameStatusEnum.Uninstalling]: TrashIcon, - [GameStatusEnum.Running]: PlayIcon + [GameStatusEnum.Running]: PlayIcon, }; const buttonActions: { [key in GameStatusEnum]: () => void } = { @@ -104,7 +159,7 @@ const buttonActions: { [key in GameStatusEnum]: () => void } = { [GameStatusEnum.SetupRequired]: () => emit("launch"), [GameStatusEnum.Installed]: () => emit("launch"), [GameStatusEnum.Updating]: () => emit("queue"), - [GameStatusEnum.Uninstalling]: () => { }, - [GameStatusEnum.Running]: () => emit("kill") + [GameStatusEnum.Uninstalling]: () => {}, + [GameStatusEnum.Running]: () => emit("kill"), }; diff --git a/desktop/composables/game.ts b/desktop/composables/game.ts index 0335a7a0..0d247e14 100644 --- a/desktop/composables/game.ts +++ b/desktop/composables/game.ts @@ -1,8 +1,9 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import type { Game, GameStatus, GameStatusEnum } from "~/types"; +import type { Game, GameStatus, GameStatusEnum, GameVersion } from "~/types"; -const gameRegistry: { [key: string]: Game } = {}; +const gameRegistry: { [key: string]: { game: Game; version?: GameVersion } } = + {}; const gameStatusRegistry: { [key: string]: Ref } = {}; @@ -31,13 +32,14 @@ export const parseStatus = (status: SerializedGameStatus): GameStatus => { export const useGame = async (gameId: string) => { if (!gameRegistry[gameId]) { - const data: { game: Game; status: SerializedGameStatus } = await invoke( - "fetch_game", - { - gameId, - } - ); - gameRegistry[gameId] = data.game; + 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)); @@ -53,5 +55,9 @@ export const useGame = async (gameId: string) => { const game = gameRegistry[gameId]; const status = gameStatusRegistry[gameId]; - return { game, status }; -}; \ No newline at end of file + return { ...game, status }; +}; + +export type FrontendGameConfiguration = { + launchString: string; +}; diff --git a/desktop/drop-base b/desktop/drop-base new file mode 160000 index 00000000..26698e5b --- /dev/null +++ b/desktop/drop-base @@ -0,0 +1 @@ +Subproject commit 26698e5b069d463b9a02a3dd61e4888d28fa9f88 diff --git a/desktop/nuxt.config.ts b/desktop/nuxt.config.ts index 0a4f39ca..a69a0e73 100644 --- a/desktop/nuxt.config.ts +++ b/desktop/nuxt.config.ts @@ -13,5 +13,5 @@ export default defineNuxtConfig({ ssr: false, - extends: [["github:drop-oss/drop-base"]], + extends: [["./drop-base"]], }); diff --git a/desktop/pages/library/[id]/index.vue b/desktop/pages/library/[id]/index.vue index 96b51ea9..1d828349 100644 --- a/desktop/pages/library/[id]/index.vue +++ b/desktop/pages/library/[id]/index.vue @@ -24,18 +24,16 @@ - - installFlow()" - @launch="() => launch()" - @queue="() => queue()" - @uninstall="() => uninstall()" - @kill="() => kill()" - :status="status" - /> - + + installFlow()" + @launch="() => launch()" + @queue="() => queue()" + @uninstall="() => uninstall()" + @kill="() => kill()" + @options="() => (configureModalOpen = true)" + :status="status" + /> + + >(); const currentImageIndex = ref(0); +const configureModalOpen = ref(false); + async function installFlow() { installFlowOpen.value = true; versionOptions.value = undefined; diff --git a/desktop/src-tauri/src/games/library.rs b/desktop/src-tauri/src/games/library.rs index c3e6a68e..cb79ba6d 100644 --- a/desktop/src-tauri/src/games/library.rs +++ b/desktop/src-tauri/src/games/library.rs @@ -16,12 +16,13 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::remote::auth::generate_authorization_header; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; use crate::remote::requests::make_request; -use crate::AppState; +use crate::{AppState, DB}; #[derive(Serialize, Deserialize)] pub struct FetchGameStruct { game: Game, status: GameStatusWithTransient, + version: Option, } #[derive(Serialize, Deserialize, Clone, Debug, Default)] @@ -140,6 +141,24 @@ pub fn fetch_game_logic( ) -> Result { let mut state_handle = state.lock().unwrap(); + let handle = DB.borrow_data().unwrap(); + + let metadata_option = handle.applications.installed_game_version.get(&id); + let version = match metadata_option { + None => None, + Some(metadata) => Some( + handle + .applications + .game_versions + .get(&metadata.id) + .unwrap() + .get(metadata.version.as_ref().unwrap()) + .unwrap() + .clone(), + ), + }; + drop(handle); + let game = state_handle.games.get(&id); if let Some(game) = game { let status = GameStatusManager::fetch_state(&id); @@ -147,6 +166,7 @@ pub fn fetch_game_logic( let data = FetchGameStruct { game: game.clone(), status, + version, }; cache_object(id, game)?; @@ -185,6 +205,7 @@ pub fn fetch_game_logic( let data = FetchGameStruct { game: game.clone(), status, + version, }; cache_object(id, &game)?; @@ -196,9 +217,31 @@ pub fn fetch_game_logic_offline( id: String, _state: tauri::State<'_, Mutex>, ) -> Result { + let handle = DB.borrow_data().unwrap(); + let metadata_option = handle.applications.installed_game_version.get(&id); + let version = match metadata_option { + None => None, + Some(metadata) => Some( + handle + .applications + .game_versions + .get(&metadata.id) + .unwrap() + .get(metadata.version.as_ref().unwrap()) + .unwrap() + .clone(), + ), + }; + drop(handle); + let status = GameStatusManager::fetch_state(&id); let game = get_cached_object::(id)?; - Ok(FetchGameStruct { game, status }) + + Ok(FetchGameStruct { + game, + status, + version, + }) } pub fn fetch_game_verion_options_logic( @@ -401,3 +444,54 @@ pub fn push_game_update(app_handle: &AppHandle, game_id: &String, status: GameSt ) .unwrap(); } + +// TODO @quexeky fix error types (I used String lmao) + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FrontendGameOptions { + launch_string: String, +} + +#[tauri::command] +pub fn update_game_configuration( + game_id: String, + options: FrontendGameOptions, +) -> Result<(), String> { + let mut handle = DB.borrow_data_mut().unwrap(); + let installed_version = handle + .applications + .installed_game_version + .get(&game_id) + .ok_or("Game not installed")?; + + + let id = installed_version.id.clone(); + let version = installed_version.version.clone().unwrap(); + + let mut existing_configuration = handle + .applications + .game_versions + .get(&id) + .unwrap() + .get(&version) + .unwrap() + .clone(); + + // Add more options in here + existing_configuration.launch_command_template = options.launch_string; + + // Add no more options past here + + handle + .applications + .game_versions + .get_mut(&id) + .unwrap() + .insert(version.to_string(), existing_configuration); + + drop(handle); + DB.save().map_err(|e| e.to_string())?; + + Ok(()) +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 49f4fb88..3d7e6a5c 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -33,7 +33,7 @@ use games::commands::{ fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, }; use games::downloads::commands::download_game; -use games::library::Game; +use games::library::{update_game_configuration, Game}; use http::Response; use http::{header::*, response::Builder as ResponseBuilder}; use log::{debug, info, warn, LevelFilter}; @@ -255,6 +255,7 @@ pub fn run() { fetch_download_dir_stats, fetch_game_status, fetch_game_verion_options, + update_game_configuration, // Collections fetch_collections, fetch_collection, diff --git a/desktop/src-tauri/src/process/process_manager.rs b/desktop/src-tauri/src/process/process_manager.rs index 83da24c1..53f46d7f 100644 --- a/desktop/src-tauri/src/process/process_manager.rs +++ b/desktop/src-tauri/src/process/process_manager.rs @@ -266,13 +266,13 @@ impl ProcessManager<'_> { .map_err(|e| ProcessError::FormatError(e.to_string()))? .to_string(); - info!("launching process {} in {}", launch_string, install_dir); - #[cfg(target_os = "windows")] let mut command = Command::new("cmd"); #[cfg(target_os = "windows")] command.args(["/C", &launch_string]); + info!("launching (in {}): {}", install_dir, launch_string,); + #[cfg(unix)] let mut command: Command = Command::new("sh"); #[cfg(unix)] diff --git a/desktop/types.ts b/desktop/types.ts index e31cb4dc..670d17e7 100644 --- a/desktop/types.ts +++ b/desktop/types.ts @@ -37,6 +37,10 @@ export type Game = { mImageCarousel: string[]; }; +export type GameVersion = { + launchCommandTemplate: string; +}; + export enum AppStatus { NotConfigured = "NotConfigured", Offline = "Offline", @@ -54,7 +58,7 @@ export enum GameStatusEnum { Updating = "Updating", Uninstalling = "Uninstalling", SetupRequired = "SetupRequired", - Running = "Running" + Running = "Running", } export type GameStatus = { @@ -66,17 +70,17 @@ export enum DownloadableType { Game = "Game", Tool = "Tool", DLC = "DLC", - Mod = "Mod" + Mod = "Mod", } export type DownloadableMetadata = { - id: string, - version: string, - downloadType: DownloadableType -} + id: string; + version: string; + downloadType: DownloadableType; +}; export type Settings = { - autostart: boolean, - maxDownloadThreads: number, - forceOffline: boolean -} \ No newline at end of file + autostart: boolean; + maxDownloadThreads: number; + forceOffline: boolean; +};
+ Override the launch string. Passed to system's default shell, and replaces + "{}" with the command to start the game. + Leaving it blank will cause the game not to start. +