diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d285b61..d44f3b8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -14,7 +14,7 @@ use pngmeta::PNGImage; use regex::Regex; use util::cache::Cache; use std::{env, fs, thread}; -use tauri::{Emitter, Manager, WindowEvent}; +use tauri::{Emitter, Manager, State, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; // TODO: Linux support @@ -147,8 +147,9 @@ fn main() { .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_shell::init()) - .register_asynchronous_uri_scheme_protocol("photo", |_ctx, req, res| { - util::handle_uri_proto::handle_uri_proto(req, res); + .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| { + let cache: State = ctx.app_handle().state(); + util::handle_uri_proto::handle_uri_proto(req, res, cache); }) .on_window_event(|window, event| match event { WindowEvent::CloseRequested { api, .. } => { diff --git a/src-tauri/src/util/handle_uri_proto.rs b/src-tauri/src/util/handle_uri_proto.rs index ed2fd49..fa9f308 100644 --- a/src-tauri/src/util/handle_uri_proto.rs +++ b/src-tauri/src/util/handle_uri_proto.rs @@ -9,11 +9,14 @@ use std::{ thread, }; use tauri::{ - http::{Request, Response}, - UriSchemeResponder, + http::{Request, Response}, State, UriSchemeResponder }; -pub fn handle_uri_proto(request: Request>, responder: UriSchemeResponder) { +use super::cache::Cache; + +pub fn handle_uri_proto( request: Request>, responder: UriSchemeResponder, cache: State ) { + let photo_path = cache.get("photo-path".into()); + thread::spawn(move || { // Loads the requested image file, sends data back to the user let uri = request.uri(); @@ -42,6 +45,7 @@ pub fn handle_uri_proto(request: Request>, responder: UriSchemeResponder #[cfg(unix)] let path = uri.path(); + let path = format!("{}/{}", photo_path.unwrap(), path); let file = fs::File::open(path); match file { diff --git a/src/Components/App.tsx b/src/Components/App.tsx index 91e5a7e..8291682 100644 --- a/src/Components/App.tsx +++ b/src/Components/App.tsx @@ -1,6 +1,4 @@ -import { createSignal, createEffect, Switch, Match, onMount } from "solid-js"; -import { listen } from '@tauri-apps/api/event'; -import { fetch } from "@tauri-apps/plugin-http" +import { createSignal, onMount } from "solid-js"; import anime from "animejs"; import { invoke } from '@tauri-apps/api/core'; @@ -14,138 +12,11 @@ import SettingsMenu from "./SettingsMenu"; function App() { invoke('close_splashscreen') - let [ loggedIn, setLoggedIn ] = createSignal({ loggedIn: false, username: '', avatar: '', id: '', serverVersion: '0.0' }); - let [ storageInfo, setStorageInfo ] = createSignal({ storage: 0, used: 0, sync: false }); - let [ loadingType, setLoadingType ] = createSignal('load'); let [ currentPhotoView, setCurrentPhotoView ] = createSignal(null); let [ photoNavChoice, setPhotoNavChoice ] = createSignal(''); - let [ confirmationBoxText, setConfirmationBoxText ] = createSignal(''); - let confirmationBoxCallback = () => {} - - let [ photoCount, setPhotoCount ] = createSignal(0); - let [ photoSize, setPhotoSize ] = createSignal(0); - - let [ requestPhotoReload, setRequestPhotoReload ] = createSignal(false); - let [ isPhotosSyncing, setIsPhotosSyncing ] = createSignal(false); - let setConfirmationBox = ( text: string, cb: () => void ) => { - setConfirmationBoxText(text); - confirmationBoxCallback = cb; - } - - invoke('get_config_value_string', { key: 'token' }) - .then(token => { - if(token){ - fetch('https://photos.phazed.xyz/api/v1/account?token='+token) - .then(data => data.json()) - .then(data => { - if(!data.ok){ - return console.error(data); - } - - setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); - setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); - - if(!isPhotosSyncing() && data.user.settings.enableSync){ - setIsPhotosSyncing(true); - invoke('sync_photos', { token: token }); - } - }) - .catch(e => { - console.error(e); - }) - } - }) - - setTimeout(() => { - setLoadingType('none'); - }, 500); - - let loadingBlackout: HTMLElement; - let loadingShown = false; - - let confirmationBox: HTMLElement; - - createEffect(() => { - if(confirmationBoxText() !== ''){ - confirmationBox.style.display = 'block'; - - setTimeout(() => { - confirmationBox.style.opacity = '1'; - }, 1); - } else{ - confirmationBox.style.opacity = '0'; - - setTimeout(() => { - confirmationBox.style.display = 'none'; - }, 250); - } - }) - - createEffect(() => { - let type = loadingType(); - - if(loadingShown != (type != 'none')){ - loadingShown = (type != 'none'); - - if(loadingShown){ - loadingBlackout.style.display = 'flex'; - anime({ - targets: loadingBlackout, - opacity: 1, - easing: 'easeInOutQuad', - duration: 250 - }) - } else{ - anime({ - targets: loadingBlackout, - opacity: 0, - easing: 'easeInOutQuad', - duration: 250, - complete: () => { - loadingBlackout.style.display = 'none'; - } - }) - } - } - }) - - listen('auth-callback', ( event: any ) => { - let token = event.payload; - - fetch('https://photos.phazed.xyz/api/v1/account?token='+token) - .then(data => data.json()) - .then(data => { - if(!data.ok){ - console.error(data); - return setLoadingType('none'); - } - - console.log(data); - invoke('set_config_value_string', { key: 'token', value: token }); - - setLoadingType('none'); - setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); - setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); - - if(!isPhotosSyncing() && data.user.settings.enableSync){ - setIsPhotosSyncing(true); - invoke('sync_photos', { token: token }); - } - }) - .catch(e => { - setLoadingType('none'); - console.error(e); - }) - }) - - listen('auth-denied', () => { - setLoadingType('none'); - console.warn('Authetication Denied'); - }) - onMount(() => { anime.set('.settings', { @@ -158,66 +29,24 @@ function App() { return (
+ setPhotoNavChoice={setPhotoNavChoice} /> + setCurrentPhotoView={setCurrentPhotoView} /> - +
Image Copied!
- -
loadingBlackout = el}> - - -

