A photo manager for VRChat.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

start tidying up frontend

+605 -606
+4 -3
src-tauri/src/main.rs
··· 14 14 use regex::Regex; 15 15 use util::cache::Cache; 16 16 use std::{env, fs, thread}; 17 - use tauri::{Emitter, Manager, WindowEvent}; 17 + use tauri::{Emitter, Manager, State, WindowEvent}; 18 18 use tauri_plugin_deep_link::DeepLinkExt; 19 19 20 20 // TODO: Linux support ··· 147 147 .plugin(tauri_plugin_process::init()) 148 148 .plugin(tauri_plugin_http::init()) 149 149 .plugin(tauri_plugin_shell::init()) 150 - .register_asynchronous_uri_scheme_protocol("photo", |_ctx, req, res| { 151 - util::handle_uri_proto::handle_uri_proto(req, res); 150 + .register_asynchronous_uri_scheme_protocol("photo", |ctx, req, res| { 151 + let cache: State<Cache> = ctx.app_handle().state(); 152 + util::handle_uri_proto::handle_uri_proto(req, res, cache); 152 153 }) 153 154 .on_window_event(|window, event| match event { 154 155 WindowEvent::CloseRequested { api, .. } => {
+7 -3
src-tauri/src/util/handle_uri_proto.rs
··· 9 9 thread, 10 10 }; 11 11 use tauri::{ 12 - http::{Request, Response}, 13 - UriSchemeResponder, 12 + http::{Request, Response}, State, UriSchemeResponder 14 13 }; 15 14 16 - pub fn handle_uri_proto(request: Request<Vec<u8>>, responder: UriSchemeResponder) { 15 + use super::cache::Cache; 16 + 17 + pub fn handle_uri_proto( request: Request<Vec<u8>>, responder: UriSchemeResponder, cache: State<Cache> ) { 18 + let photo_path = cache.get("photo-path".into()); 19 + 17 20 thread::spawn(move || { 18 21 // Loads the requested image file, sends data back to the user 19 22 let uri = request.uri(); ··· 42 45 #[cfg(unix)] 43 46 let path = uri.path(); 44 47 48 + let path = format!("{}/{}", photo_path.unwrap(), path); 45 49 let file = fs::File::open(path); 46 50 47 51 match file {
+4 -175
src/Components/App.tsx
··· 1 - import { createSignal, createEffect, Switch, Match, onMount } from "solid-js"; 2 - import { listen } from '@tauri-apps/api/event'; 3 - import { fetch } from "@tauri-apps/plugin-http" 1 + import { createSignal, onMount } from "solid-js"; 4 2 import anime from "animejs"; 5 3 import { invoke } from '@tauri-apps/api/core'; 6 4 ··· 14 12 function App() { 15 13 invoke('close_splashscreen') 16 14 17 - let [ loggedIn, setLoggedIn ] = createSignal({ loggedIn: false, username: '', avatar: '', id: '', serverVersion: '0.0' }); 18 - let [ storageInfo, setStorageInfo ] = createSignal({ storage: 0, used: 0, sync: false }); 19 - let [ loadingType, setLoadingType ] = createSignal('load'); 20 15 let [ currentPhotoView, setCurrentPhotoView ] = createSignal<any>(null); 21 16 let [ photoNavChoice, setPhotoNavChoice ] = createSignal<string>(''); 22 17 23 - let [ confirmationBoxText, setConfirmationBoxText ] = createSignal<string>(''); 24 - let confirmationBoxCallback = () => {} 25 - 26 - let [ photoCount, setPhotoCount ] = createSignal(0); 27 - let [ photoSize, setPhotoSize ] = createSignal(0); 28 - 29 - let [ requestPhotoReload, setRequestPhotoReload ] = createSignal(false); 30 - 31 18 let [ isPhotosSyncing, setIsPhotosSyncing ] = createSignal(false); 32 19 33 - let setConfirmationBox = ( text: string, cb: () => void ) => { 34 - setConfirmationBoxText(text); 35 - confirmationBoxCallback = cb; 36 - } 37 - 38 - invoke('get_config_value_string', { key: 'token' }) 39 - .then(token => { 40 - if(token){ 41 - fetch('https://photos.phazed.xyz/api/v1/account?token='+token) 42 - .then(data => data.json()) 43 - .then(data => { 44 - if(!data.ok){ 45 - return console.error(data); 46 - } 47 - 48 - setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 49 - setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 50 - 51 - if(!isPhotosSyncing() && data.user.settings.enableSync){ 52 - setIsPhotosSyncing(true); 53 - invoke('sync_photos', { token: token }); 54 - } 55 - }) 56 - .catch(e => { 57 - console.error(e); 58 - }) 59 - } 60 - }) 61 - 62 - setTimeout(() => { 63 - setLoadingType('none'); 64 - }, 500); 65 - 66 - let loadingBlackout: HTMLElement; 67 - let loadingShown = false; 68 - 69 - let confirmationBox: HTMLElement; 70 - 71 - createEffect(() => { 72 - if(confirmationBoxText() !== ''){ 73 - confirmationBox.style.display = 'block'; 74 - 75 - setTimeout(() => { 76 - confirmationBox.style.opacity = '1'; 77 - }, 1); 78 - } else{ 79 - confirmationBox.style.opacity = '0'; 80 - 81 - setTimeout(() => { 82 - confirmationBox.style.display = 'none'; 83 - }, 250); 84 - } 85 - }) 86 - 87 - createEffect(() => { 88 - let type = loadingType(); 89 - 90 - if(loadingShown != (type != 'none')){ 91 - loadingShown = (type != 'none'); 92 - 93 - if(loadingShown){ 94 - loadingBlackout.style.display = 'flex'; 95 - anime({ 96 - targets: loadingBlackout, 97 - opacity: 1, 98 - easing: 'easeInOutQuad', 99 - duration: 250 100 - }) 101 - } else{ 102 - anime({ 103 - targets: loadingBlackout, 104 - opacity: 0, 105 - easing: 'easeInOutQuad', 106 - duration: 250, 107 - complete: () => { 108 - loadingBlackout.style.display = 'none'; 109 - } 110 - }) 111 - } 112 - } 113 - }) 114 - 115 - listen('auth-callback', ( event: any ) => { 116 - let token = event.payload; 117 - 118 - fetch('https://photos.phazed.xyz/api/v1/account?token='+token) 119 - .then(data => data.json()) 120 - .then(data => { 121 - if(!data.ok){ 122 - console.error(data); 123 - return setLoadingType('none'); 124 - } 125 - 126 - console.log(data); 127 - invoke('set_config_value_string', { key: 'token', value: token }); 128 - 129 - setLoadingType('none'); 130 - setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 131 - setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 132 - 133 - if(!isPhotosSyncing() && data.user.settings.enableSync){ 134 - setIsPhotosSyncing(true); 135 - invoke('sync_photos', { token: token }); 136 - } 137 - }) 138 - .catch(e => { 139 - setLoadingType('none'); 140 - console.error(e); 141 - }) 142 - }) 143 - 144 - listen('auth-denied', () => { 145 - setLoadingType('none'); 146 - console.warn('Authetication Denied'); 147 - }) 148 - 149 20 onMount(() => { 150 21 anime.set('.settings', 151 22 { ··· 158 29 return ( 159 30 <div class="container"> 160 31 <NavBar 161 - setLoadingType={setLoadingType} 162 - loggedIn={loggedIn} 163 - setStorageInfo={setStorageInfo} 164 32 setIsPhotosSyncing={setIsPhotosSyncing} /> 165 33 166 34 <PhotoList 167 - storageInfo={storageInfo} 168 35 isPhotosSyncing={isPhotosSyncing} 169 36 setIsPhotosSyncing={setIsPhotosSyncing} 170 37 setCurrentPhotoView={setCurrentPhotoView} 171 38 currentPhotoView={currentPhotoView} 172 39 photoNavChoice={photoNavChoice} 173 - setPhotoNavChoice={setPhotoNavChoice} 174 - setConfirmationBox={setConfirmationBox} 175 - loggedIn={loggedIn} 176 - setPhotoCount={setPhotoCount} 177 - setPhotoSize={setPhotoSize} 178 - requestPhotoReload={requestPhotoReload} 179 - setRequestPhotoReload={setRequestPhotoReload} /> 40 + setPhotoNavChoice={setPhotoNavChoice} /> 180 41 181 42 <PhotoViewer 182 43 setPhotoNavChoice={setPhotoNavChoice} 183 44 currentPhotoView={currentPhotoView} 184 - setCurrentPhotoView={setCurrentPhotoView} 185 - storageInfo={storageInfo} 186 - loggedIn={loggedIn} 187 - setConfirmationBox={setConfirmationBox} /> 45 + setCurrentPhotoView={setCurrentPhotoView} /> 188 46 189 - <SettingsMenu 190 - setLoggedIn={setLoggedIn} 191 - setLoadingType={setLoadingType} 192 - photoCount={photoCount} 193 - photoSize={photoSize} 194 - setRequestPhotoReload={setRequestPhotoReload} 195 - loggedIn={loggedIn} 196 - storageInfo={storageInfo} 197 - setStorageInfo={setStorageInfo} 198 - setConfirmationBox={setConfirmationBox} /> 47 + <SettingsMenu /> 199 48 200 49 <div class="copy-notif">Image Copied!</div> 201 - 202 - <div class="loading" ref={( el ) => loadingBlackout = el}> 203 - <Switch> 204 - <Match when={loadingType() === 'auth'}> 205 - <p>Waiting for authentication in browser.</p> 206 - </Match> 207 - <Match when={loadingType() === 'load'}> 208 - <p>Loading App...</p> 209 - </Match> 210 - </Switch> 211 - </div> 212 - 213 - <div class="confirmation-box" ref={( el ) => confirmationBox = el}> 214 - <div class="confirmation-box-container"> 215 - { confirmationBoxText() }<br /><br /> 216 - 217 - <div class="button-danger" onClick={() => { confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm</div> 218 - <div class="button" onClick={() => setConfirmationBoxText('') }>Deny</div> 219 - </div> 220 - </div> 221 50 </div> 222 51 ); 223 52 }
+5 -12
src/Components/FilterMenu.tsx
··· 1 - enum FilterType{ 2 - USER, WORLD 3 - } 1 + import { FilterType } from "./Structs/FilterType"; 4 2 5 - class FilterMenuProps{ 6 - setFilterType!: ( type: FilterType ) => void; 7 - setFilter!: ( filter: string ) => void; 8 - } 9 - 10 - let FilterMenu = ( props: FilterMenuProps ) => { 3 + let FilterMenu = () => { 11 4 let selectionButtons: HTMLDivElement[] = []; 12 5 13 6 let select = ( index: number ) => { ··· 20 13 <div class="filter-type-select"> 21 14 <div class="selected-filter" ref={( el ) => selectionButtons.push(el)} onClick={() => { 22 15 select(0); 23 - props.setFilterType(FilterType.USER); 16 + window.PhotoLoadingManager.SetFilterType(FilterType.USER); 24 17 }}>User</div> 25 18 <div ref={( el ) => selectionButtons.push(el)} onClick={() => { 26 19 select(1); 27 - props.setFilterType(FilterType.WORLD); 20 + window.PhotoLoadingManager.SetFilterType(FilterType.WORLD); 28 21 }}>World</div> 29 22 </div> 30 23 31 - <input class="filter-search" type="text" onInput={( el ) => props.setFilter(el.target.value)} placeholder="Enter Search Term..."></input> 24 + <input class="filter-search" type="text" onInput={( el ) => window.PhotoLoadingManager.SetFilter(el.target.value)} placeholder="Enter Search Term..."></input> 32 25 </> 33 26 ) 34 27 }
+133
src/Components/Managers/AccountManager.tsx
··· 1 + import { invoke } from "@tauri-apps/api/core"; 2 + 3 + import { ProfileData } from "../Structs/ProfileData"; 4 + import { StorageData } from "../Structs/StorageData"; 5 + import { Accessor, createSignal, Setter } from "solid-js"; 6 + 7 + import { listen } from "@tauri-apps/api/event"; 8 + import { fetch } from "@tauri-apps/plugin-http"; 9 + 10 + export class AccountManager{ 11 + public Profile: Accessor<ProfileData | null>; 12 + public Storage: Accessor<StorageData | null>; 13 + 14 + public hasAccount: Accessor<boolean>; 15 + public isLoading: Accessor<boolean>; 16 + 17 + private _setProfile: Setter<ProfileData | null>; 18 + private _setStorage: Setter<StorageData | null>; 19 + 20 + private _setHasAccount: Setter<boolean>; 21 + private _setIsLoading: Setter<boolean>; 22 + 23 + private _loginEventCallbacks: Array<() => void> = []; 24 + 25 + private _emitLoginCallbacks(){ 26 + this._loginEventCallbacks.forEach(e => e()); 27 + } 28 + 29 + constructor(){ 30 + let [ hasAccount, setHasAccount ] = createSignal(false); 31 + let [ isLoading, setIsLoading ] = createSignal(true); 32 + 33 + let [ profile, setProfile ] = createSignal<ProfileData | null>(null); 34 + let [ storage, setStorage ] = createSignal<StorageData | null>(null); 35 + 36 + this.Profile = profile; 37 + this.Storage = storage; 38 + 39 + this.hasAccount = hasAccount; 40 + this.isLoading = isLoading; 41 + 42 + this._setProfile = setProfile; 43 + this._setStorage = setStorage; 44 + 45 + this._setHasAccount = setHasAccount; 46 + this._setIsLoading = setIsLoading; 47 + 48 + invoke('get_config_value_string', { key: 'token' }) 49 + .then(( token: any ) => { 50 + this.verifyToken(token); 51 + }) 52 + 53 + listen('auth-callback', ( event: any ) => { 54 + window.LoadingManager.SetLoading(""); 55 + 56 + let token = event.payload; 57 + this.verifyToken(token); 58 + }) 59 + 60 + listen('auth-denied', () => { 61 + window.LoadingManager.SetLoading(""); 62 + console.warn('Authetication Denied'); 63 + }) 64 + } 65 + 66 + private async verifyToken( token: string ){ 67 + if(!token){ 68 + this._setHasAccount(false); 69 + this._setIsLoading(false); 70 + 71 + return this._emitLoginCallbacks(); 72 + } 73 + 74 + let dat = await fetch('https://photos.phazed.xyz/api/v1/account?token='+token); 75 + if(dat.status !== 200){ 76 + this._setHasAccount(false); 77 + this._setIsLoading(false); 78 + 79 + return this._emitLoginCallbacks(); 80 + } 81 + 82 + let json = await dat.json(); 83 + 84 + let profile = new ProfileData(); 85 + let storage = new StorageData(); 86 + 87 + profile.id = json.user._id; 88 + profile.username = json.user.username; 89 + profile.avatar = json.user.avatar; 90 + profile.serverVersion = json.user.serverVersion; 91 + 92 + storage.used = json.user.used; 93 + storage.total = json.user.storage; 94 + storage.isSyncing = json.user.settings.enableSync; 95 + 96 + this._setProfile(profile); 97 + this._setStorage(storage); 98 + 99 + this._setHasAccount(true); 100 + this._setIsLoading(false); 101 + 102 + this._emitLoginCallbacks(); 103 + } 104 + 105 + public login(){ 106 + window.LoadingManager.SetLoading("Waiting for Authentication"); 107 + invoke('start_user_auth'); 108 + } 109 + 110 + public async logout(){ 111 + let dat = await fetch('https://photos.phazed.xyz/api/v1/deauth?token='+(await invoke('get_config_value_string', { key: 'token' }))!) 112 + if(dat.status !== 200) 113 + throw new Error(dat.statusText); 114 + 115 + let json = await dat.json(); 116 + if(!json.ok) 117 + throw new Error(json.error); 118 + 119 + invoke('set_config_value_string', { key: 'token', value: '' }); 120 + window.location.reload(); 121 + 122 + return json; 123 + } 124 + 125 + public async Refresh(){ 126 + let token: string = await invoke('get_config_value_string', { key: 'token' }); 127 + await this.verifyToken(token); 128 + } 129 + 130 + public onLoginFinish( cb: () => void ){ 131 + this._loginEventCallbacks.push(cb); 132 + } 133 + }
+43
src/Components/Managers/ConfirmationBoxManager.tsx
··· 1 + import { createEffect, createSignal, Setter } from "solid-js"; 2 + 3 + export class ConfirmationBoxManager{ 4 + private _confirmationBoxCallback = () => {}; 5 + private _setConfirmationBoxText: Setter<string> 6 + 7 + constructor(){ 8 + let [ confirmationBoxText, setConfirmationBoxText ] = createSignal(''); 9 + this._setConfirmationBoxText = setConfirmationBoxText; 10 + 11 + let confirmationBox: HTMLElement; 12 + 13 + createEffect(() => { 14 + if(confirmationBoxText() !== ''){ 15 + confirmationBox.style.display = 'block'; 16 + 17 + setTimeout(() => { 18 + confirmationBox.style.opacity = '1'; 19 + }, 1); 20 + } else{ 21 + confirmationBox.style.opacity = '0'; 22 + 23 + setTimeout(() => { 24 + confirmationBox.style.display = 'none'; 25 + }, 250); 26 + } 27 + }) 28 + 29 + document.body.appendChild(<div class="confirmation-box" ref={( el ) => confirmationBox = el}> 30 + <div class="confirmation-box-container"> 31 + { confirmationBoxText() }<br /><br /> 32 + 33 + <div class="button-danger" onClick={() => { this._confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm</div> 34 + <div class="button" onClick={() => setConfirmationBoxText('') }>Deny</div> 35 + </div> 36 + </div> as HTMLElement); 37 + } 38 + 39 + public SetConfirmationBox( text: string, cb: () => void ){ 40 + this._setConfirmationBoxText(text); 41 + this._confirmationBoxCallback = cb; 42 + } 43 + }
+18
src/Components/Managers/LoadingManager.tsx
··· 1 + import { createSignal, Setter, Show } from "solid-js"; 2 + 3 + export class LoadingManager{ 4 + public SetLoading: Setter<string>; 5 + 6 + constructor(){ 7 + let [ loading, setLoading ] = createSignal(""); 8 + this.SetLoading = setLoading; 9 + 10 + document.body.appendChild( 11 + <div><Show when={loading() !== ""}> 12 + <div class="loading"> 13 + <p>{ loading() }</p> 14 + </div> 15 + </Show></div> as HTMLElement 16 + ); 17 + } 18 + }
+172
src/Components/Managers/PhotoLoadingManager.tsx
··· 1 + import { listen } from "@tauri-apps/api/event"; 2 + import { Accessor, createSignal } from "solid-js"; 3 + import { Photo } from "../Structs/Photo"; 4 + import { invoke } from "@tauri-apps/api/core"; 5 + import { PhotoMetadata } from "../Structs/PhotoMetadata"; 6 + import { Vars } from "../Structs/Vars"; 7 + import { FilterType } from "../FilterMenu"; 8 + 9 + export class PhotoLoadingManager{ 10 + public PhotoCount: Accessor<number>; 11 + public PhotoSize: Accessor<number>; 12 + 13 + public Photos: Photo[] = []; 14 + public FilteredPhotos: Photo[] = []; 15 + 16 + public HasFirstLoaded = false; 17 + 18 + private _amountLoaded = 0; 19 + private _finishedLoadingCallbacks: (() => void)[] = []; 20 + 21 + private _filterType: FilterType = FilterType.USER; 22 + private _filter: string = ""; 23 + 24 + constructor(){ 25 + let [ photoCount, setPhotoCount ] = createSignal(-1); 26 + let [ photoSize, setPhotoSize ] = createSignal(-1); 27 + 28 + this.PhotoCount = photoCount; 29 + this.PhotoSize = photoSize; 30 + 31 + listen('photos_loaded', ( event: any ) => { 32 + let photoPaths = event.payload.photos.reverse(); 33 + console.log(photoPaths); 34 + 35 + setPhotoCount(photoPaths.length); 36 + setPhotoSize(event.payload.size); 37 + 38 + let doesHaveLegacy = false; 39 + 40 + photoPaths.forEach(( path: string ) => { 41 + let photo 42 + 43 + if(path.slice(0, 9) === "legacy://"){ 44 + photo = new Photo(path.slice(9), true); 45 + doesHaveLegacy = true; 46 + } else 47 + photo = new Photo(path, false); 48 + 49 + this.Photos.push(photo); 50 + photo.loadMeta(); 51 + }) 52 + 53 + if(doesHaveLegacy){ 54 + this.Photos = this.Photos.sort(( a, b ) => b.date.valueOf() - a.date.valueOf()); 55 + } 56 + 57 + console.log(this.Photos.length + ' Photos found.'); 58 + if(this.Photos.length === 0){ 59 + console.log('No photos found, Skipping loading stage.'); 60 + 61 + this.FilteredPhotos = this.Photos; 62 + this.HasFirstLoaded = true; 63 + 64 + this._finishedLoadingCallbacks.forEach(cb => cb()); 65 + } 66 + }); 67 + 68 + listen('photo_meta_loaded', ( event: any ) => { 69 + let data: PhotoMetadata = event.payload; 70 + 71 + let photo = this.Photos.find(x => x.path === data.path); 72 + if(!photo)return; 73 + 74 + photo.width = data.width; 75 + photo.height = data.height; 76 + 77 + let scale = Vars.PHOTO_HEIGHT / photo.height; 78 + 79 + photo.scaledWidth = photo.width * scale; 80 + photo.scaledHeight = Vars.PHOTO_HEIGHT; 81 + 82 + photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; 83 + this._amountLoaded++; 84 + 85 + photo.metaLoaded = true; 86 + photo.onMetaLoaded(); 87 + 88 + this.ReloadFilters(); 89 + 90 + if(this._amountLoaded === this.Photos.length && !this.HasFirstLoaded){ 91 + this.FilteredPhotos = this.Photos; 92 + this.HasFirstLoaded = true; 93 + 94 + this._finishedLoadingCallbacks.forEach(cb => cb()); 95 + } 96 + }) 97 + 98 + listen('photo_create', async ( event: any ) => { 99 + let photo = new Photo(event.payload); 100 + 101 + this.Photos.splice(0, 0, photo); 102 + photo.loadMeta(); 103 + 104 + // TODO: SyncManager 105 + // if(!props.isPhotosSyncing() && window.AccountManager.Storage()?.isSyncing){ 106 + // props.setIsPhotosSyncing(true); 107 + // invoke('sync_photos', { token: (await invoke('get_config_value_string', { key: 'token' })) }); 108 + // } 109 + }) 110 + 111 + listen('photo_remove', ( event: any ) => { 112 + this.Photos = this.Photos.filter(x => x.path !== event.payload); 113 + this.FilteredPhotos = this.FilteredPhotos.filter(x => x.path !== event.payload); 114 + 115 + // TODO: PhotoViewerManager 116 + // if(event.payload === props.currentPhotoView().path){ 117 + // currentPhotoIndex = -1; 118 + // props.setCurrentPhotoView(null); 119 + // } 120 + }) 121 + } 122 + 123 + public SetFilterType( type: FilterType ){ 124 + this._filterType = type; 125 + this.ReloadFilters(); 126 + } 127 + 128 + public SetFilter( filter: string ){ 129 + this._filter = filter; 130 + this.ReloadFilters(); 131 + } 132 + 133 + public ReloadFilters(){ 134 + this.FilteredPhotos = []; 135 + 136 + switch(this._filterType){ 137 + case FilterType.USER: 138 + this.Photos.map(p => { 139 + if(p.metadata){ 140 + let meta = JSON.parse(p.metadata); 141 + let photo = meta.players.find(( y: any ) => y.displayName.toLowerCase().includes(this._filter) || y.id === this._filter); 142 + 143 + if(photo)this.FilteredPhotos.push(p); 144 + } 145 + }) 146 + break; 147 + case FilterType.WORLD: 148 + this.Photos.map(p => { 149 + if(p.metadata){ 150 + let meta = JSON.parse(p.metadata); 151 + let photo = meta.world.name.toLowerCase().includes(this._filter) || meta.world.id === this._filter; 152 + 153 + if(photo)this.FilteredPhotos.push(p); 154 + } 155 + }) 156 + break; 157 + } 158 + } 159 + 160 + public Load(){ 161 + this.Photos = []; 162 + this.FilteredPhotos = []; 163 + 164 + this._amountLoaded = 0; 165 + 166 + invoke('load_photos'); 167 + } 168 + 169 + public OnLoadingFinished( cb: () => void ){ 170 + this._finishedLoadingCallbacks.push(cb); 171 + } 172 + }
+20
src/Components/Managers/PhotoViewerManager.tsx
··· 1 + import { Accessor, createSignal, Setter } from "solid-js"; 2 + import { Photo } from "../Structs/Photo"; 3 + 4 + export class PhotoViewerManager{ 5 + private _currentPhoto: Accessor<Photo | null>; 6 + 7 + private _setCurrentPhoto: Setter<Photo | null>; 8 + 9 + constructor(){ 10 + [ this._currentPhoto, this._setCurrentPhoto ] = createSignal<Photo | null>(null); 11 + } 12 + 13 + public Close(){ 14 + this._setCurrentPhoto(null); 15 + } 16 + 17 + public OpenPhoto( photo: Photo ){ 18 + this._setCurrentPhoto(photo); 19 + } 20 + }
+10 -38
src/Components/NavBar.tsx
··· 1 1 import { invoke } from '@tauri-apps/api/core'; 2 2 import { emit, listen } from '@tauri-apps/api/event'; 3 - import { fetch } from "@tauri-apps/plugin-http"; 4 3 import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 5 4 import anime from 'animejs'; 6 5 import { Show, createSignal, onMount } from 'solid-js'; 7 - const appWindow = getCurrentWebviewWindow() 6 + 7 + const appWindow = getCurrentWebviewWindow(); 8 8 9 9 class NavBarProps{ 10 - setLoadingType!: ( type: string ) => string; 11 - loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; 12 - setStorageInfo!: ( info: { storage: number, used: number, sync: boolean } ) => { storage: number, used: number, sync: boolean }; 13 10 setIsPhotosSyncing!: ( syncing: boolean ) => boolean; 14 11 } 15 12 ··· 98 95 return ( 99 96 <> 100 97 <div class="navbar" data-tauri-drag-region> 101 - <div class="tabs" data-tauri-drag-region > 98 + <div class="tabs" data-tauri-drag-region> 102 99 <div class="nav-tab" onClick={() => { 103 100 anime( 104 101 { ··· 126 123 </Show> 127 124 </div> 128 125 <div class="account" onClick={() => setDropdownVisibility(!dropdownVisible)}> 129 - <Show when={props.loggedIn().loggedIn}> 130 - <div class="user-pfp" style={{ background: `url('https://cdn.phazed.xyz/id/avatars/${props.loggedIn().id}/${props.loggedIn().avatar}.png')` }}></div> 126 + <Show when={window.AccountManager.hasAccount()}> 127 + <div class="user-pfp" style={{ background: 128 + `url('https://cdn.phazed.xyz/id/avatars/${window.AccountManager.Profile()?.id}/${window.AccountManager.Profile()?.avatar}.png')` }}></div> 131 129 </Show> 132 130 <div class="icon"> 133 131 <img draggable="false" width="24" height="24" src="/icon/caret-down-solid.svg"></img> ··· 157 155 duration: 250 158 156 }) 159 157 160 - fetch('https://photos.phazed.xyz/api/v1/account?token='+ (await invoke('get_config_value_string', { key: 'token' }))!) 161 - .then(data => data.json()) 162 - .then(data => { 163 - if(!data.ok){ 164 - console.error(data); 165 - return; 166 - } 167 - 168 - console.log(data); 169 - props.setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 170 - }) 171 - .catch(e => { 172 - console.error(e); 173 - }) 174 - 175 158 setDropdownVisibility(false); 176 159 }}>Settings</div> 177 160 178 - <Show when={props.loggedIn().loggedIn == false} fallback={ 161 + <Show when={!window.AccountManager.hasAccount()} fallback={ 179 162 <div class="dropdown-button" onClick={async () => { 180 - fetch('https://photos.phazed.xyz/api/v1/deauth?token='+(await invoke('get_config_value_string', { key: 'token' }))!) 181 - .then(data => data.json()) 163 + window.AccountManager.logout() 182 164 .then(data => { 183 165 console.log(data); 184 - 185 - invoke('set_config_value_string', { key: 'token', value: '' }); 186 - window.location.reload(); 187 - 188 166 setDropdownVisibility(false); 189 167 }) 190 168 .catch(e => { ··· 198 176 }}>Sign Out</div> 199 177 }> 200 178 <div class="dropdown-button" onClick={() => { 201 - props.setLoadingType('auth'); 202 - 203 - setTimeout(() => { 204 - props.setLoadingType('none'); 205 - }, 5000); 206 - 207 - invoke('start_user_auth'); 208 - setDropdownVisibility(false); 179 + window.AccountManager.login(); 180 + setDropdownVisibility(false); 209 181 }}>Sign In</div> 210 182 </Show> 211 183 </div>
+34 -308
src/Components/PhotoList.tsx
··· 1 1 import { createEffect, onCleanup, onMount } from "solid-js"; 2 - import { invoke } from '@tauri-apps/api/core'; 3 2 import { listen } from '@tauri-apps/api/event'; 4 3 import { Window } from "@tauri-apps/api/window"; 5 4 6 5 import anime from "animejs"; 7 - import FilterMenu, { FilterType } from "./FilterMenu"; 8 - 9 - const PHOTO_HEIGHT = 200; 10 - const MAX_IMAGE_LOAD = 10; 6 + import FilterMenu from "./FilterMenu"; 7 + import { Photo } from "./Structs/Photo"; 11 8 12 9 let months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; 13 10 14 11 class PhotoListProps{ 15 12 setCurrentPhotoView!: ( view: any ) => any; 16 - setPhotoCount!: ( value: any ) => any; 17 - setPhotoSize!: ( value: any ) => any; 18 13 currentPhotoView!: () => any; 19 14 photoNavChoice!: () => string; 20 15 setPhotoNavChoice!: ( view: any ) => any; 21 - setConfirmationBox!: ( text: string, cb: () => void ) => void; 22 - requestPhotoReload!: () => boolean; 23 - setRequestPhotoReload!: ( val: boolean ) => boolean; 24 - storageInfo!: () => { storage: number, used: number, sync: boolean }; 25 - loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; 26 16 isPhotosSyncing!: () => boolean; 27 17 setIsPhotosSyncing!: ( syncing: boolean ) => boolean; 28 18 } ··· 33 23 } 34 24 35 25 let PhotoList = ( props: PhotoListProps ) => { 36 - let amountLoaded = 0; 37 - let imagesLoading = 0; 38 - 39 - let hasFirstLoaded = false; 40 - 41 26 let photoTreeLoadingContainer: HTMLElement; 42 27 43 28 let scrollToTop: HTMLElement; ··· 51 36 let ctx: CanvasRenderingContext2D; 52 37 let ctxBG: CanvasRenderingContext2D; 53 38 54 - let photos: Photo[] = []; 55 39 let currentPhotoIndex: number = -1; 56 40 57 41 let scroll: number = 0; 58 42 let targetScroll: number = 0; 59 43 60 44 let quitRender: boolean = true; 61 - let photoPath: string; 62 45 63 46 let currentPopup = ListPopup.NONE; 64 47 65 - let filterType: FilterType = FilterType.USER; 66 - let filter = ''; 67 - 68 - let filteredPhotos: Photo[] = []; 69 - 70 48 Window.getCurrent().isVisible().then(visible => { 71 49 quitRender = !visible; 72 50 }) ··· 93 71 94 72 break; 95 73 } 96 - } 97 - 98 - createEffect(() => { 99 - if(props.requestPhotoReload()){ 100 - props.setRequestPhotoReload(false); 101 - reloadPhotos(); 102 - } 103 - }) 104 - 105 - class PhotoMetadata{ 106 - width!: number; 107 - height!: number; 108 - metadata!: string; 109 - path!: string; 110 - } 111 - 112 - class Photo{ 113 - path: string; 114 - loaded: boolean = false; 115 - loading: boolean = false; 116 - metaLoaded: boolean = false; 117 - image?: HTMLCanvasElement; 118 - imageEl?: HTMLImageElement; 119 - width?: number; 120 - height?: number; 121 - loadingRotate: number = 0; 122 - metadata: any; 123 - 124 - frames: number = 0; 125 - shown: boolean = false; 126 - 127 - x: number = 0; 128 - y: number = 0; 129 - scaledWidth?: number; 130 - scaledHeight?: number; 131 - 132 - dateString: string; 133 - date: Date; 134 - 135 - legacy: boolean = false; 136 - 137 - public onMetaLoaded: () => void = () => {}; 138 - 139 - constructor( path: string, isLegacy: boolean = false ){ 140 - this.path = path; 141 - this.legacy = isLegacy; 142 - 143 - if(this.legacy) 144 - this.dateString = this.path.split('_')[2]; 145 - else 146 - this.dateString = this.path.split('_')[1]; 147 - 148 - let splitDateString = this.dateString.split('-'); 149 - 150 - this.date = new Date(); 151 - 152 - this.date.setFullYear(parseInt(splitDateString[0])); 153 - this.date.setMonth(parseInt(splitDateString[1])); 154 - this.date.setDate(parseInt(splitDateString[2])); 155 - } 156 - 157 - loadMeta(){ 158 - invoke('load_photo_meta', { photo: this.path }); 159 - } 160 - 161 - loadImage(){ 162 - if(this.loading || this.loaded || imagesLoading >= MAX_IMAGE_LOAD)return; 163 - 164 - this.loadMeta(); 165 - if(!this.metaLoaded)return; 166 - 167 - this.loading = true; 168 - 169 - imagesLoading++; 170 - 171 - this.image = document.createElement('canvas'); 172 - 173 - this.imageEl = document.createElement('img'); 174 - this.imageEl.crossOrigin = 'anonymous'; 175 - 176 - this.imageEl.src = (window.OS === "windows" ? "http://photo.localhost/" : "photo://localhost") + photoPath + this.path + "?downscale"; 177 - 178 - this.imageEl.onload = () => { 179 - this.image!.width = this.scaledWidth!; 180 - this.image!.height = this.scaledHeight!; 181 - 182 - this.image!.getContext('2d')!.drawImage(this.imageEl!, 0, 0, this.scaledWidth!, this.scaledHeight!); 183 - 184 - this.loaded = true; 185 - this.loading = false; 186 - 187 - imagesLoading--; 188 - } 189 - } 190 - } 74 + } 191 75 192 76 createEffect(() => { 193 77 let action = props.photoNavChoice(); 194 78 195 79 switch(action){ 196 80 case 'prev': 197 - if(!filteredPhotos[currentPhotoIndex - 1])break; 198 - props.setCurrentPhotoView(filteredPhotos[currentPhotoIndex - 1]); 81 + if(!window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex - 1])break; 82 + props.setCurrentPhotoView(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex - 1]); 199 83 200 84 currentPhotoIndex--; 201 85 break; 202 86 case 'next': 203 - if(!filteredPhotos[currentPhotoIndex + 1])break; 204 - props.setCurrentPhotoView(filteredPhotos[currentPhotoIndex + 1]); 87 + if(!window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex + 1])break; 88 + props.setCurrentPhotoView(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex + 1]); 205 89 206 90 currentPhotoIndex++; 207 91 break; ··· 211 95 }) 212 96 213 97 let render = () => { 98 + // TODO: Tidy this up, optimise it more 99 + // I am really procrastinating rewriting this... 100 + 214 101 if(!quitRender) 215 102 requestAnimationFrame(render); 216 103 else ··· 237 124 scroll = scroll + (targetScroll - scroll) * 0.2; 238 125 239 126 let lastPhoto; 240 - for (let i = 0; i < filteredPhotos.length; i++) { 241 - let p = filteredPhotos[i]; 127 + for (let i = 0; i < window.PhotoLoadingManager.FilteredPhotos.length; i++) { 128 + let p = window.PhotoLoadingManager.FilteredPhotos[i]; 242 129 243 130 if(currentRowIndex * 210 - scroll > photoContainer.height){ 244 131 p.shown = false; ··· 358 245 }) 359 246 } 360 247 361 - if(filteredPhotos.length == 0){ 248 + if(window.PhotoLoadingManager.FilteredPhotos.length == 0){ 362 249 ctx.textAlign = 'center'; 363 250 ctx.textBaseline = 'middle'; 364 251 ctx.globalAlpha = 1; ··· 381 268 console.log('Shown Window'); 382 269 quitRender = false; 383 270 384 - if(hasFirstLoaded) 271 + if(window.PhotoLoadingManager.HasFirstLoaded) 385 272 requestAnimationFrame(render); 386 273 }) 387 274 388 - listen('photo_meta_loaded', ( event: any ) => { 389 - let data: PhotoMetadata = event.payload; 390 - 391 - let photo = photos.find(x => x.path === data.path); 392 - if(!photo)return; 393 - 394 - photo.width = data.width; 395 - photo.height = data.height; 396 - 397 - let scale = PHOTO_HEIGHT / photo.height; 398 - 399 - photo.scaledWidth = photo.width * scale; 400 - photo.scaledHeight = PHOTO_HEIGHT; 401 - 402 - photo.metadata = data.metadata.split('\u0000').filter(x => x !== '')[1]; 403 - amountLoaded++; 404 - 405 - photo.metaLoaded = true; 406 - photo.onMetaLoaded(); 407 - 408 - reloadFilters(); 409 - 410 - if(amountLoaded === photos.length && !hasFirstLoaded){ 411 - filteredPhotos = photos; 412 - hasFirstLoaded = true; 413 - 414 - anime({ 415 - targets: photoTreeLoadingContainer, 416 - height: 0, 417 - easing: 'easeInOutQuad', 418 - duration: 500, 419 - opacity: 0, 420 - complete: () => { 421 - photoTreeLoadingContainer.style.display = 'none'; 422 - } 423 - }) 424 - 425 - anime({ 426 - targets: '.reload-photos', 427 - opacity: 1, 428 - duration: 150, 429 - easing: 'easeInOutQuad' 430 - }) 431 - 432 - render(); 433 - } 434 - }) 435 - 436 - listen('photo_create', async ( event: any ) => { 437 - let photo = new Photo(event.payload); 438 - 439 - photos.splice(0, 0, photo); 440 - photo.loadMeta(); 441 - 442 - if(!props.isPhotosSyncing() && props.storageInfo().sync){ 443 - props.setIsPhotosSyncing(true); 444 - invoke('sync_photos', { token: (await invoke('get_config_value_string', { key: 'token' })) }); 445 - } 446 - }) 447 - 448 - listen('photo_remove', ( event: any ) => { 449 - photos = photos.filter(x => x.path !== event.payload); 450 - filteredPhotos = filteredPhotos.filter(x => x.path !== event.payload); 451 - 452 - if(event.payload === props.currentPhotoView().path){ 453 - currentPhotoIndex = -1; 454 - props.setCurrentPhotoView(null); 455 - } 456 - }) 457 - 458 - let reloadPhotos = async () => { 459 - photoPath = await invoke('get_user_photos_path') + '/'; 460 - 461 - photoTreeLoadingContainer.style.opacity = '1'; 462 - photoTreeLoadingContainer.style.height = '100%'; 463 - photoTreeLoadingContainer.style.display = 'flex'; 464 - 465 - quitRender = true; 466 - amountLoaded = 0; 467 - scroll = 0; 468 - 469 - photos = []; 470 - filteredPhotos = []; 275 + window.PhotoLoadingManager.OnLoadingFinished(() => { 276 + anime({ 277 + targets: photoTreeLoadingContainer, 278 + height: 0, 279 + easing: 'easeInOutQuad', 280 + duration: 500, 281 + opacity: 0, 282 + complete: () => { 283 + photoTreeLoadingContainer.style.display = 'none'; 284 + } 285 + }) 471 286 472 287 anime({ 473 288 targets: '.reload-photos', 474 - opacity: 0, 289 + opacity: 1, 475 290 duration: 150, 476 291 easing: 'easeInOutQuad' 477 292 }) 478 293 479 - invoke('load_photos'); 480 - } 481 - 482 - let loadPhotos = async () => { 483 - photoPath = await invoke('get_user_photos_path') + '/'; 484 - 485 - listen('photos_loaded', ( event: any ) => { 486 - let photoPaths = event.payload.photos.reverse(); 487 - console.log(photoPaths); 488 - 489 - props.setPhotoCount(photoPaths.length); 490 - props.setPhotoSize(event.payload.size); 491 - 492 - let doesHaveLegacy = false; 493 - 494 - photoPaths.forEach(( path: string ) => { 495 - let photo 496 - 497 - if(path.slice(0, 9) === "legacy://"){ 498 - photo = new Photo(path.slice(9), true); 499 - doesHaveLegacy = true; 500 - } else 501 - photo = new Photo(path, false); 502 - 503 - photos.push(photo); 504 - photo.loadMeta(); 505 - }) 506 - 507 - if(doesHaveLegacy){ 508 - photos = photos.sort(( a, b ) => b.date.valueOf() - a.date.valueOf()); 509 - } 510 - 511 - console.log(photos.length + ' Photos found.'); 512 - if(photos.length === 0){ 513 - console.log('No photos found, Skipping loading stage.'); 514 - 515 - filteredPhotos = photos; 516 - hasFirstLoaded = true; 517 - 518 - anime({ 519 - targets: photoTreeLoadingContainer, 520 - height: 0, 521 - easing: 'easeInOutQuad', 522 - duration: 500, 523 - opacity: 0, 524 - complete: () => { 525 - photoTreeLoadingContainer.style.display = 'none'; 526 - } 527 - }) 528 - 529 - anime({ 530 - targets: '.reload-photos', 531 - opacity: 1, 532 - duration: 150, 533 - easing: 'easeInOutQuad' 534 - }) 535 - 536 - render(); 537 - } 538 - }); 539 - 540 - invoke('load_photos'); 541 - } 294 + render(); 295 + }); 542 296 543 297 onMount(() => { 544 298 ctx = photoContainer.getContext('2d')!; 545 299 ctxBG = photoContainerBG.getContext('2d')!; 546 - loadPhotos(); 300 + 301 + window.PhotoLoadingManager.Load(); 547 302 548 303 anime.set(scrollToTop, { opacity: 0, translateY: '-10px', display: 'none' }); 549 304 ··· 571 326 }) 572 327 573 328 photoContainer.addEventListener('click', ( e: MouseEvent ) => { 574 - let photo = filteredPhotos.find(x => 329 + let photo = window.PhotoLoadingManager.FilteredPhotos.find(x => 575 330 e.clientX > x.x && 576 331 e.clientY > x.y && 577 332 e.clientX < x.x + x.scaledWidth! && ··· 581 336 582 337 if(photo){ 583 338 props.setCurrentPhotoView(photo); 584 - currentPhotoIndex = filteredPhotos.indexOf(photo); 339 + currentPhotoIndex = window.PhotoLoadingManager.FilteredPhotos.indexOf(photo); 585 340 } else 586 341 currentPhotoIndex = -1; 587 342 }) ··· 591 346 window.removeEventListener('keyup', closeWithKey); 592 347 }) 593 348 594 - let reloadFilters = () => { 595 - filteredPhotos = []; 596 - 597 - switch(filterType){ 598 - case FilterType.USER: 599 - photos.map(p => { 600 - if(p.metadata){ 601 - let meta = JSON.parse(p.metadata); 602 - let photo = meta.players.find(( y: any ) => y.displayName.toLowerCase().includes(filter) || y.id === filter); 603 - 604 - if(photo)filteredPhotos.push(p); 605 - } 606 - }) 607 - break; 608 - case FilterType.WORLD: 609 - photos.map(p => { 610 - if(p.metadata){ 611 - let meta = JSON.parse(p.metadata); 612 - let photo = meta.world.name.toLowerCase().includes(filter) || meta.world.id === filter; 613 - 614 - if(photo)filteredPhotos.push(p); 615 - } 616 - }) 617 - break; 618 - } 619 - } 620 - 621 349 return ( 622 350 <div class="photo-list"> 623 351 <div ref={filterContainer!} class="filter-container"> 624 - <FilterMenu 625 - setFilter={( f ) => { filter = f.toLowerCase(); reloadFilters(); }} 626 - setFilterType={( t ) => { filterType = t; reloadFilters(); }} /> 352 + <FilterMenu /> 627 353 </div> 628 354 629 355 <div class="photo-tree-loading" ref={( el ) => photoTreeLoadingContainer = el}>Scanning Photo Tree...</div> ··· 633 359 <img draggable="false" src="/icon/angle-up-solid.svg"></img> 634 360 </div> 635 361 </div> 636 - <div class="reload-photos" onClick={() => props.setConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", () => window.location.reload())}> 362 + <div class="reload-photos" onClick={() => window.ConfirmationBoxManager.SetConfirmationBox("Are you sure you want to reload all photos? This can cause the application to slow down while it is loading...", () => window.location.reload())}> 637 363 <div class="icon" style={{ width: '17px' }}> 638 364 <img draggable="false" width="17" height="17" src="/icon/arrows-rotate-solid.svg"></img> 639 365 </div>
+2 -5
src/Components/PhotoViewer.tsx
··· 7 7 currentPhotoView!: () => any; 8 8 setCurrentPhotoView!: ( view: any ) => any; 9 9 setPhotoNavChoice!: ( view: any ) => any; 10 - setConfirmationBox!: ( text: string, cb: () => void ) => void; 11 - storageInfo!: () => { storage: number, used: number, sync: boolean }; 12 - loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; 13 10 } 14 11 15 12 class WorldCache{ ··· 539 536 <div class="viewer-button" 540 537 onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })} 541 538 onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })} 542 - onClick={() => props.setConfirmationBox("Are you sure you want to delete this photo?", async () => { invoke("delete_photo", { 539 + onClick={() => window.ConfirmationBoxManager.SetConfirmationBox("Are you sure you want to delete this photo?", async () => { invoke("delete_photo", { 543 540 path: props.currentPhotoView().path, 544 541 token: (await invoke('get_config_value_string', { key: 'token' })) || "none", 545 - isSyncing: props.loggedIn().loggedIn ? props.storageInfo().sync : false 542 + isSyncing: window.AccountManager.hasAccount() ? window.AccountManager.Storage()?.isSyncing : false 546 543 }); 547 544 })}> 548 545 <div class="icon" style={{ width: '12px', margin: '0' }}>
+26 -61
src/Components/SettingsMenu.tsx
··· 2 2 import { bytesToFormatted } from "../utils"; 3 3 import { invoke } from '@tauri-apps/api/core'; 4 4 import anime from "animejs"; 5 - import { fetch } from "@tauri-apps/plugin-http" 6 5 7 - class SettingsMenuProps{ 8 - setLoadingType!: ( type: string ) => string; 9 - photoCount!: () => number; 10 - photoSize!: () => number; 11 - setRequestPhotoReload!: ( val: boolean ) => boolean; 12 - loggedIn!: () => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; 13 - storageInfo!: () => { storage: number, used: number, sync: boolean }; 14 - setStorageInfo!: ( info: { storage: number, used: number, sync: boolean } ) => { storage: number, used: number, sync: boolean }; 15 - setConfirmationBox!: ( text: string, cb: () => void ) => void; 16 - setLoggedIn!: ( val: { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string } ) => { loggedIn: boolean, username: string, avatar: string, id: string, serverVersion: string }; 17 - } 18 - 19 - let SettingsMenu = ( props: SettingsMenuProps ) => { 6 + let SettingsMenu = () => { 20 7 let sliderBar: HTMLElement; 21 8 let settingsContainer: HTMLElement; 22 9 let currentButton = 0; ··· 191 178 window.removeEventListener('keyup', closeWithKey); 192 179 }) 193 180 194 - let refreshAccount = async () => { 195 - fetch('https://photos.phazed.xyz/api/v1/account?token='+(await invoke('get_config_value_string', { key: 'token' }))!) 196 - .then(data => data.json()) 197 - .then(data => { 198 - if(!data.ok){ 199 - console.error(data); 200 - return; 201 - } 202 - 203 - console.log(data); 204 - props.setLoggedIn({ loggedIn: true, username: data.user.username, avatar: data.user.avatar, id: data.user._id, serverVersion: data.user.serverVersion }); 205 - props.setStorageInfo({ storage: data.user.storage, used: data.user.used, sync: data.user.settings.enableSync }); 206 - }) 207 - .catch(e => { 208 - console.error(e); 209 - }) 210 - } 211 - 212 181 return ( 213 182 <div class="settings"> 214 183 <div class="settings-container" ref={( el ) => settingsContainer = el}> 215 184 <div class="settings-block"> 216 185 <h1>Storage Settings</h1> 217 - <p>{ props.photoCount() } Photos ({ bytesToFormatted(props.photoSize(), 0) })</p> 186 + <p>{ window.PhotoLoadingManager.PhotoCount() } Photos ({ bytesToFormatted(window.PhotoLoadingManager.PhotoSize(), 0) })</p> 218 187 219 188 <div class="selector"> 220 189 <input type="checkbox" id="start-in-bg-check" ref={async ( el ) => { ··· 322 291 } 323 292 }) 324 293 325 - props.setRequestPhotoReload(true); 294 + window.location.reload(); 326 295 }}> 327 296 Save 328 297 </span> ··· 348 317 <div class="settings-block"> 349 318 <h1>Account Settings</h1> 350 319 351 - <Show when={props.loggedIn().loggedIn} fallback={ 320 + <Show when={window.AccountManager.hasAccount()} fallback={ 352 321 <div> 353 322 You aren't logged in. To enable cloud sync and sharing features you need to login to your PhazeID.<br /><br /> 354 323 <div class="button" onClick={() => { 355 - props.setLoadingType('auth'); 356 - 357 - setTimeout(() => { 358 - props.setLoadingType('none'); 359 - }, 5000); 360 - 361 - invoke('start_user_auth'); 324 + window.AccountManager.login(); 362 325 }}>Login</div> 363 326 </div> 364 327 }> 365 328 <div class="account-profile"> 366 - <div class="account-pfp" style={{ background: `url('https://cdn.phazed.xyz/id/avatars/${props.loggedIn().id}/${props.loggedIn().avatar}.png')` }}></div> 329 + <div class="account-pfp" style={{ background: `url('https://cdn.phazed.xyz/id/avatars/${window.AccountManager.Profile()?.id}/${window.AccountManager.Profile()?.avatar}.png')` }}></div> 367 330 <div class="account-desc"> 368 - <div class="reload-photos" onClick={() => refreshAccount()} style={{ opacity: 1 }}> 331 + <div class="reload-photos" onClick={() => window.AccountManager.Refresh()} style={{ opacity: 1 }}> 369 332 <div class="icon" style={{ width: '17px' }}> 370 333 <img draggable="false" width="17" height="17" src="/icon/arrows-rotate-solid.svg"></img> 371 334 </div> 372 335 </div> 373 - <h2>{ props.loggedIn().username }</h2> 336 + <h2>{ window.AccountManager.Profile()?.username }</h2> 374 337 375 - <Show when={props.storageInfo().sync}> 338 + <Show when={window.AccountManager.Storage()?.isSyncing}> 376 339 <div class="storage-bar"> 377 - <div class="storage-bar-inner" style={{ width: ((props.storageInfo().used / props.storageInfo().storage) * 100) + '%' }}></div> 340 + <div class="storage-bar-inner" style={{ width: ((window.AccountManager.Storage()!.used / window.AccountManager.Storage()!.total) * 100) + '%' }}></div> 378 341 </div> 379 342 380 343 <div> 381 - { bytesToFormatted(props.storageInfo().used, 0) } / { bytesToFormatted(props.storageInfo().storage, 0) }<br /><br /> 344 + { bytesToFormatted(window.AccountManager.Storage()!.used, 0) } / { bytesToFormatted(window.AccountManager.Storage()!.total, 0) }<br /><br /> 382 345 383 - <span style={{ 'font-size': '10px' }}>Server Version: { props.loggedIn().serverVersion }</span> 346 + <span style={{ 'font-size': '10px' }}>Server Version: { window.AccountManager.Profile()?.serverVersion }</span> 384 347 </div> 385 348 </Show> 386 349 </div> ··· 390 353 391 354 <div class="account-notice" style={{ display: 'flex' }}> 392 355 <Show when={!deletingPhotos()} fallback={ "We are deleting your photos, please leave this window open while we delete them." }> 393 - <div class="button-danger" onClick={() => props.setConfirmationBox("You are about to delete all your photos from the cloud, and disable syncing. This will NOT delete any local files.", async () => { 394 - props.setStorageInfo({ used: 0, storage: 0, sync: false }); 395 - setDeletingPhotos(true); 356 + <div class="button-danger" onClick={() => window.ConfirmationBoxManager.SetConfirmationBox("You are about to delete all your photos from the cloud, and disable syncing. This will NOT delete any local files.", async () => { 357 + // TODO: Rework all of this 358 + 359 + // props.setStorageInfo({ used: 0, storage: 0, sync: false }); 360 + // setDeletingPhotos(true); 396 361 397 - fetch('https://photos-cdn.phazed.xyz/api/v1/allphotos', { 398 - method: 'DELETE', 399 - headers: { auth: (await invoke('get_config_value_string', { key: 'token' }))! } 400 - }) 401 - .then(data => data.json()) 402 - .then(data => { 403 - console.log(data); 404 - setDeletingPhotos(false); 405 - }) 362 + // fetch('https://photos-cdn.phazed.xyz/api/v1/allphotos', { 363 + // method: 'DELETE', 364 + // headers: { auth: (await invoke('get_config_value_string', { key: 'token' }))! } 365 + // }) 366 + // .then(data => data.json()) 367 + // .then(data => { 368 + // console.log(data); 369 + // setDeletingPhotos(false); 370 + // }) 406 371 })}>Delete All Photos.</div> <div>This deletes all photos stored in the cloud and disables syncing.</div> 407 372 </Show> 408 373 </div>
+3
src/Components/Structs/FilterType.ts
··· 1 + export enum FilterType{ 2 + USER, WORLD 3 + }
+84
src/Components/Structs/Photo.ts
··· 1 + import { invoke } from "@tauri-apps/api/core"; 2 + import { Vars } from "./Vars"; 3 + 4 + let imagesLoading = 0; 5 + 6 + export class Photo{ 7 + path: string; 8 + loaded: boolean = false; 9 + loading: boolean = false; 10 + metaLoaded: boolean = false; 11 + image?: HTMLCanvasElement; 12 + imageEl?: HTMLImageElement; 13 + width?: number; 14 + height?: number; 15 + loadingRotate: number = 0; 16 + metadata: any; 17 + 18 + frames: number = 0; 19 + shown: boolean = false; 20 + 21 + x: number = 0; 22 + y: number = 0; 23 + scaledWidth?: number; 24 + scaledHeight?: number; 25 + 26 + dateString: string; 27 + date: Date; 28 + 29 + legacy: boolean = false; 30 + 31 + public onMetaLoaded: () => void = () => {}; 32 + 33 + constructor( path: string, isLegacy: boolean = false ){ 34 + this.path = path; 35 + this.legacy = isLegacy; 36 + 37 + if(this.legacy) 38 + this.dateString = this.path.split('_')[2]; 39 + else 40 + this.dateString = this.path.split('_')[1]; 41 + 42 + let splitDateString = this.dateString.split('-'); 43 + 44 + this.date = new Date(); 45 + 46 + this.date.setFullYear(parseInt(splitDateString[0])); 47 + this.date.setMonth(parseInt(splitDateString[1])); 48 + this.date.setDate(parseInt(splitDateString[2])); 49 + } 50 + 51 + loadMeta(){ 52 + invoke('load_photo_meta', { photo: this.path }); 53 + } 54 + 55 + loadImage(){ 56 + if(this.loading || this.loaded || imagesLoading >= Vars.MAX_IMAGE_LOAD)return; 57 + 58 + this.loadMeta(); 59 + if(!this.metaLoaded)return; 60 + 61 + this.loading = true; 62 + 63 + imagesLoading++; 64 + 65 + this.image = document.createElement('canvas'); 66 + 67 + this.imageEl = document.createElement('img'); 68 + this.imageEl.crossOrigin = 'anonymous'; 69 + 70 + this.imageEl.src = (window.OS === "windows" ? "http://photo.localhost/" : "photo://localhost") + this.path + "?downscale"; 71 + 72 + this.imageEl.onload = () => { 73 + this.image!.width = this.scaledWidth!; 74 + this.image!.height = this.scaledHeight!; 75 + 76 + this.image!.getContext('2d')!.drawImage(this.imageEl!, 0, 0, this.scaledWidth!, this.scaledHeight!); 77 + 78 + this.loaded = true; 79 + this.loading = false; 80 + 81 + imagesLoading--; 82 + } 83 + } 84 + }
+6
src/Components/Structs/PhotoMetadata.ts
··· 1 + export class PhotoMetadata{ 2 + width!: number; 3 + height!: number; 4 + metadata!: string; 5 + path!: string; 6 + }
+6
src/Components/Structs/ProfileData.ts
··· 1 + export class ProfileData{ 2 + id!: string; 3 + username!: string; 4 + avatar!: string; 5 + serverVersion!: number; 6 + }
+5
src/Components/Structs/StorageData.ts
··· 1 + export class StorageData{ 2 + used!: number; 3 + total!: number; 4 + isSyncing!: boolean; 5 + }
+4
src/Components/Structs/Vars.ts
··· 1 + export class Vars{ 2 + static MAX_IMAGE_LOAD = 10; 3 + static PHOTO_HEIGHT = 200; 4 + }
+19 -1
src/index.tsx
··· 3 3 4 4 declare global{ 5 5 interface Window { 6 - CloseAllPopups: (() => void)[] 6 + AccountManager: AccountManager; 7 + LoadingManager: LoadingManager; 8 + PhotoLoadingManager: PhotoLoadingManager; 9 + ConfirmationBoxManager: ConfirmationBoxManager; 10 + PhotoViewerManager: PhotoViewerManager; 11 + 12 + CloseAllPopups: (() => void)[]; 7 13 OS: string; 8 14 } 9 15 } ··· 15 21 import "./styles.css"; 16 22 import App from "./Components/App"; 17 23 import { invoke } from "@tauri-apps/api/core"; 24 + 25 + import { AccountManager } from "./Components/Managers/AccountManager"; 26 + import { LoadingManager } from "./Components/Managers/LoadingManager"; 27 + import { PhotoLoadingManager } from "./Components/Managers/PhotoLoadingManager"; 28 + import { ConfirmationBoxManager } from "./Components/Managers/ConfirmationBoxManager"; 29 + import { PhotoViewerManager } from "./Components/Managers/PhotoViewerManager"; 30 + 31 + window.AccountManager = new AccountManager(); 32 + window.LoadingManager = new LoadingManager(); 33 + window.PhotoLoadingManager = new PhotoLoadingManager(); 34 + window.ConfirmationBoxManager = new ConfirmationBoxManager(); 35 + window.PhotoViewerManager = new PhotoViewerManager(); 18 36 19 37 (async () => { 20 38 window.OS = await invoke('get_os');