feat(settings): ability to add more download dirs

This commit is contained in:
DecDuck
2024-11-24 21:04:56 +11:00
parent b065e101e6
commit 384f7a5be9
13 changed files with 291 additions and 80 deletions

View File

@ -21,7 +21,7 @@
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute bg-zinc-900 right-0 top-10 z-10 w-56 origin-top-right focus:outline-none shadow-md"
class="absolute bg-zinc-900 right-0 top-10 z-50 w-56 origin-top-right focus:outline-none shadow-md"
>
<PanelWidget class="flex-col gap-y-2">
<NuxtLink

View File

@ -1,5 +1,5 @@
import type { RouteLocationNormalized } from "vue-router";
import type { NavigationItem } from "~/components/types";
import type { NavigationItem } from "~/types";
export const useCurrentNavigationIndex = (
navigation: Array<NavigationItem>

View File

@ -17,7 +17,7 @@
"@prisma/client": "5.20.0",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-shell": ">=2.0.0",
"nuxt": "^3.13.0",
"scss": "^0.2.4",

View File

@ -3,7 +3,7 @@
class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden"
>
<!-- banner image -->
<div class="absolute flex top-0 h-fit inset-x-0 -z-[20]">
<div class="absolute flex top-0 h-fit inset-x-0 z-[-20]">
<img :src="bannerUrl" class="w-full h-auto object-cover" />
<h1
class="absolute inset-x-0 w-full text-center top-32 -translate-y-[50%] text-4xl text-zinc-100 font-bold font-display z-50"

View File

@ -13,8 +13,8 @@
:href="item.route"
:class="[
itemIdx === currentPageIndex
? 'bg-zinc-800/50 text-blue-600'
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-blue-600',
? 'bg-zinc-800/50 text-zinc-100'
: 'text-zinc-400 hover:bg-zinc-800/30 hover:text-zinc-200',
'transition group flex gap-x-3 rounded-md p-2 pr-12 text-sm font-semibold leading-6',
]"
>
@ -22,8 +22,8 @@
:is="item.icon"
:class="[
itemIdx === currentPageIndex
? 'text-blue-600'
: 'text-zinc-400 group-hover:text-blue-600',
? 'text-zinc-100'
: 'text-zinc-400 group-hover:text-zinc-200',
'transition h-6 w-6 shrink-0',
]"
aria-hidden="true"

View File