Waiting for authentication in browser.

-
- -

Loading App...

-
-
-
- -
confirmationBox = el}> -
- { confirmationBoxText() }

- -
{ confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm
-
setConfirmationBoxText('') }>Deny
-
-
); } diff --git a/src/Components/FilterMenu.tsx b/src/Components/FilterMenu.tsx index f1ee395..585fe48 100644 --- a/src/Components/FilterMenu.tsx +++ b/src/Components/FilterMenu.tsx @@ -1,13 +1,6 @@ -enum FilterType{ - USER, WORLD -} - -class FilterMenuProps{ - setFilterType!: ( type: FilterType ) => void; - setFilter!: ( filter: string ) => void; -} +import { FilterType } from "./Structs/FilterType"; -let FilterMenu = ( props: FilterMenuProps ) => { +let FilterMenu = () => { let selectionButtons: HTMLDivElement[] = []; let select = ( index: number ) => { @@ -20,15 +13,15 @@ let FilterMenu = ( props: FilterMenuProps ) => {
selectionButtons.push(el)} onClick={() => { select(0); - props.setFilterType(FilterType.USER); + window.PhotoLoadingManager.SetFilterType(FilterType.USER); }}>User
selectionButtons.push(el)} onClick={() => { select(1); - props.setFilterType(FilterType.WORLD); + window.PhotoLoadingManager.SetFilterType(FilterType.WORLD); }}>World
- props.setFilter(el.target.value)} placeholder="Enter Search Term..."> + window.PhotoLoadingManager.SetFilter(el.target.value)} placeholder="Enter Search Term..."> ) } diff --git a/src/Components/Managers/AccountManager.tsx b/src/Components/Managers/AccountManager.tsx new file mode 100644 index 0000000..ce0e099 --- /dev/null +++ b/src/Components/Managers/AccountManager.tsx @@ -0,0 +1,133 @@ +import { invoke } from "@tauri-apps/api/core"; + +import { ProfileData } from "../Structs/ProfileData"; +import { StorageData } from "../Structs/StorageData"; +import { Accessor, createSignal, Setter } from "solid-js"; + +import { listen } from "@tauri-apps/api/event"; +import { fetch } from "@tauri-apps/plugin-http"; + +export class AccountManager{ + public Profile: Accessor; + public Storage: Accessor; + + public hasAccount: Accessor; + public isLoading: Accessor; + + private _setProfile: Setter; + private _setStorage: Setter; + + private _setHasAccount: Setter; + private _setIsLoading: Setter; + + private _loginEventCallbacks: Array<() => void> = []; + + private _emitLoginCallbacks(){ + this._loginEventCallbacks.forEach(e => e()); + } + + constructor(){ + let [ hasAccount, setHasAccount ] = createSignal(false); + let [ isLoading, setIsLoading ] = createSignal(true); + + let [ profile, setProfile ] = createSignal(null); + let [ storage, setStorage ] = createSignal(null); + + this.Profile = profile; + this.Storage = storage; + + this.hasAccount = hasAccount; + this.isLoading = isLoading; + + this._setProfile = setProfile; + this._setStorage = setStorage; + + this._setHasAccount = setHasAccount; + this._setIsLoading = setIsLoading; + + invoke('get_config_value_string', { key: 'token' }) + .then(( token: any ) => { + this.verifyToken(token); + }) + + listen('auth-callback', ( event: any ) => { + window.LoadingManager.SetLoading(""); + + let token = event.payload; + this.verifyToken(token); + }) + + listen('auth-denied', () => { + window.LoadingManager.SetLoading(""); + console.warn('Authetication Denied'); + }) + } + + private async verifyToken( token: string ){ + if(!token){ + this._setHasAccount(false); + this._setIsLoading(false); + + return this._emitLoginCallbacks(); + } + + let dat = await fetch('https://photos.phazed.xyz/api/v1/account?token='+token); + if(dat.status !== 200){ + this._setHasAccount(false); + this._setIsLoading(false); + + return this._emitLoginCallbacks(); + } + + let json = await dat.json(); + + let profile = new ProfileData(); + let storage = new StorageData(); + + profile.id = json.user._id; + profile.username = json.user.username; + profile.avatar = json.user.avatar; + profile.serverVersion = json.user.serverVersion; + + storage.used = json.user.used; + storage.total = json.user.storage; + storage.isSyncing = json.user.settings.enableSync; + + this._setProfile(profile); + this._setStorage(storage); + + this._setHasAccount(true); + this._setIsLoading(false); + + this._emitLoginCallbacks(); + } + + public login(){ + window.LoadingManager.SetLoading("Waiting for Authentication"); + invoke('start_user_auth'); + } + + public async logout(){ + let dat = await fetch('https://photos.phazed.xyz/api/v1/deauth?token='+(await invoke('get_config_value_string', { key: 'token' }))!) + if(dat.status !== 200) + throw new Error(dat.statusText); + + let json = await dat.json(); + if(!json.ok) + throw new Error(json.error); + + invoke('set_config_value_string', { key: 'token', value: '' }); + window.location.reload(); + + return json; + } + + public async Refresh(){ + let token: string = await invoke('get_config_value_string', { key: 'token' }); + await this.verifyToken(token); + } + + public onLoginFinish( cb: () => void ){ + this._loginEventCallbacks.push(cb); + } +} \ No newline at end of file diff --git a/src/Components/Managers/ConfirmationBoxManager.tsx b/src/Components/Managers/ConfirmationBoxManager.tsx new file mode 100644 index 0000000..722eede --- /dev/null +++ b/src/Components/Managers/ConfirmationBoxManager.tsx @@ -0,0 +1,43 @@ +import { createEffect, createSignal, Setter } from "solid-js"; + +export class ConfirmationBoxManager{ + private _confirmationBoxCallback = () => {}; + private _setConfirmationBoxText: Setter + + constructor(){ + let [ confirmationBoxText, setConfirmationBoxText ] = createSignal(''); + this._setConfirmationBoxText = setConfirmationBoxText; + + let confirmationBox: HTMLElement; + + createEffect(() => { + if(confirmationBoxText() !== ''){ + confirmationBox.style.display = 'block'; + + setTimeout(() => { + confirmationBox.style.opacity = '1'; + }, 1); + } else{ + confirmationBox.style.opacity = '0'; + + setTimeout(() => { + confirmationBox.style.display = 'none'; + }, 250); + } + }) + + document.body.appendChild(
confirmationBox = el}> +
+ { confirmationBoxText() }

+ +
{ this._confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm
+
setConfirmationBoxText('') }>Deny
+
+
as HTMLElement); + } + + public SetConfirmationBox( text: string, cb: () => void ){ + this._setConfirmationBoxText(text); + this._confirmationBoxCallback = cb; + } +} \ No newline at end of file diff --git a/src/Components/Managers/LoadingManager.tsx b/src/Components/Managers/LoadingManager.tsx new file mode 100644 index 0000000..2b6fd54 --- /dev/null +++ b/src/Components/Managers/LoadingManager.tsx @@ -0,0 +1,18 @@ +import { createSignal, Setter, Show } from "solid-js"; + +export class LoadingManager{ + public SetLoading: Setter; + + constructor(){ + let [ loading, setLoading ] = createSignal(""); + this.SetLoading = setLoading; + + document.body.appendChild( +
+
+

{ loading() }

+
+
as HTMLElement + ); + } +} \ No newline at end of file diff --git a/src/Components/Managers/PhotoLoadingManager.tsx b/src/Components/Managers/PhotoLoadingManager.tsx new file mode 100644 index 0000000..9781ae6 --- /dev/null +++ b/src/Components/Managers/PhotoLoadingManager.tsx @@ -0,0 +1,172 @@ +import { listen } from "@tauri-apps/api/event"; +import { Accessor, createSignal } from "solid-js"; +import { Photo } from "../Structs/Photo"; +import { invoke } from "@tauri-apps/api/core"; +import { PhotoMetadata } from "../Structs/PhotoMetadata"; +import { Vars } from "../Structs/Vars"; +import { FilterType } from "../FilterMenu"; + +export class PhotoLoadingManager{ + public PhotoCount: Accessor; + public PhotoSize: Accessor; + + public Photos: Photo[] = []; + public FilteredPhotos: Photo[] = []; + + public HasFirstLoaded = false; + + private _amountLoaded = 0; + private _finishedLoadingCallbacks: (() => void)[] = []; + + private _filterType: FilterType = FilterType.USER; + private _filter: string = ""; + + constructor(){ + let [ photoCount, setPhotoCount ] = createSignal(-1); + let [ photoSize, setPhotoSize ] = createSignal(-1); + + this.PhotoCount = photoCount; + this.PhotoSize = photoSize; + + listen('photos_loaded', ( event: any ) => { + let photoPaths = event.payload.photos.reverse(); + console.log(photoPaths); + + setPhotoCount(photoPaths.length); + setPhotoSize(event.payload.size); + + let doesHaveLegacy = false; + + photoPaths.forEach(( path: string ) => { + let photo + + if(path.slice(0, 9) === "legacy://"){ + photo = new Photo(path.slice(9), true); + doesHaveLegacy = true; + } else + photo = new Photo(path, false); + + this.Photos.push(photo); + photo.loadMeta(); + }) + + if(doesHaveLegacy){ + this.Photos = this.Photos.sort(( a, b ) => b.date.valueOf() - a.date.valueOf()); + } + + console.log(this.Photos.length + ' Photos found.'); + if(this.Photos.length === 0){ + console.log('No photos found, Skipping loading stage.'); + + this.FilteredPhotos = this.Photos; + this.HasFirstLoaded = true; + + this._finishedLoadingCallbacks.forEach(cb => cb()); + } + }); + + listen('photo_meta_loaded', ( event: any ) => { + let data: PhotoMetadata = event.payload; + + let photo = this.Photos.find(x => x.path === data.path); + if(!photo)return; + + photo.width = data.width; + photo.height = data.height; + + let scale = Vars.PHOTO_HEIGHT / photo.height; + + photo.scaledWidth = photo.width * scale; + photo.scaledHeight = Vars.PHOTO_HEIGHT; + + photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; + this._amountLoaded++; + + photo.metaLoaded = true; + photo.onMetaLoaded(); + + this.ReloadFilters(); + + if(this._amountLoaded === this.Photos.length && !this.HasFirstLoaded){ + this.FilteredPhotos = this.Photos; + this.HasFirstLoaded = true; + + this._finishedLoadingCallbacks.forEach(cb => cb()); + } + }) + + listen('photo_create', async ( event: any ) => { + let photo = new Photo(event.payload); + + this.Photos.splice(0, 0, photo); + photo.loadMeta(); + + // TODO: SyncManager + // if(!props.isPhotosSyncing() && window.AccountManager.Storage()?.isSyncing){ + // props.setIsPhotosSyncing(true); + // invoke('sync_photos', { token: (await invoke('get_config_value_string', { key: 'token' })) }); + // } + }) + + listen('photo_remove', ( event: any ) => { + this.Photos = this.Photos.filter(x => x.path !== event.payload); + this.FilteredPhotos = this.FilteredPhotos.filter(x => x.path !== event.payload); + + // TODO: PhotoViewerManager + // if(event.payload === props.currentPhotoView().path){ + // currentPhotoIndex = -1; + // props.setCurrentPhotoView(null); + // } + }) + } + + public SetFilterType( type: FilterType ){ + this._filterType = type; + this.ReloadFilters(); + } + + public SetFilter( filter: string ){ + this._filter = filter; + this.ReloadFilters(); + } + + public ReloadFilters(){ + this.FilteredPhotos = []; + + switch(this._filterType){ + case FilterType.USER: + this.Photos.map(p => { + if(p.metadata){ + let meta = JSON.parse(p.metadata); + let photo = meta.players.find(( y: any ) => y.displayName.toLowerCase().includes(this._filter) || y.id === this._filter); + + if(photo)this.FilteredPhotos.push(p); + } + }) + break; + case FilterType.WORLD: + this.Photos.map(p => { + if(p.metadata){ + let meta = JSON.parse(p.metadata); + let photo = meta.world.name.toLowerCase().includes(this._filter) || meta.world.id === this._filter; + + if(photo)this.FilteredPhotos.push(p); + } + }) + break; + } + } + + public Load(){ + this.Photos = []; + this.FilteredPhotos = []; + + this._amountLoaded = 0; + + invoke('load_photos'); + } + + public OnLoadingFinished( cb: () => void ){ + this._finishedLoadingCallbacks.push(cb); + } +} \ No newline at end of file diff --git a/src/Components/Managers/PhotoViewerManager.tsx b/src/Components/Managers/PhotoViewerManager.tsx new file mode 100644 index 0000000..f63a732 --- /dev/null +++ b/src/Components/Managers/PhotoViewerManager.tsx @@ -0,0 +1,20 @@ +import { Accessor, createSignal, Setter } from "solid-js"; +import { Photo } from "../Structs/Photo"; + +export class PhotoViewerManager{ + private _currentPhoto: Accessor; + + private _setCurrentPhoto: Setter; + + constructor(){ + [ this._currentPhoto, this._setCurrentPhoto ] = createSignal(null); + } + + public Close(){ + this._setCurrentPhoto(null); + } + + public OpenPhoto( photo: Photo ){ + this._setCurrentPhoto(photo); + } +} \ No newline at end of file diff --git a/src/Components/NavBar.tsx b/src/Components/NavBar.tsx index 388b334..722039f 100644 --- a/src/Components/NavBar.tsx +++ b/src/Components/NavBar.tsx @@ -1,15 +1,12 @@ import { invoke } from '@tauri-apps/api/core'; import { emit, listen } from '@tauri-apps/api/event'; -import { fetch } from "@tauri-apps/plugin-http"; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import anime from 'animejs'; import { Show, createSignal, onMount } from 'solid-js'; -const appWindow = getCurrentWebviewWindow() + +const appWindow = getCurrentWebviewWindow(); class NavBarProps{ - setLoadingType!: ( type: string ) => string; - loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; - setStorageInfo!: ( info: { storage: number, used: number, sync: boolean } ) => { storage: number, used: number, sync: boolean }; setIsPhotosSyncing!: ( syncing: boolean ) => boolean; } @@ -98,7 +95,7 @@ let NavBar = ( props: NavBarProps ) => { return ( <>