diff --git a/apps/app-frontend/eslint.config.mjs b/apps/app-frontend/eslint.config.mjs index 05f559424b..76db133ffb 100644 --- a/apps/app-frontend/eslint.config.mjs +++ b/apps/app-frontend/eslint.config.mjs @@ -17,6 +17,7 @@ export default createConfigForNuxt().append([ rules: { 'vue/html-self-closing': 'off', 'vue/multi-word-component-names': 'off', + '@typescript-eslint/no-explicit-any': 'off', }, }, ]) diff --git a/apps/app-frontend/src/components/ui/ScreenshotCard.vue b/apps/app-frontend/src/components/ui/ScreenshotCard.vue new file mode 100644 index 0000000000..f671bceccf --- /dev/null +++ b/apps/app-frontend/src/components/ui/ScreenshotCard.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/apps/app-frontend/src/helpers/screenshots.ts b/apps/app-frontend/src/helpers/screenshots.ts new file mode 100644 index 0000000000..11dd6fcd76 --- /dev/null +++ b/apps/app-frontend/src/helpers/screenshots.ts @@ -0,0 +1,47 @@ +import { invoke } from '@tauri-apps/api/core' + +export type Screenshot = { + path: string + creation_date: string +} + +export async function getAllProfileScreenshots(profilePath: string): Promise { + return await invoke('plugin:screenshots|get_all_profile_screenshots', { + path: profilePath, + }) +} + +export async function deleteProfileScreenshot( + profilePath: string, + screenshot: Screenshot, +): Promise { + return await invoke('plugin:screenshots|delete_profile_screenshot', { + path: profilePath, + screenshot, + }) +} + +export async function openProfileScreenshot( + profilePath: string, + screenshot: Screenshot, +): Promise { + return await invoke('plugin:screenshots|open_profile_screenshot', { + path: profilePath, + screenshot, + }) +} + +export async function getScreenshotData( + profilePath: string, + screenshot: Screenshot, +): Promise { + return await invoke('plugin:screenshots|get_screenshot_data', { + path: profilePath, + screenshot, + }) +} + +export function getScreenshotFileName(path: string | undefined) { + if (!path) return 'Untitled' + return path.split('/').pop()! +} diff --git a/apps/app-frontend/src/helpers/types.d.ts b/apps/app-frontend/src/helpers/types.d.ts index aa60ec2f74..8fd9a041d1 100644 --- a/apps/app-frontend/src/helpers/types.d.ts +++ b/apps/app-frontend/src/helpers/types.d.ts @@ -1,4 +1,5 @@ import type { ModrinthId } from '@modrinth/utils' +import type { Screenshot } from '@/helpers/screenshots.ts' type GameInstance = { path: string @@ -140,3 +141,25 @@ export type InstanceSettingsTabProps = { instance: GameInstance offline?: boolean } + +export type ServersUpdatedEvent = { event: 'servers_updated' } +export type WorldUpdatedEvent = { event: 'world_updated'; world: string } +export type ServerJoinedEvent = { + event: 'server_joined' + host: string + port: number + timestamp: string +} +export type ScreenshotChangedEvent = { + event: 'screenshot_changed' + screenshot: Screenshot + file_exists: boolean +} + +// TODO: Refactor events.js -> events.ts with proper types for all the events. +export type ProfileEvent = { profile_path_id: string } & ( + | ServersUpdatedEvent + | WorldUpdatedEvent + | ServerJoinedEvent + | ScreenshotChangedEvent +) diff --git a/apps/app-frontend/src/helpers/worlds.ts b/apps/app-frontend/src/helpers/worlds.ts index 89f98d7d72..a389275904 100644 --- a/apps/app-frontend/src/helpers/worlds.ts +++ b/apps/app-frontend/src/helpers/worlds.ts @@ -4,6 +4,7 @@ import { openPath } from '@/helpers/utils' import { autoToHTML } from '@geometrically/minecraft-motd-parser' import dayjs from 'dayjs' import type { GameVersion } from '@modrinth/ui' +import type { ProfileEvent } from '@/helpers/types' type BaseWorld = { name: string @@ -309,19 +310,3 @@ export function hasQuickPlaySupport(gameVersions: GameVersion[], currentVersion: return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex } - -export type ProfileEvent = { profile_path_id: string } & ( - | { - event: 'servers_updated' - } - | { - event: 'world_updated' - world: string - } - | { - event: 'server_joined' - host: string - port: number - timestamp: string - } -) diff --git a/apps/app-frontend/src/pages/instance/Index.vue b/apps/app-frontend/src/pages/instance/Index.vue index 65bfbf68f3..64f693db56 100644 --- a/apps/app-frontend/src/pages/instance/Index.vue +++ b/apps/app-frontend/src/pages/instance/Index.vue @@ -272,6 +272,10 @@ const tabs = computed(() => [ label: 'Worlds', href: `${basePath.value}/worlds`, }, + { + label: 'Screenshots', + href: `${basePath.value}/screenshots`, + }, { label: 'Logs', href: `${basePath.value}/logs`, diff --git a/apps/app-frontend/src/pages/instance/Screenshots.vue b/apps/app-frontend/src/pages/instance/Screenshots.vue new file mode 100644 index 0000000000..a8f66f9c86 --- /dev/null +++ b/apps/app-frontend/src/pages/instance/Screenshots.vue @@ -0,0 +1,190 @@ + + + diff --git a/apps/app-frontend/src/pages/instance/index.js b/apps/app-frontend/src/pages/instance/index.js index fa77df524e..1413e12b28 100644 --- a/apps/app-frontend/src/pages/instance/index.js +++ b/apps/app-frontend/src/pages/instance/index.js @@ -3,5 +3,6 @@ import Overview from './Overview.vue' import Worlds from './Worlds.vue' import Mods from './Mods.vue' import Logs from './Logs.vue' +import Screenshots from './Screenshots.vue' -export { Index, Overview, Worlds, Mods, Logs } +export { Index, Overview, Worlds, Mods, Logs, Screenshots } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 6d5e4e3726..c7bbfb58cd 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -132,6 +132,15 @@ export default new createRouter({ breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Worlds' }], }, }, + { + path: 'screenshots', + name: 'Screenshots', + component: Instance.Screenshots, + meta: { + useRootContext: true, + breadcrumb: [{ name: '?Instance', link: '/instance/{id}/' }, { name: 'Screenshots' }], + }, + }, { path: '', name: 'Mods', diff --git a/apps/app/build.rs b/apps/app/build.rs index 644d22b681..2b3cc81298 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -264,6 +264,19 @@ fn main() { .default_permission( DefaultPermissionRule::AllowAllCommands, ), + ) + .plugin( + "screenshots", + InlinedPlugin::new() + .commands(&[ + "get_all_profile_screenshots", + "get_screenshot_data", + "delete_profile_screenshot", + "open_profile_screenshot", + ]) + .default_permission( + DefaultPermissionRule::AllowAllCommands, + ), ), ) .expect("Failed to run tauri-build"); diff --git a/apps/app/capabilities/plugins.json b/apps/app/capabilities/plugins.json index b9777b6d9f..7a196d09e0 100644 --- a/apps/app/capabilities/plugins.json +++ b/apps/app/capabilities/plugins.json @@ -36,6 +36,7 @@ "utils:default", "ads:default", "friends:default", - "worlds:default" + "worlds:default", + "screenshots:default" ] } diff --git a/apps/app/src/api/mod.rs b/apps/app/src/api/mod.rs index 09d37e87ab..c2d08c210e 100644 --- a/apps/app/src/api/mod.rs +++ b/apps/app/src/api/mod.rs @@ -19,6 +19,7 @@ pub mod utils; pub mod ads; pub mod cache; pub mod friends; +pub mod screenshots; pub mod worlds; pub type Result = std::result::Result; diff --git a/apps/app/src/api/screenshots.rs b/apps/app/src/api/screenshots.rs new file mode 100644 index 0000000000..6a7d675d31 --- /dev/null +++ b/apps/app/src/api/screenshots.rs @@ -0,0 +1,56 @@ +use tauri::{AppHandle, Runtime}; +use tauri_plugin_opener::OpenerExt; +use theseus::profile::get_full_path; +use theseus::screenshots::{self, Screenshot, get_valid_screenshot_path}; + +pub fn init() -> tauri::plugin::TauriPlugin { + tauri::plugin::Builder::new("screenshots") + .invoke_handler(tauri::generate_handler![ + get_all_profile_screenshots, + get_screenshot_data, + delete_profile_screenshot, + open_profile_screenshot + ]) + .build() +} + +#[tauri::command] +pub async fn get_all_profile_screenshots( + path: &str, +) -> crate::api::Result> { + Ok(screenshots::get_all_profile_screenshots(path).await?) +} + +#[tauri::command] +pub async fn get_screenshot_data( + path: &str, + screenshot: Screenshot, +) -> crate::api::Result> { + let profile_dir = get_full_path(path).await?; + Ok(screenshots::get_screenshot_data(&profile_dir, &screenshot).await?) +} + +#[tauri::command] +pub async fn delete_profile_screenshot( + path: &str, + screenshot: Screenshot, +) -> crate::api::Result { + Ok(screenshots::delete_profile_screenshot(path, &screenshot).await?) +} + +#[tauri::command] +pub async fn open_profile_screenshot( + app: AppHandle, + path: &str, + screenshot: Screenshot, +) -> crate::api::Result { + let profile_dir = get_full_path(path).await?; + if let Some(path) = + get_valid_screenshot_path(&profile_dir, &screenshot).await? + { + app.opener().reveal_item_in_dir(path).unwrap(); + Ok(true) + } else { + Ok(false) + } +} diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 4291431df8..e19b3e239a 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -260,6 +260,7 @@ fn main() { .plugin(api::ads::init()) .plugin(api::friends::init()) .plugin(api::worlds::init()) + .plugin(api::screenshots::init()) .invoke_handler(tauri::generate_handler![ initialize_state, is_dev, diff --git a/apps/frontend/src/pages/[type]/[id]/gallery.vue b/apps/frontend/src/pages/[type]/[id]/gallery.vue index 4f355236ce..6a54dcc871 100644 --- a/apps/frontend/src/pages/[type]/[id]/gallery.vue +++ b/apps/frontend/src/pages/[type]/[id]/gallery.vue @@ -129,72 +129,12 @@ proceed-label="Delete" @proceed="deleteGalleryImage" /> -
-
- - -
-
-

- {{ expandedGalleryItem.title }} -

-

- {{ expandedGalleryItem.description }} -

-
-
-
- - - - - - -
-
-
-
-
+