@ -1,3 +1,217 @@
<template>
</template>
<div>
<div class="border-b border-zinc-600 py-2 px-1">
<div
class="-ml-4 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
>
<div class="ml-4 mt-2">
<h3 class="text-base font-display font-semibold text-zinc-100">
Install directories
</h3>
<p class="mt-1 text-sm text-zinc-400 max-w-xl">
This is where Drop will download game files to, and store them
indefinitely while you play. Drop and games may store other
information elsewhere, like saves or mods.
</p>
</div>
<div class="ml-4 mt-2 shrink-0">
<button
@click="() => (open = true)"
type="button"
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Add new directory
</button>
</div>
</div>
</div>
<ul role="list" class="divide-y divide-gray-800">
<li
v-for="dir in dirs"
:key="dir"
class="flex justify-between gap-x-6 py-5"
>
<div class="flex min-w-0 gap-x-4">
<FolderIcon
class="h-6 w-6 text-blue-600 flex-none rounded-full"
alt=""
/>
<div class="min-w-0 flex-auto">
<p class="text-sm/6 text-zinc-100">
{{ dir }}
</p>
</div>
</div>
<div class="flex shrink-0 items-center gap-x-6">
<button class="-m-2.5 block p-2.5 text-zinc-400 hover:text-zinc-100">
<span class="sr-only">Open options</span>
<TrashIcon class="size-5" aria-hidden="true" />
</button>
</div>
</li>
</ul>
</div>
<TransitionRoot as="template" :show="open">
<Dialog class="relative z-50" @close="open = false">
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0"
enter-to="opacity-100"
leave="ease-in duration-200"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed inset-0 bg-zinc-950 bg-opacity-75 transition-opacity"
/>
</TransitionChild>
<div class="fixed inset-0 z-10 w-screen overflow-y-auto">
<div
class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
>
<TransitionChild
as="template"
enter="ease-out duration-300"
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leave-from="opacity-100 translate-y-0 sm:scale-100"
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<DialogPanel
class="relative transform overflow-hidden rounded-lg bg-zinc-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6"
>
<div class="sm:flex sm:items-start">
<div class="mt-3 w-full sm:ml-4 sm:mt-0">
<div>
<label
for="dir"
class="block text-sm/6 font-medium text-zinc-100"
>Select game directory</label
>
<div class="mt-2">
<button
@click="() => selectDirectory()"
class="block text-left w-full rounded-md border-0 px-3 py-1.5 text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 bg-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm/6"
>
{{
currentDirectory ?? "Click to select a directory..."
}}
</button>
</div>
<p class="mt-2 text-sm text-zinc-400" id="dir-description">
Select an empty directory to add.
</p>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<LoadingButton
:disabled="currentDirectory == undefined"
type="button"
:loading="createDirectoryLoading"
@click="() => submitDirectory()"
:class="[
'inline-flex w-full shadow-sm sm:ml-3 sm:w-auto',
currentDirectory === undefined
? 'text-zinc-400 bg-blue-600/10 hover:bg-blue-600/10'
: 'text-white bg-blue-600 hover:bg-blue-500',
]"
>
Upload
</LoadingButton>
<button
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-800 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="() => cancelDirectory()"
ref="cancelButtonRef"
>
Cancel
</button>
</div>
<div v-if="error" class="mt-3 rounded-md bg-red-600/10 p-4">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon
class="h-5 w-5 text-red-600"
aria-hidden="true"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-600">
{{ error }}
</h3>
</div>
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</Dialog>
</TransitionRoot>
</template>
<script setup lang="ts">
import {
Dialog,
DialogPanel,
TransitionChild,
TransitionRoot,
} from "@headlessui/vue";
import { FolderIcon, TrashIcon, XCircleIcon } from "@heroicons/vue/16/solid";
import { invoke } from "@tauri-apps/api/core";
const open = ref(false);
const currentDirectory = ref<string | undefined>(undefined);
const error = ref<string | undefined>(undefined);
const createDirectoryLoading = ref(false);
const dirs = ref(await invoke<Array<string>>("fetch_download_dir_stats"));
async function selectDirectoryDialog(): Promise<string> {
const res = await invoke("plugin:dialog|open", {
options: { directory: true },
});
return res as string;
}
async function selectDirectory() {
try {
const dir = await selectDirectoryDialog();
currentDirectory.value = dir;
} catch (e) {
error.value = e as string;
}
}
function cancelDirectory() {
open.value = false;
currentDirectory.value = undefined;
}
async function submitDirectory() {
try {
error.value = undefined;
if (!currentDirectory.value)
throw new Error("Please select a directory first.");
createDirectoryLoading.value = true;
// Add directory
await invoke("add_new_download_dir", { newDir: currentDirectory.value });
// Update list
const newDirs = await invoke<Array<string>>("fetch_download_dir_stats");
dirs.value = newDirs;
createDirectoryLoading.value = false;
open.value = false;
} catch (e) {
error.value = e as string;
createDirectoryLoading.value = false;
}
}
</script>

28
src-tauri/Cargo.lock generated
View File

