cleanup and game UI beginnings

This commit is contained in:
DecDuck
2024-10-15 20:05:13 +11:00
parent b3963b60b5
commit 5ef6b8e528
17 changed files with 447 additions and 40 deletions

View File

@ -45,7 +45,7 @@
<script setup lang="ts">
import { BellIcon, UserGroupIcon } from "@heroicons/vue/16/solid";
import type { NavigationItem, QuickActionNav } from "./types";
import type { NavigationItem, QuickActionNav } from "../types";
import HeaderWidget from "./HeaderWidget.vue";
import { getCurrentWindow } from "@tauri-apps/api/window";

View File

@ -71,7 +71,7 @@
<script setup lang="ts">
import { Menu, MenuButton, MenuItem, MenuItems } from "@headlessui/vue";
import { ChevronDownIcon } from "@heroicons/vue/16/solid";
import type { NavigationItem } from "./types";
import type { NavigationItem } from "../types";
import HeaderWidget from "./HeaderWidget.vue";
import { useAppState } from "~/composables/app-state";
import { invoke } from "@tauri-apps/api/core";

13
components/types.d.ts vendored
View File

@ -1,13 +0,0 @@
import type { Component } from "vue"
export type NavigationItem = {
prefix: string,
route: string,
label: string,
}
export type QuickActionNav = {
icon: Component,
notifications?: number,
action: () => Promise<void>,
}

View File

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

View File

@ -14,6 +14,7 @@
"dependencies": {
"@headlessui/vue": "^1.7.23",
"@heroicons/vue": "^2.1.5",
"@prisma/client": "5.20.0",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-deep-link": "~2",
"@tauri-apps/plugin-dialog": "~2",
@ -28,6 +29,7 @@
"@tauri-apps/cli": ">=2.0.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"prisma": "^5.20.0",
"sass-embedded": "^1.79.4",
"tailwindcss": "^3.4.13"
},

61
pages/library.vue Normal file
View File

@ -0,0 +1,61 @@
<template>
<div class="flex flex-row h-full">
<div class="flex-none h-full w-64 bg-zinc-950 px-2 py-1">
<ul class="flex flex-col gap-y-1">
<NuxtLink
v-for="(nav, navIdx) in navigation"
:key="nav.route"
:class="[
'transition group rounded flex justify-between gap-x-6 py-2 px-3',
navIdx === currentNavigationIndex ? 'bg-zinc-900' : '',
]"
:href="nav.route"
>
<div class="flex items-center min-w-0 gap-x-2">
<img
class="h-5 w-5 flex-none object-cover rounded-sm bg-zinc-900"
:src="icons[navIdx]"
alt=""
/>
<div class="min-w-0 flex-auto">
<p
:class="[
navIdx === currentNavigationIndex
? 'text-zinc-100'
: 'text-zinc-400 group-hover:text-zinc-300',
'transition text-sm font-display leading-6',
]"
>
{{ nav.label }}
</p>
</div>
</div>
</NuxtLink>
</ul>
</div>
<div class="grow">
<NuxtPage />
</div>
</div>
</template>
<script setup lang="ts">
import type { Game } from "@prisma/client";
import { invoke } from "@tauri-apps/api/core";
import type { NavigationItem } from "~/types";
const rawGames = await invoke<string>("fetch_library");
const games: Array<Game> = JSON.parse(rawGames);
const icons = await Promise.all(games.map((e) => useObject(e.mIconId)));
const navigation = games.map((e) => {
const item: NavigationItem = {
label: e.mName,
route: `/library/${e.id}`,
prefix: `/library/${e.id}`,
};
return item;
});
const currentNavigationIndex = useCurrentNavigationIndex(navigation);
</script>

View File

@ -0,0 +1,28 @@
<template>
<div
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]">
<img :src="bannerUrl" class="w-full h-auto object-cover" />
<div
class="absolute inset-0 bg-gradient-to-b from-transparent to-50% to-zinc-900"
/>
</div>
<!-- main page -->
<div class="w-full min-h-screen mx-auto bg-zinc-900 px-16 py-12"></div>
</div>
</template>
<script setup lang="ts">
import type { Game } from "@prisma/client";
import { invoke } from "@tauri-apps/api/core";
const route = useRoute();
const id = route.params.id;
const rawGame = await invoke<string>("fetch_game", { id: id });
const game: Game = JSON.parse(rawGame);
const bannerUrl = await useObject(game.mBannerId);
</script>

3
pages/library/index.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
</template>

150
prisma/schema.prisma Normal file
View File

@ -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")
}

1
src-tauri/Cargo.lock generated
View File

@ -1022,6 +1022,7 @@ dependencies = [
"directories",
"env_logger",
"hex",
"http",
"log",
"openssl",
"rayon",

View File

@ -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"

View File

@ -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<User, ()> {
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();

View File

@ -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()
}

View File

@ -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<User>,
games: HashMap<String, Game>,
}
#[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<DatabaseInterface> = 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");
}

70
src-tauri/src/library.rs Normal file
View File

@ -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<String>,
}
#[tauri::command]
pub fn fetch_library(app: AppHandle) -> Result<String, String> {
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::<Vec<Game>>().unwrap();
let state = app.state::<Mutex<AppState>>();
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<String, String> {
let state = app.state::<Mutex<AppState>>();
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());
}

25
types.d.ts vendored
View File

@ -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<void>,
}
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;
};
}

View File

@ -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"