From 0cb6bfb8bf71ffce7ee9835f1ddbd6263342013b Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 15 Oct 2024 20:05:13 +1100 Subject: [PATCH] cleanup and game UI beginnings --- desktop/components/Header.vue | 2 +- desktop/components/HeaderUserWidget.vue | 2 +- desktop/components/types.d.ts | 13 -- desktop/composables/use-object.ts | 5 + desktop/package.json | 2 + desktop/pages/library.vue | 61 ++++++++++ desktop/pages/library/[id]/index.vue | 28 +++++ desktop/pages/library/index.vue | 3 + desktop/prisma/schema.prisma | 150 ++++++++++++++++++++++++ desktop/src-tauri/Cargo.lock | 1 + desktop/src-tauri/Cargo.toml | 1 + desktop/src-tauri/src/auth.rs | 7 +- desktop/src-tauri/src/db.rs | 6 + desktop/src-tauri/src/lib.rs | 59 ++++++++-- desktop/src-tauri/src/library.rs | 70 +++++++++++ desktop/types.d.ts | 25 ++-- desktop/yarn.lock | 52 +++++++- 17 files changed, 447 insertions(+), 40 deletions(-) delete mode 100644 desktop/components/types.d.ts create mode 100644 desktop/composables/use-object.ts create mode 100644 desktop/pages/library.vue create mode 100644 desktop/pages/library/[id]/index.vue create mode 100644 desktop/pages/library/index.vue create mode 100644 desktop/prisma/schema.prisma create mode 100644 desktop/src-tauri/src/library.rs diff --git a/desktop/components/Header.vue b/desktop/components/Header.vue index 5b257de3..eeb5d655 100644 --- a/desktop/components/Header.vue +++ b/desktop/components/Header.vue @@ -45,7 +45,7 @@ diff --git a/desktop/pages/library/[id]/index.vue b/desktop/pages/library/[id]/index.vue new file mode 100644 index 00000000..f8109b8a --- /dev/null +++ b/desktop/pages/library/[id]/index.vue @@ -0,0 +1,28 @@ + + + diff --git a/desktop/pages/library/index.vue b/desktop/pages/library/index.vue new file mode 100644 index 00000000..61c7765c --- /dev/null +++ b/desktop/pages/library/index.vue @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/desktop/prisma/schema.prisma b/desktop/prisma/schema.prisma new file mode 100644 index 00000000..1ca51a1f --- /dev/null +++ b/desktop/prisma/schema.prisma @@ -0,0 +1,150 @@ +// This should be copied from the main Drop repo +// TODO: do this automatically + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + username String @unique + admin Boolean @default(false) + + email String + displayName String + profilePicture String // Object + + authMecs LinkedAuthMec[] + clients Client[] +} + +enum AuthMec { + Simple +} + +model LinkedAuthMec { + userId String + mec AuthMec + + credentials Json + + user User @relation(fields: [userId], references: [id]) + + @@id([userId, mec]) +} + +enum ClientCapabilities { + DownloadAggregation +} + +enum Platform { + Windows @map("windows") + Linux @map("linux") +} + +// References a device +model Client { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id]) + + endpoint String + capabilities ClientCapabilities[] + + name String + platform Platform + lastConnected DateTime +} + +enum MetadataSource { + Custom + GiantBomb +} + +model Game { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + + // Any field prefixed with m is filled in from metadata + // Acts as a cache so we can search and filter it + mName String // Name of game + mShortDescription String // Short description + mDescription String // Supports markdown + mDevelopers Developer[] + mPublishers Publisher[] + + mReviewCount Int + mReviewRating Float + + mIconId String // linked to objects in s3 + mBannerId String // linked to objects in s3 + mCoverId String + mImageLibrary String[] // linked to objects in s3 + + versions GameVersion[] + libraryBasePath String @unique // Base dir for all the game versions + + @@unique([metadataSource, metadataId], name: "metadataKey") +} + +// A particular set of files that relate to the version +model GameVersion { + gameId String + game Game @relation(fields: [gameId], references: [id]) + versionName String // Sub directory for the game files + + platform Platform + launchCommand String // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine + setupCommand String // Command to setup game (dependencies and such) + dropletManifest Json // Results from droplet + + versionIndex Int + delta Boolean @default(false) + + @@id([gameId, versionName]) +} + +model Developer { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String + + mName String + mShortDescription String + mDescription String + mLogo String + mBanner String + mWebsite String + + games Game[] + + @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") +} + +model Publisher { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String + + mName String + mShortDescription String + mDescription String + mLogo String + mBanner String + mWebsite String + + games Game[] + + @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") +} diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index f9489fef..c28ec7ed 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1022,6 +1022,7 @@ dependencies = [ "directories", "env_logger", "hex", + "http", "log", "openssl", "rayon", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index b8145409..00f4a04b 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ structured-logger = "1.0.3" hex = "0.4.3" tauri-plugin-dialog = "2" env_logger = "0.11.5" +http = "1.1.0" [dependencies.uuid] version = "1.10.0" diff --git a/desktop/src-tauri/src/auth.rs b/desktop/src-tauri/src/auth.rs index 153161e9..4f832ae4 100644 --- a/desktop/src-tauri/src/auth.rs +++ b/desktop/src-tauri/src/auth.rs @@ -18,7 +18,7 @@ use tauri::{http::response, App, AppHandle, Emitter, EventLoopMessage, Manager, use url::Url; use uuid::Uuid; -use crate::{db::DatabaseAuth, AppState, AppStatus, User, DB}; +use crate::{db::{fetch_base_url, DatabaseAuth}, AppState, AppStatus, User, DB}; #[derive(Serialize)] struct InitiateRequestBody { @@ -82,10 +82,7 @@ pub fn generate_authorization_header() -> String { } pub fn fetch_user() -> Result { - let base_url = { - let handle = DB.borrow_data().unwrap(); - Url::parse(&handle.base_url).unwrap() - }; + let base_url = fetch_base_url(); let endpoint = base_url.join("/api/v1/client/user").unwrap(); let header = generate_authorization_header(); diff --git a/desktop/src-tauri/src/db.rs b/desktop/src-tauri/src/db.rs index 4683f849..0e3fbaf0 100644 --- a/desktop/src-tauri/src/db.rs +++ b/desktop/src-tauri/src/db.rs @@ -7,6 +7,7 @@ use std::{ use directories::BaseDirs; use rustbreak::{deser::Bincode, PathDatabase}; use serde::Deserialize; +use url::Url; use crate::DB; @@ -60,3 +61,8 @@ pub fn setup() -> DatabaseInterface { pub fn is_set_up() -> bool { return !DB.borrow_data().unwrap().base_url.is_empty(); } + +pub fn fetch_base_url() -> Url { + let handle = DB.borrow_data().unwrap(); + Url::parse(&handle.base_url).unwrap() +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 3cc76169..249b256f 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -1,20 +1,21 @@ mod auth; mod db; +mod library; mod remote; mod unpacker; -use std::{ - io, - sync::{LazyLock, Mutex}, - task, thread, -}; +use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; +use db::{fetch_base_url, DatabaseInterface, DATA_ROOT_DIR}; use env_logger; use env_logger::Env; -use auth::{auth_initiate, recieve_handshake}; -use db::{DatabaseInterface, DATA_ROOT_DIR}; +use http::{header::*, response::Builder as ResponseBuilder, status::StatusCode}; +use library::{fetch_game, fetch_library, Game}; use log::info; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, io, sync::{LazyLock, Mutex}, task, thread +}; use structured_logger::{json::new_writer, Builder}; use tauri_plugin_deep_link::DeepLinkExt; @@ -38,6 +39,7 @@ pub struct User { pub struct AppState { status: AppStatus, user: Option, + games: HashMap, } #[tauri::command] @@ -56,6 +58,7 @@ fn setup<'a>() -> AppState { return AppState { status: AppStatus::NotConfigured, user: None, + games: HashMap::new(), }; } @@ -63,6 +66,7 @@ fn setup<'a>() -> AppState { return AppState { status: auth_result.0, user: auth_result.1, + games: HashMap::new(), }; } @@ -71,7 +75,7 @@ pub static DB: LazyLock = LazyLock::new(|| db::setup()); #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { let state = setup(); - info!("Initialized drop client"); + info!("initialized drop client"); let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init()); @@ -86,10 +90,16 @@ pub fn run() { .plugin(tauri_plugin_deep_link::init()) .manage(Mutex::new(state)) .invoke_handler(tauri::generate_handler![ + // DB fetch_state, + // Auth auth_initiate, + // Remote use_remote, gen_drop_url, + // Library + fetch_library, + fetch_game, ]) .plugin(tauri_plugin_shell::init()) .setup(|app| { @@ -102,7 +112,7 @@ pub fn run() { let handle = app.handle().clone(); - let main_window = tauri::WebviewWindowBuilder::new( + let _main_window = tauri::WebviewWindowBuilder::new( &handle, "main", // BTW this is not the name of the window, just the label. Keep this 'main', there are permissions & configs that depend on it tauri::WebviewUrl::App("index.html".into()), @@ -127,6 +137,37 @@ pub fn run() { Ok(()) }) + .register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| { + let base_url = fetch_base_url(); + + // Drop leading / + let object_id = &request.uri().path()[1..]; + + let object_url = base_url + .join("/api/v1/client/object/") + .unwrap() + .join(object_id) + .unwrap(); + + info!["{}", object_url.to_string()]; + + let header = generate_authorization_header(); + let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); + let response = client + .get(object_url.to_string()) + .header("Authorization", header) + .send() + .unwrap(); + + let resp_builder = ResponseBuilder::new().header( + CONTENT_TYPE, + response.headers().get("Content-Type").unwrap(), + ); + let data = Vec::from(response.bytes().unwrap()); + let resp = resp_builder.body(data).unwrap(); + + responder.respond(resp); + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/desktop/src-tauri/src/library.rs b/desktop/src-tauri/src/library.rs new file mode 100644 index 00000000..534d810f --- /dev/null +++ b/desktop/src-tauri/src/library.rs @@ -0,0 +1,70 @@ +use std::{borrow::BorrowMut, sync::Mutex}; + +use serde::{Deserialize, Serialize}; +use serde_json::json; +use tauri::{AppHandle, Manager}; + +use crate::{auth::generate_authorization_header, db::fetch_base_url, AppState}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct Game { + id: String, + mName: String, + mShortDescription: String, + mDescription: String, + // mDevelopers + // mPublishers + + mIconId: String, + mBannerId: String, + mCoverId: String, + mImageLibrary: Vec, +} + +#[tauri::command] +pub fn fetch_library(app: AppHandle) -> Result { + let base_url = fetch_base_url(); + let library_url = base_url.join("/api/v1/client/user/library").unwrap(); + + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(library_url.to_string()) + .header("Authorization", header) + .send() + .unwrap(); + + if response.status() != 200 { + return Err(format!( + "Library fetch request failed with {}", + response.status() + )); + } + + // Keep as string + let games = response.json::>().unwrap(); + + let state = app.state::>(); + let mut handle = state.lock().unwrap(); + + for game in games.iter() { + handle.games.insert(game.id.clone(), game.clone()); + } + + drop(handle); + + return Ok(json!(games.clone()).to_string()); +} + +#[tauri::command] +pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { + let state = app.state::>(); + let handle = state.lock().unwrap(); + let game = handle.games.get(&id); + if game.is_some() { + return Ok(json!(game.unwrap()).to_string()); + } + + return Ok("".to_string()); +} diff --git a/desktop/types.d.ts b/desktop/types.d.ts index 9b7405cc..566929f2 100644 --- a/desktop/types.d.ts +++ b/desktop/types.d.ts @@ -1,3 +1,17 @@ +import type { User } from "@prisma/client"; +import type { Component } from "vue" + +export type NavigationItem = { + prefix: string, + route: string, + label: string, +} + +export type QuickActionNav = { + icon: Component, + notifications?: number, + action: () => Promise, +} export type AppState = { status: AppStatus; user?: User; @@ -8,13 +22,4 @@ export enum AppStatus { SignedOut = "SignedOut", SignedIn = "SignedIn", SignedInNeedsReauth = "SignedInNeedsReauth", -} - -export type User = { - id: string; - username: string; - admin: boolean; - email: string; - displayName: string; - profilePicture: string; -}; +} \ No newline at end of file diff --git a/desktop/yarn.lock b/desktop/yarn.lock index b623e1b5..da122f89 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -1075,6 +1075,47 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== +"@prisma/client@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.20.0.tgz#4fc9f2b2341c9c997c139df4445688dd6b39663b" + integrity sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA== + +"@prisma/debug@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.20.0.tgz#c6d1cf6e3c6e9dba150347f13ca200b1d66cc9fc" + integrity sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ== + +"@prisma/engines-version@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284": + version "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz#9a53b13cdcfd706ae54198111000f33c63655c39" + integrity sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA== + +"@prisma/engines@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.20.0.tgz#86fe407e55219d33d03ebc26dc829a422faed545" + integrity sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ== + dependencies: + "@prisma/debug" "5.20.0" + "@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" + "@prisma/fetch-engine" "5.20.0" + "@prisma/get-platform" "5.20.0" + +"@prisma/fetch-engine@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz#b917880fb08f654981f14ca49923031b39683586" + integrity sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw== + dependencies: + "@prisma/debug" "5.20.0" + "@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" + "@prisma/get-platform" "5.20.0" + +"@prisma/get-platform@5.20.0": + version "5.20.0" + resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.20.0.tgz#c1a53a8d8af67f2b4a6b97dd4d25b1c603236804" + integrity sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA== + dependencies: + "@prisma/debug" "5.20.0" + "@rollup/plugin-alias@^5.1.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz#53601d88cda8b1577aa130b4a6e452283605bf26" @@ -2775,7 +2816,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@2.3.3, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -4304,6 +4345,15 @@ pretty-bytes@^6.1.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== +prisma@^5.20.0: + version "5.20.0" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.20.0.tgz#f2ab266a0d59383506886e7acbff0dbf322f4c7e" + integrity sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig== + dependencies: + "@prisma/engines" "5.20.0" + optionalDependencies: + fsevents "2.3.3" + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"