@ -58,9 +58,9 @@ dependencies = [
[[package]]
name = "anstream"
version = "0.6.15"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
@ -73,36 +73,36 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.8"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.1"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.4"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@ -612,9 +612,9 @@ dependencies = [
[[package]]
name = "colorchoice"
version = "1.0.2"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "combine"

View File

@ -6,7 +6,7 @@ use std::{
};
use directories::BaseDirs;
use log::debug;
use log::{debug, info};
use rustbreak::{deser::Bincode, PathDatabase};
use rustix::path::Arg;
use serde::{Deserialize, Serialize};
@ -68,23 +68,23 @@ impl DatabaseImpls for DatabaseInterface {
debug!("Creating games directory");
create_dir_all(games_base_dir.clone()).unwrap();
let default = Database {
auth: None,
base_url: "".to_string(),
games: DatabaseGames {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
games_statuses: HashMap::new(),
},
};
#[allow(clippy::let_and_return)]
let exists = fs::exists(db_path.clone()).unwrap();
let db = match exists {
true => PathDatabase::load_from_path(db_path).expect("Database loading failed"),
false => {
let default = Database {
auth: None,
base_url: "".to_string(),
games: DatabaseGames {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
games_statuses: HashMap::new(),
},
};
debug!("Creating database at path {}", db_path.as_str().unwrap());
PathDatabase::create_at_path(db_path, default).expect("Database could not be created")
},
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
}
};
db
@ -114,8 +114,8 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> {
let dir_contents = new_dir_path
.read_dir()
.map_err(|e| format!("Unable to check directory contents: {}", e))?;
if dir_contents.count() == 0 {
return Err("Path is not empty".to_string());
if dir_contents.count() != 0 {
return Err("Directory is not empty".to_string());
}
} else {
create_dir_all(new_dir_path)
@ -126,6 +126,18 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> {
let mut lock = DB.borrow_data_mut().unwrap();
lock.games.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
Ok(())
}
// Will, in future, return disk/remaining size
// Just returns the directories that have been set up
#[tauri::command]
pub fn fetch_download_dir_stats() -> Result<Vec<String>, String> {
let lock = DB.borrow_data().unwrap();
let directories = lock.games.install_dirs.clone();
drop(lock);
Ok(directories)
}

View File

@ -2,43 +2,39 @@ use std::sync::Mutex;
use log::info;
use crate::{AppError, AppState};
use crate::AppState;
#[tauri::command]
pub fn download_game(
game_id: String,
game_version: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), AppError> {
) -> Result<(), String> {
state
.lock()
.unwrap()
.download_manager
.queue_game(game_id, game_version, 0)
.map_err(|_| AppError::Signal)
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
}
#[tauri::command]
pub fn get_current_game_download_progress(
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<f64, AppError> {
) -> Result<f64, String> {
match state
.lock()
.unwrap()
.download_manager
.get_current_game_download_progress()
{
Some(progress) => Ok(progress),
None => Err(AppError::DoesNotExist),
}
{
Some(progress) => Ok(progress),
None => Err("Game does not exist".to_string()),
}
}
#[tauri::command]
pub fn stop_game_download(
state: tauri::State<'_, Mutex<AppState>>,
game_id: String
) {
pub fn stop_game_download(state: tauri::State<'_, Mutex<AppState>>, game_id: String) {
info!("Cancelling game download {}", game_id);
state
.lock()
@ -47,12 +43,7 @@ pub fn stop_game_download(
.cancel_download(game_id);
}
#[tauri::command]
pub fn get_current_write_speed(
state: tauri::State<'_, Mutex<AppState>>,
) {
}
pub fn get_current_write_speed(state: tauri::State<'_, Mutex<AppState>>) {}
/*
fn use_download_agent(

View File

@ -10,7 +10,7 @@ mod tests;
use crate::db::DatabaseImpls;
use auth::{auth_initiate, generate_authorization_header, recieve_handshake};
use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR};
use db::{add_new_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR};
use downloads::download_commands::*;
use downloads::download_manager::DownloadManagerBuilder;
use downloads::download_manager_interface::DownloadManager;
@ -36,12 +36,6 @@ pub enum AppStatus {
SignedInNeedsReauth,
ServerUnavailable,
}
#[derive(Debug, Serialize)]
pub enum AppError {
DoesNotExist,
Signal,
RemoteAccess(String)
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -65,7 +59,7 @@ pub struct AppState {
}
#[tauri::command]
fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<AppState, AppError> {
fn fetch_state(state: tauri::State<'_, Mutex<AppState>>) -> Result<AppState, String> {
let guard = state.lock().unwrap();
let cloned_state = guard.clone();
drop(guard);
@ -133,12 +127,14 @@ pub fn run() {
fetch_library,
fetch_game,
add_new_download_dir,
fetch_download_dir_stats,
// Downloads
download_game,
get_current_game_download_progress,
stop_game_download
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{

View File

@ -1,5 +1,6 @@
use std::sync::Mutex;
use log::info;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::{AppHandle, Manager};
@ -7,7 +8,6 @@ use tauri::{AppHandle, Manager};
use crate::db::DatabaseGameStatus;
use crate::db::DatabaseImpls;
use crate::remote::RemoteAccessError;
use crate::AppError;
use crate::{auth::generate_authorization_header, AppState, DB};
#[derive(serde::Serialize)]
@ -19,7 +19,7 @@ struct FetchGameStruct {
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Game {
game_id: String,
id: String,
m_name: String,
m_short_description: String,
m_description: String,
@ -55,12 +55,12 @@ fn fetch_library_logic(app: AppHandle) -> Result<String, RemoteAccessError> {
let mut db_handle = DB.borrow_data_mut().unwrap();
for game in games.iter() {
handle.games.insert(game.game_id.clone(), game.clone());
if !db_handle.games.games_statuses.contains_key(&game.game_id) {
handle.games.insert(game.id.clone(), game.clone());
if !db_handle.games.games_statuses.contains_key(&game.id) {
db_handle
.games
.games_statuses
.insert(game.game_id.clone(), DatabaseGameStatus::Remote);
.insert(game.id.clone(), DatabaseGameStatus::Remote);
}
}
@ -70,9 +70,8 @@ fn fetch_library_logic(app: AppHandle) -> Result<String, RemoteAccessError> {
}
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<String, AppError> {
fetch_library_logic(app)
.map_err(|e| AppError::RemoteAccess(e.to_string()))
pub fn fetch_library(app: AppHandle) -> Result<String, String> {
fetch_library_logic(app).map_err(|e| e.to_string())
}
fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result<String, RemoteAccessError> {
@ -88,7 +87,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result<String, RemoteA
status: db_handle
.games
.games_statuses
.get(&game.game_id)
.get(&game.id)
.unwrap()
.clone(),
};

View File

@ -8,7 +8,7 @@ use log::{info, warn};
use serde::{Deserialize, Serialize};
use url::{ParseError, Url};
use crate::{AppError, AppState, AppStatus, DB};
use crate::{AppState, AppStatus, DB};
#[derive(Debug, Clone)]
pub enum RemoteAccessError {
@ -31,7 +31,6 @@ impl Display for RemoteAccessError {
write!(f, "{}", parse_error)
}
RemoteAccessError::InvalidCodeError(error) => write!(f, "HTTP {}", error),
RemoteAccessError::ParsingError(parse_error) => todo!(),
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"),
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),

View File

@ -1376,10 +1376,10 @@
dependencies:
"@tauri-apps/api" "^2.0.0"
"@tauri-apps/plugin-dialog@~2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0.tgz#f1e2840c7f824572a76b375fd1b538a36f28de14"
integrity sha512-ApNkejXP2jpPBSifznPPcHTXxu9/YaRW+eJ+8+nYwqp0lLUtebFHG4QhxitM43wwReHE81WAV1DQ/b+2VBftOA==
"@tauri-apps/plugin-dialog@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz#cca38f2ef361c6d92495f5aa12154492cf3fa779"
integrity sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==
dependencies:
"@tauri-apps/api" "^2.0.0"