A photo manager for VRChat.

tidying up frontend

+12 -12
src/Components/Managers/ConfirmationBoxManager.tsx
··· 9 9 this._setConfirmationBoxText = setConfirmationBoxText; 10 10 11 11 let confirmationBox: HTMLElement; 12 - 12 + 13 + document.body.appendChild(<div class="confirmation-box" ref={( el ) => confirmationBox = el}> 14 + <div class="confirmation-box-container"> 15 + { confirmationBoxText() }<br /><br /> 16 + 17 + <div class="button-danger" onClick={() => { this._confirmationBoxCallback(); setConfirmationBoxText('') }}>Confirm</div> 18 + <div class="button" onClick={() => setConfirmationBoxText('') }>Deny</div> 19 + </div> 20 + </div> as HTMLElement); 21 + 13 22 createEffect(() => { 14 23 if(confirmationBoxText() !== ''){ 15 24 confirmationBox.style.display = 'block'; 16 - 25 + 17 26 setTimeout(() => { 18 27 confirmationBox.style.opacity = '1'; 19 28 }, 1); 20 29 } else{ 21 30 confirmationBox.style.opacity = '0'; 22 - 31 + 23 32 setTimeout(() => { 24 33 confirmationBox.style.display = 'none'; 25 34 }, 250); 26 35 } 27 36 }) 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 37 } 38 38 39 39 public SetConfirmationBox( text: string, cb: () => void ){
+2 -3
src/Components/Managers/PhotoViewerManager.tsx
··· 2 2 import { Photo } from "../Structs/Photo"; 3 3 4 4 export class PhotoViewerManager{ 5 - private _currentPhoto: Accessor<Photo | null>; 6 - 5 + public CurrentPhoto: Accessor<Photo | null>; 7 6 private _setCurrentPhoto: Setter<Photo | null>; 8 7 9 8 constructor(){ 10 - [ this._currentPhoto, this._setCurrentPhoto ] = createSignal<Photo | null>(null); 9 + [ this.CurrentPhoto, this._setCurrentPhoto ] = createSignal<Photo | null>(null); 11 10 } 12 11 13 12 public Close(){
+61
src/Components/Managers/WorldCacheManager.tsx
··· 1 + import { invoke } from "@tauri-apps/api/core"; 2 + import { WorldCache } from "../Structs/WorldCache"; 3 + import { listen } from "@tauri-apps/api/event"; 4 + 5 + export class WorldCacheManager{ 6 + private _worldCache: WorldCache[] = []; 7 + private _resolveWorld: ( world: WorldCache | null ) => void = () => {}; 8 + 9 + constructor(){ 10 + invoke('get_config_value_string', { key: 'worldcache' }) 11 + .then((data: any) => { 12 + if(data)this._worldCache = JSON.parse(data); 13 + }) 14 + 15 + listen('world_data', ( event: any ) => { 16 + let worldData = { 17 + expiresOn: Date.now() + 1.2096E+09, 18 + worldData: { 19 + id: event.payload.id, 20 + name: event.payload.name.split('\\').join('').slice(1, -1), 21 + author: event.payload.author.split('\\').join('').slice(1, -1), 22 + authorId: event.payload.authorId.split('\\').join('').slice(1, -1), 23 + desc: event.payload.desc.split('\\').join('').slice(1, -1), 24 + img: event.payload.img.split('\\').join('').slice(1, -1), 25 + maxUsers: event.payload.maxUsers, 26 + visits: event.payload.visits, 27 + favourites: event.payload.favourites, 28 + tags: event.payload.tags, 29 + from: event.payload.from, 30 + fromSite: event.payload.fromSite, 31 + found: event.payload.found 32 + } 33 + } 34 + 35 + this._worldCache.push(worldData); 36 + invoke('set_config_value_string', { key: 'worldcache', value: JSON.stringify(this._worldCache) }); 37 + 38 + this._resolveWorld(worldData); 39 + }) 40 + } 41 + 42 + getWorldById( id: string ): Promise<WorldCache | null>{ 43 + let promise = new Promise<WorldCache | null>(( res ) => { this._resolveWorld = res }); 44 + let worldData = this._worldCache.find(x => x.worldData.id === id); 45 + 46 + if(!worldData){ 47 + console.log('Fetching new world data'); 48 + 49 + invoke('find_world_by_id', { worldId: id }); 50 + } else if(worldData.expiresOn < Date.now()){ 51 + console.log('Fetching new world data since cache has expired'); 52 + 53 + this._worldCache = this._worldCache.filter(x => x.worldData.id !== id); 54 + invoke('find_world_by_id', { worldId: id }); 55 + } else{ 56 + this._resolveWorld(worldData); 57 + } 58 + 59 + return promise; 60 + } 61 + }
+3 -5
src/Components/PhotoList.tsx
··· 9 9 let months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; 10 10 11 11 class PhotoListProps{ 12 - setCurrentPhotoView!: ( view: any ) => any; 13 - currentPhotoView!: () => any; 14 12 photoNavChoice!: () => string; 15 13 setPhotoNavChoice!: ( view: any ) => any; 16 14 isPhotosSyncing!: () => boolean; ··· 79 77 switch(action){ 80 78 case 'prev': 81 79 if(!window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex - 1])break; 82 - props.setCurrentPhotoView(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex - 1]); 80 + window.PhotoViewerManager.OpenPhoto(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex - 1]); 83 81 84 82 currentPhotoIndex--; 85 83 break; 86 84 case 'next': 87 85 if(!window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex + 1])break; 88 - props.setCurrentPhotoView(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex + 1]); 86 + window.PhotoViewerManager.OpenPhoto(window.PhotoLoadingManager.FilteredPhotos[currentPhotoIndex + 1]); 89 87 90 88 currentPhotoIndex++; 91 89 break; ··· 335 333 ); 336 334 337 335 if(photo){ 338 - props.setCurrentPhotoView(photo); 336 + window.PhotoViewerManager.OpenPhoto(photo); 339 337 currentPhotoIndex = window.PhotoLoadingManager.FilteredPhotos.indexOf(photo); 340 338 } else 341 339 currentPhotoIndex = -1;
+51 -152
src/Components/PhotoViewer.tsx
··· 1 1 import { For, Show, createEffect, onCleanup, onMount } from "solid-js"; 2 2 import { invoke } from '@tauri-apps/api/core'; 3 - import { listen } from '@tauri-apps/api/event'; 4 3 import anime from 'animejs'; 4 + import { WorldCache } from "./Structs/WorldCache"; 5 5 6 6 class PhotoViewerProps{ 7 - currentPhotoView!: () => any; 8 - setCurrentPhotoView!: ( view: any ) => any; 9 7 setPhotoNavChoice!: ( view: any ) => any; 10 8 } 11 9 12 - class WorldCache{ 13 - expiresOn!: number; 14 - worldData!: { 15 - id: string, 16 - name: string, 17 - author: string, 18 - authorId: string, 19 - desc: string, 20 - img: string, 21 - maxUsers: number, 22 - visits: number, 23 - favourites: number, 24 - tags: any, 25 - from: string, 26 - fromSite: string, 27 - found: boolean 28 - } 29 - } 30 - 31 - let worldCache: WorldCache[] = []; 32 - 33 - invoke('get_config_value_string', { key: 'worldcache' }) 34 - .then((data: any) => { 35 - if(data)worldCache = JSON.parse(data); 36 - }) 37 - 38 10 let PhotoViewer = ( props: PhotoViewerProps ) => { 39 11 let viewer: HTMLElement; 40 12 let imageViewer: HTMLImageElement; ··· 48 20 let photoTrayCloseBtn: HTMLElement; 49 21 50 22 let worldInfoContainer: HTMLElement; 51 - let photoPath: string; 52 23 53 24 let viewerContextMenu: HTMLElement; 54 25 let viewerContextMenuButtons: HTMLElement[] = []; ··· 59 30 let switchPhotoWithKey = ( e: KeyboardEvent ) => { 60 31 switch(e.key){ 61 32 case 'Escape': 62 - props.setCurrentPhotoView(null); 33 + window.PhotoViewerManager.Close(); 63 34 64 35 break; 65 36 case 'ArrowUp': ··· 114 85 }) 115 86 } 116 87 88 + let copyImage = () => { 89 + let canvas = document.createElement('canvas'); 90 + let ctx = canvas.getContext('2d')!; 91 + 92 + canvas.width = window.PhotoViewerManager.CurrentPhoto()?.width || 0; 93 + canvas.height = window.PhotoViewerManager.CurrentPhoto()?.height || 0; 94 + 95 + ctx.drawImage(imageViewer, 0, 0); 96 + 97 + canvas.toBlob(( blob ) => { 98 + navigator.clipboard.write([ 99 + new ClipboardItem({ 100 + 'image/png': blob! 101 + }) 102 + ]); 103 + 104 + canvas.remove(); 105 + 106 + anime.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 107 + anime({ 108 + targets: '.copy-notif', 109 + opacity: 1, 110 + translateY: '0px' 111 + }); 112 + 113 + setTimeout(() => { 114 + anime({ 115 + targets: '.copy-notif', 116 + opacity: 0, 117 + translateY: '-100px' 118 + }); 119 + }, 2000); 120 + }); 121 + } 122 + 117 123 let closeTray = () => { 118 124 if(!trayOpen || trayInAnimation)return; 119 125 trayInAnimation = true; ··· 171 177 window.CloseAllPopups.forEach(p => p()); 172 178 // Context Menu -> Open file location 173 179 174 - let path = await invoke('get_user_photos_path') + '\\' + props.currentPhotoView().path; 180 + let path = await invoke('get_user_photos_path') + '\\' + window.PhotoViewerManager.CurrentPhoto()?.path; 175 181 invoke('open_folder', { url: path }); 176 182 } 177 183 178 184 viewerContextMenuButtons[1].onclick = () => { 179 185 window.CloseAllPopups.forEach(p => p()); 180 186 // Context Menu -> Copy image 181 - 182 - let canvas = document.createElement('canvas'); 183 - let ctx = canvas.getContext('2d')!; 184 - 185 - canvas.width = props.currentPhotoView().width; 186 - canvas.height = props.currentPhotoView().height; 187 - 188 - ctx.drawImage(imageViewer, 0, 0); 189 - 190 - canvas.toBlob(( blob ) => { 191 - navigator.clipboard.write([ 192 - new ClipboardItem({ 193 - 'image/png': blob! 194 - }) 195 - ]); 196 - 197 - canvas.remove(); 198 - 199 - anime.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 200 - anime({ 201 - targets: '.copy-notif', 202 - opacity: 1, 203 - translateY: '0px' 204 - }); 205 - 206 - setTimeout(() => { 207 - anime({ 208 - targets: '.copy-notif', 209 - opacity: 0, 210 - translateY: '-100px' 211 - }); 212 - }, 2000); 213 - }); 187 + copyImage(); 214 188 } 215 189 216 190 imageViewer.oncontextmenu = ( e ) => { ··· 249 223 } 250 224 251 225 createEffect(() => { 252 - let photo = props.currentPhotoView(); 226 + let photo = window.PhotoViewerManager.CurrentPhoto(); 253 227 allowedToOpenTray = false; 254 228 255 229 imageViewer.style.opacity = '0'; 256 230 257 231 if(photo){ 258 - (async () => { 259 - if(!photoPath) 260 - photoPath = await invoke('get_user_photos_path') + '/'; 261 - 262 - imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost') + (photoPath + props.currentPhotoView().path).split('\\').join('/') + "?full"; 263 - imageViewer.crossOrigin = 'anonymous'; 264 - })(); 232 + imageViewer.src = (window.OS === "windows" ? "http://photo.localhost/" : 'photo://localhost') + window.PhotoViewerManager.CurrentPhoto()?.path.split('\\').join('/') + "?full"; 233 + imageViewer.crossOrigin = 'anonymous'; 265 234 266 235 anime({ 267 236 targets: imageViewer, ··· 276 245 photo.onMetaLoaded = () => {} 277 246 278 247 let meta = JSON.parse(photo.metadata); 279 - let worldData = worldCache.find(x => x.worldData.id === meta.world.id); 280 248 281 249 allowedToOpenTray = true; 282 250 trayButton.style.display = 'flex'; ··· 306 274 </div> as Node 307 275 ); 308 276 309 - if(!worldData){ 310 - console.log('Fetching new world data'); 311 - 312 - invoke('find_world_by_id', { worldId: meta.world.id }); 313 - } else if(worldData.expiresOn < Date.now()){ 314 - console.log('Fetching new world data since cache has expired'); 315 - 316 - worldCache = worldCache.filter(x => x.worldData.id !== meta.world.id) 317 - invoke('find_world_by_id', { worldId: meta.world.id }); 318 - } else 319 - loadWorldData(worldData); 277 + window.WorldCacheManager.getWorldById(meta.world.id) 278 + .then(worldData => { 279 + if(worldData) 280 + loadWorldData(worldData); 281 + }); 320 282 } else{ 321 283 trayButton.style.display = 'none'; 322 284 closeTray(); 323 285 } 324 286 } 325 287 326 - photo.onMetaLoaded = () => handleMetaDataLoaded(); 327 288 handleMetaDataLoaded(); 328 - 329 - photo.loadImage(); 330 289 } 331 290 332 291 if(photo && !isOpen){ ··· 382 341 }) 383 342 384 343 let loadWorldData = ( data: WorldCache ) => { 385 - let meta = props.currentPhotoView().metadata; 344 + let meta = window.PhotoViewerManager.CurrentPhoto()?.metadata; 386 345 if(!meta)return; 387 346 388 347 worldInfoContainer.innerHTML = ''; ··· 411 370 ) 412 371 } 413 372 414 - listen('world_data', ( event: any ) => { 415 - let worldData = { 416 - expiresOn: Date.now() + 1.2096E+09, 417 - worldData: { 418 - id: event.payload.id, 419 - name: event.payload.name.split('\\').join('').slice(1, -1), 420 - author: event.payload.author.split('\\').join('').slice(1, -1), 421 - authorId: event.payload.authorId.split('\\').join('').slice(1, -1), 422 - desc: event.payload.desc.split('\\').join('').slice(1, -1), 423 - img: event.payload.img.split('\\').join('').slice(1, -1), 424 - maxUsers: event.payload.maxUsers, 425 - visits: event.payload.visits, 426 - favourites: event.payload.favourites, 427 - tags: event.payload.tags, 428 - from: event.payload.from, 429 - fromSite: event.payload.fromSite, 430 - found: event.payload.found 431 - } 432 - } 433 - 434 - worldCache.push(worldData); 435 - invoke('set_config_value_string', { key: 'worldcache', value: JSON.stringify(worldCache) }); 436 - 437 - loadWorldData(worldData); 438 - }) 439 - 440 373 return ( 441 374 <div class="photo-viewer" ref={( el ) => viewer = el}> 442 375 <div class="photo-context-menu" ref={( el ) => viewerContextMenu = el}> ··· 444 377 <div ref={( el ) => viewerContextMenuButtons.push(el)}>Copy image</div> 445 378 </div> 446 379 447 - <div class="viewer-close viewer-button" onClick={() => props.setCurrentPhotoView(null)}> 380 + <div class="viewer-close viewer-button" onClick={() => window.PhotoViewerManager.Close()}> 448 381 <div class="icon" style={{ width: '10px', margin: '0' }}> 449 382 <img draggable="false" src="/icon/x-solid.svg"></img> 450 383 </div> ··· 484 417 <div class="viewer-button" 485 418 onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })} 486 419 onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })} 487 - onClick={() => { 488 - let canvas = document.createElement('canvas'); 489 - let ctx = canvas.getContext('2d')!; 490 - 491 - canvas.width = props.currentPhotoView().width; 492 - canvas.height = props.currentPhotoView().height; 493 - 494 - ctx.drawImage(imageViewer, 0, 0); 495 - 496 - canvas.toBlob(( blob ) => { 497 - navigator.clipboard.write([ 498 - new ClipboardItem({ 499 - 'image/png': blob! 500 - }) 501 - ]); 502 - 503 - canvas.remove(); 504 - 505 - anime.set('.copy-notif', { translateX: '-50%', translateY: '-100px' }); 506 - anime({ 507 - targets: '.copy-notif', 508 - opacity: 1, 509 - translateY: '0px' 510 - }); 511 - 512 - setTimeout(() => { 513 - anime({ 514 - targets: '.copy-notif', 515 - opacity: 0, 516 - translateY: '-100px' 517 - }); 518 - }, 2000); 519 - }); 520 - }} 521 - > 420 + onClick={() => { copyImage(); }}> 522 421 <div class="icon" style={{ width: '12px', margin: '0' }}> 523 422 <img draggable="false" src="/icon/copy-solid.svg"></img> 524 423 </div> ··· 537 436 onMouseOver={( el ) => anime({ targets: el.currentTarget, width: '40px', height: '40px', 'margin-left': '15px', 'margin-right': '15px', 'margin-top': '-10px' })} 538 437 onMouseLeave={( el ) => anime({ targets: el.currentTarget, width: '30px', height: '30px', 'margin-left': '20px', 'margin-right': '20px', 'margin-top': '0px' })} 539 438 onClick={() => window.ConfirmationBoxManager.SetConfirmationBox("Are you sure you want to delete this photo?", async () => { invoke("delete_photo", { 540 - path: props.currentPhotoView().path, 439 + path: window.PhotoViewerManager.CurrentPhoto()?.path, 541 440 token: (await invoke('get_config_value_string', { key: 'token' })) || "none", 542 441 isSyncing: window.AccountManager.hasAccount() ? window.AccountManager.Storage()?.isSyncing : false 543 442 });
+2 -4
src/Components/SettingsMenu.tsx
··· 1 - import { createSignal, onCleanup, onMount, Show } from "solid-js"; 1 + import { onCleanup, onMount, Show } from "solid-js"; 2 2 import { bytesToFormatted } from "../utils"; 3 3 import { invoke } from '@tauri-apps/api/core'; 4 4 import anime from "animejs"; ··· 12 12 let finalPathInput: HTMLElement; 13 13 let finalPathData: string; 14 14 let finalPathPreviousData: string; 15 - 16 - let [ deletingPhotos, setDeletingPhotos ] = createSignal(false); 17 15 18 16 let closeWithKey = ( e: KeyboardEvent ) => { 19 17 if(e.key === 'Escape'){ ··· 352 350 <div class="account-notice">To enable cloud storage or get more storage please contact "_phaz" on discord</div> 353 351 354 352 <div class="account-notice" style={{ display: 'flex' }}> 355 - <Show when={!deletingPhotos()} fallback={ "We are deleting your photos, please leave this window open while we delete them." }> 353 + <Show when={false} fallback={ "We are deleting your photos, please leave this window open while we delete them." }> 356 354 <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 355 // TODO: Rework all of this 358 356
+18
src/Components/Structs/WorldCache.ts
··· 1 + export class WorldCache{ 2 + expiresOn!: number; 3 + worldData!: { 4 + id: string, 5 + name: string, 6 + author: string, 7 + authorId: string, 8 + desc: string, 9 + img: string, 10 + maxUsers: number, 11 + visits: number, 12 + favourites: number, 13 + tags: any, 14 + from: string, 15 + fromSite: string, 16 + found: boolean 17 + } 18 + }
+4 -1
src/index.tsx
··· 7 7 LoadingManager: LoadingManager; 8 8 PhotoLoadingManager: PhotoLoadingManager; 9 9 ConfirmationBoxManager: ConfirmationBoxManager; 10 - PhotoViewerManager: PhotoViewerManager; 10 + PhotoViewerManager: PhotoViewerManager; 11 + WorldCacheManager: WorldCacheManager; 11 12 12 13 CloseAllPopups: (() => void)[]; 13 14 OS: string; ··· 27 28 import { PhotoLoadingManager } from "./Components/Managers/PhotoLoadingManager"; 28 29 import { ConfirmationBoxManager } from "./Components/Managers/ConfirmationBoxManager"; 29 30 import { PhotoViewerManager } from "./Components/Managers/PhotoViewerManager"; 31 + import { WorldCacheManager } from "./Components/Managers/WorldCacheManager"; 30 32 31 33 window.AccountManager = new AccountManager(); 32 34 window.LoadingManager = new LoadingManager(); 33 35 window.PhotoLoadingManager = new PhotoLoadingManager(); 34 36 window.ConfirmationBoxManager = new ConfirmationBoxManager(); 35 37 window.PhotoViewerManager = new PhotoViewerManager(); 38 + window.WorldCacheManager = new WorldCacheManager(); 36 39 37 40 (async () => { 38 41 window.OS = await invoke('get_os');