From b99358266c2f5b7016c95f67301416351402049d Mon Sep 17 00:00:00 2001 From: Heru Date: Sun, 13 Jul 2025 11:38:50 -0700 Subject: [PATCH 1/3] First pass at Global Screen Shot Viewer --- apps/app-frontend/src/App.vue | 13 + .../src/components/ui/RenameFileModal.vue | 293 ++++++++++++++++++ .../src/components/ui/ScreenshotGrid.vue | 230 ++++++++++++++ .../src/components/ui/ScreenshotModal.vue | 282 +++++++++++++++++ .../src/composables/useScreenshots.js | 249 +++++++++++++++ apps/app-frontend/src/helpers/profile.js | 25 ++ .../src/pages/screenshots/Custom.vue | 152 +++++++++ .../src/pages/screenshots/Downloaded.vue | 146 +++++++++ .../src/pages/screenshots/Index.vue | 212 +++++++++++++ .../src/pages/screenshots/Overview.vue | 140 +++++++++ .../src/pages/screenshots/index.js | 6 + apps/app-frontend/src/routes.js | 26 ++ apps/app/build.rs | 5 + apps/app/src/api/profile.rs | 228 ++++++++++++++ apps/app/tauri.conf.json | 32 +- 15 files changed, 2032 insertions(+), 7 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/RenameFileModal.vue create mode 100644 apps/app-frontend/src/components/ui/ScreenshotGrid.vue create mode 100644 apps/app-frontend/src/components/ui/ScreenshotModal.vue create mode 100644 apps/app-frontend/src/composables/useScreenshots.js create mode 100644 apps/app-frontend/src/pages/screenshots/Custom.vue create mode 100644 apps/app-frontend/src/pages/screenshots/Downloaded.vue create mode 100644 apps/app-frontend/src/pages/screenshots/Index.vue create mode 100644 apps/app-frontend/src/pages/screenshots/Overview.vue create mode 100644 apps/app-frontend/src/pages/screenshots/index.js diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 1bc25942c7..13d65413c0 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -7,6 +7,7 @@ import { CompassIcon, DownloadIcon, HomeIcon, + ImageIcon, LeftArrowIcon, LibraryIcon, LogInIcon, @@ -425,6 +426,18 @@ function handleAuxClick(e) { + + + + + + + + + + + + diff --git a/apps/app-frontend/src/components/ui/ScreenshotGrid.vue b/apps/app-frontend/src/components/ui/ScreenshotGrid.vue new file mode 100644 index 0000000000..f12cae7e05 --- /dev/null +++ b/apps/app-frontend/src/components/ui/ScreenshotGrid.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/apps/app-frontend/src/components/ui/ScreenshotModal.vue b/apps/app-frontend/src/components/ui/ScreenshotModal.vue new file mode 100644 index 0000000000..6815eb789a --- /dev/null +++ b/apps/app-frontend/src/components/ui/ScreenshotModal.vue @@ -0,0 +1,282 @@ + + + + + diff --git a/apps/app-frontend/src/composables/useScreenshots.js b/apps/app-frontend/src/composables/useScreenshots.js new file mode 100644 index 0000000000..dd415fd145 --- /dev/null +++ b/apps/app-frontend/src/composables/useScreenshots.js @@ -0,0 +1,249 @@ +import { ref, computed, onMounted, onUnmounted } from 'vue' +import { list, getAllScreenshots, showInFolder } from '@/helpers/profile.js' +import { handleError } from '@/store/notifications.js' +import { convertFileSrc } from '@tauri-apps/api/core' + +/** + * Composable for managing screenshot functionality + * @param {Object} options - Configuration options + * @param {Function} options.filterScreenshots - Function to filter screenshots + * @param {boolean} options.defaultGrouping - Default grouping preference + */ +export function useScreenshots({ filterScreenshots, defaultGrouping = false } = {}) { + // Reactive state + const instances = ref([]) + const screenshots = ref([]) + const selectedScreenshot = ref(null) + const showModal = ref(false) + const groupByInstance = ref(defaultGrouping) + const collapsedInstances = ref(new Set()) + const renameModal = ref(null) + + // Computed property to organize screenshots based on grouping preference + const organizedScreenshots = computed(() => { + if (groupByInstance.value) { + // Group screenshots by instance + const grouped = {} + screenshots.value.forEach((screenshot) => { + const instancePath = screenshot.profile_path + if (!grouped[instancePath]) { + grouped[instancePath] = [] + } + grouped[instancePath].push(screenshot) + }) + + // Sort screenshots within each group by date (newest first) + Object.keys(grouped).forEach((instancePath) => { + grouped[instancePath].sort((a, b) => b.created - a.created) + }) + + return grouped + } else { + // Return flat array sorted by newest first + return [...screenshots.value].sort((a, b) => b.created - a.created) + } + }) + + // Functions + const toggleGrouping = () => { + groupByInstance.value = !groupByInstance.value + // Clear collapsed state when switching modes + collapsedInstances.value.clear() + } + + const toggleInstanceCollapse = (instancePath) => { + if (collapsedInstances.value.has(instancePath)) { + collapsedInstances.value.delete(instancePath) + } else { + collapsedInstances.value.add(instancePath) + } + } + + const isInstanceCollapsed = (instancePath) => { + return collapsedInstances.value.has(instancePath) + } + + const formatDate = (timestamp) => { + return new Date(timestamp * 1000).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }) + } + + const getScreenshotUrl = (path) => { + try { + return convertFileSrc(path) + } catch (error) { + console.error('Failed to convert file path:', path, error) + return null + } + } + + const openModal = (screenshot) => { + selectedScreenshot.value = screenshot + showModal.value = true + } + + const closeModal = () => { + showModal.value = false + selectedScreenshot.value = null + } + + const showInExplorer = async (screenshotPath) => { + try { + await showInFolder(screenshotPath) + } catch (error) { + console.error('Failed to show file in explorer:', error) + handleError(error) + } + } + + const showRenameModal = (screenshot) => { + if (renameModal.value) { + renameModal.value.show(screenshot.path, screenshot.filename, true) + } + } + + const onFileRenamed = async (renameData) => { + try { + // Update the screenshot in our list + const screenshotIndex = screenshots.value.findIndex((s) => s.path === renameData.originalPath) + if (screenshotIndex !== -1) { + screenshots.value[screenshotIndex] = { + ...screenshots.value[screenshotIndex], + path: renameData.newPath, + filename: renameData.newFilename, + } + } + + // Update selectedScreenshot if it's the one being renamed + if (selectedScreenshot.value && selectedScreenshot.value.path === renameData.originalPath) { + selectedScreenshot.value = { + ...selectedScreenshot.value, + path: renameData.newPath, + filename: renameData.newFilename, + } + } + } catch (error) { + console.error('Failed to update screenshot after rename:', error) + handleError(error) + } + } + + const handleKeydown = (event) => { + if (event.key === 'Escape') { + closeModal() + } else if (event.key === 'ArrowLeft') { + goToPrevious() + } else if (event.key === 'ArrowRight') { + goToNext() + } + } + + // Navigation functions + const getFlatScreenshots = () => { + // Get a flat array of all screenshots in current view + if (groupByInstance.value && typeof organizedScreenshots.value === 'object') { + const flat = [] + Object.values(organizedScreenshots.value).forEach((screenshots) => { + flat.push(...screenshots) + }) + return flat.sort((a, b) => b.created - a.created) + } else { + return organizedScreenshots.value + } + } + + const getCurrentIndex = () => { + if (!selectedScreenshot.value) return -1 + const flatScreenshots = getFlatScreenshots() + return flatScreenshots.findIndex((s) => s.path === selectedScreenshot.value.path) + } + + const hasPrevious = computed(() => { + const currentIndex = getCurrentIndex() + return currentIndex > 0 + }) + + const hasNext = computed(() => { + const currentIndex = getCurrentIndex() + const flatScreenshots = getFlatScreenshots() + return currentIndex !== -1 && currentIndex < flatScreenshots.length - 1 + }) + + const goToPrevious = () => { + const currentIndex = getCurrentIndex() + if (currentIndex > 0) { + const flatScreenshots = getFlatScreenshots() + selectedScreenshot.value = flatScreenshots[currentIndex - 1] + } + } + + const goToNext = () => { + const currentIndex = getCurrentIndex() + const flatScreenshots = getFlatScreenshots() + if (currentIndex !== -1 && currentIndex < flatScreenshots.length - 1) { + selectedScreenshot.value = flatScreenshots[currentIndex + 1] + } + } + + const loadScreenshots = async () => { + try { + // Load instances and screenshots + instances.value = await list().catch(handleError) + const allScreenshots = await getAllScreenshots() + + // Apply filter if provided, otherwise use all screenshots + if (filterScreenshots) { + screenshots.value = filterScreenshots(allScreenshots, instances.value) + } else { + screenshots.value = allScreenshots + } + } catch (error) { + console.error('Failed to load screenshots:', error) + handleError(error) + } + } + + // Lifecycle + onMounted(() => { + loadScreenshots() + document.addEventListener('keydown', handleKeydown) + }) + + onUnmounted(() => { + document.removeEventListener('keydown', handleKeydown) + }) + + return { + // State + instances, + screenshots, + selectedScreenshot, + showModal, + groupByInstance, + collapsedInstances, + renameModal, + + // Computed + organizedScreenshots, + hasPrevious, + hasNext, + + // Functions + toggleGrouping, + toggleInstanceCollapse, + isInstanceCollapsed, + formatDate, + getScreenshotUrl, + openModal, + closeModal, + goToPrevious, + goToNext, + showInExplorer, + showRenameModal, + onFileRenamed, + loadScreenshots, + } +} diff --git a/apps/app-frontend/src/helpers/profile.js b/apps/app-frontend/src/helpers/profile.js index ed9741bdbc..5c481e1ae4 100644 --- a/apps/app-frontend/src/helpers/profile.js +++ b/apps/app-frontend/src/helpers/profile.js @@ -202,3 +202,28 @@ export async function finish_install(instance) { await install(instance.path, false).catch(handleError) } } + +// Get screenshots from a specific profile +export async function getScreenshots(path) { + return await invoke('plugin:profile|profile_get_screenshots', { path }) +} + +// Get screenshots from all profiles +export async function getAllScreenshots() { + return await invoke('plugin:profile|profile_get_all_screenshots') +} + +// Open screenshots folder for a profile +export async function openScreenshotsFolder(path) { + return await invoke('plugin:profile|profile_open_screenshots_folder', { path }) +} + +// Show file in explorer/finder +export async function showInFolder(path) { + return await invoke('plugin:profile|show_in_folder', { path }) +} + +// Rename a file +export async function renameFile(oldPath, newPath) { + return await invoke('plugin:profile|rename_file', { oldPath, newPath }) +} diff --git a/apps/app-frontend/src/pages/screenshots/Custom.vue b/apps/app-frontend/src/pages/screenshots/Custom.vue new file mode 100644 index 0000000000..b1d8f5e4ac --- /dev/null +++ b/apps/app-frontend/src/pages/screenshots/Custom.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/apps/app-frontend/src/pages/screenshots/Downloaded.vue b/apps/app-frontend/src/pages/screenshots/Downloaded.vue new file mode 100644 index 0000000000..8d4524eaf7 --- /dev/null +++ b/apps/app-frontend/src/pages/screenshots/Downloaded.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/apps/app-frontend/src/pages/screenshots/Index.vue b/apps/app-frontend/src/pages/screenshots/Index.vue new file mode 100644 index 0000000000..b0dac4a1ec --- /dev/null +++ b/apps/app-frontend/src/pages/screenshots/Index.vue @@ -0,0 +1,212 @@ + + + + + diff --git a/apps/app-frontend/src/pages/screenshots/Overview.vue b/apps/app-frontend/src/pages/screenshots/Overview.vue new file mode 100644 index 0000000000..450f2212b8 --- /dev/null +++ b/apps/app-frontend/src/pages/screenshots/Overview.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/apps/app-frontend/src/pages/screenshots/index.js b/apps/app-frontend/src/pages/screenshots/index.js new file mode 100644 index 0000000000..2dcbd63570 --- /dev/null +++ b/apps/app-frontend/src/pages/screenshots/index.js @@ -0,0 +1,6 @@ +import Index from './Index.vue' +import Overview from './Overview.vue' +import Downloaded from './Downloaded.vue' +import Custom from './Custom.vue' + +export { Index, Overview, Downloaded, Custom } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 67172e68df..0b28a0c284 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -3,6 +3,7 @@ import * as Pages from '@/pages' import * as Project from '@/pages/project' import * as Instance from '@/pages/instance' import * as Library from '@/pages/library' +import * as Screenshots from '@/pages/screenshots' /** * Configures application routing. Add page to pages/index and then add to route table here. @@ -67,6 +68,31 @@ export default new createRouter({ }, ], }, + { + path: '/screenshots', + name: 'Screenshots', + component: Screenshots.Index, + meta: { + breadcrumb: [{ name: 'Screenshots' }], + }, + children: [ + { + path: '', + name: 'ScreenshotsOverview', + component: Screenshots.Overview, + }, + { + path: 'downloaded', + name: 'ScreenshotsDownloaded', + component: Screenshots.Downloaded, + }, + { + path: 'custom', + name: 'ScreenshotsCustom', + component: Screenshots.Custom, + }, + ], + }, { path: '/project/:id', name: 'Project', diff --git a/apps/app/build.rs b/apps/app/build.rs index 7a4da8872a..19be3b9985 100644 --- a/apps/app/build.rs +++ b/apps/app/build.rs @@ -174,6 +174,11 @@ fn main() { "profile_edit_icon", "profile_export_mrpack", "profile_get_pack_export_candidates", + "profile_get_screenshots", + "profile_get_all_screenshots", + "profile_open_screenshots_folder", + "show_in_folder", + "rename_file", ]) .default_permission( DefaultPermissionRule::AllowAllCommands, diff --git a/apps/app/src/api/profile.rs b/apps/app/src/api/profile.rs index 1d812639e4..e8fa92b1ea 100644 --- a/apps/app/src/api/profile.rs +++ b/apps/app/src/api/profile.rs @@ -33,6 +33,11 @@ pub fn init() -> tauri::plugin::TauriPlugin { profile_edit_icon, profile_export_mrpack, profile_get_pack_export_candidates, + profile_get_screenshots, + profile_get_all_screenshots, + profile_open_screenshots_folder, + show_in_folder, + rename_file, ]) .build() } @@ -393,3 +398,226 @@ pub async fn profile_edit_icon( profile::edit_icon(path, icon_path).await?; Ok(()) } + +// Get screenshots from a profile's screenshots folder +// invoke('plugin:profile|profile_get_screenshots') +#[tauri::command] +pub async fn profile_get_screenshots(path: &str) -> Result> { + let full_path = profile::get_full_path(path).await?; + let screenshots_path = full_path.join("screenshots"); + + let mut screenshots = Vec::new(); + + if screenshots_path.exists() && screenshots_path.is_dir() { + let mut entries = tokio::fs::read_dir(&screenshots_path).await?; + + while let Some(entry) = entries.next_entry().await? { + let entry_path = entry.path(); + + if entry_path.is_file() { + if let Some(extension) = entry_path.extension() { + let ext = extension.to_string_lossy().to_lowercase(); + if matches!( + ext.as_str(), + "png" | "jpg" | "jpeg" | "gif" | "bmp" | "webp" + ) { + if let Some(filename) = entry_path.file_name() { + let metadata = entry.metadata().await?; + let modified = metadata + .modified() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + let created = metadata + .created() + .unwrap_or(std::time::SystemTime::UNIX_EPOCH); + + screenshots.push(Screenshot { + filename: filename + .to_string_lossy() + .to_string(), + path: entry_path.to_string_lossy().to_string(), + size: metadata.len(), + modified: modified + .duration_since( + std::time::SystemTime::UNIX_EPOCH, + ) + .unwrap_or_default() + .as_secs(), + created: created + .duration_since( + std::time::SystemTime::UNIX_EPOCH, + ) + .unwrap_or_default() + .as_secs(), + profile_path: path.to_string(), + }); + } + } + } + } + } + } + + // Sort by creation time, newest first + screenshots.sort_by(|a, b| b.created.cmp(&a.created)); + + Ok(screenshots) +} + +// Get screenshots from all profiles +// invoke('plugin:profile|profile_get_all_screenshots') +#[tauri::command] +pub async fn profile_get_all_screenshots() -> Result> { + let profiles = profile::list().await?; + let mut all_screenshots = Vec::new(); + + for profile in profiles { + match profile_get_screenshots(&profile.path).await { + Ok(mut screenshots) => { + all_screenshots.append(&mut screenshots); + } + Err(_) => { + // Continue if a profile fails, don't break the whole operation + } + } + } + + // Sort all screenshots by creation time, newest first + all_screenshots.sort_by(|a, b| b.created.cmp(&a.created)); + + Ok(all_screenshots) +} + +// Opens the screenshots folder of a profile +// invoke('plugin:profile|profile_open_screenshots_folder', path) +#[tauri::command] +pub async fn profile_open_screenshots_folder(path: &str) -> Result<()> { + let full_path = profile::get_full_path(path).await?; + let screenshots_path = full_path.join("screenshots"); + + // Create the screenshots folder if it doesn't exist + if !screenshots_path.exists() { + tokio::fs::create_dir_all(&screenshots_path).await?; + } + + // Open the folder using the system's default file manager + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer") + .arg(&screenshots_path) + .spawn()?; + } + + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg(&screenshots_path) + .spawn()?; + } + + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open") + .arg(&screenshots_path) + .spawn()?; + } + + Ok(()) +} + +// Shows a specific file in the system's file explorer +// invoke('show_in_folder', { path: '/path/to/file.png' }) +#[tauri::command] +pub async fn show_in_folder(path: &str) -> Result<()> { + let file_path = std::path::Path::new(path); + + if !file_path.exists() { + return Err(theseus::Error::from(theseus::ErrorKind::FSError( + "File does not exist".to_string(), + )) + .into()); + } + + // Open the file in the system's default file manager and select it + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer") + .args(["/select,", path]) + .spawn()?; + } + + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .args(["-R", path]) + .spawn()?; + } + + #[cfg(target_os = "linux")] + { + // For Linux, we'll open the parent directory since most file managers + // don't support selecting a specific file + if let Some(parent_dir) = file_path.parent() { + std::process::Command::new("xdg-open") + .arg(parent_dir) + .spawn()?; + } + } + + Ok(()) +} + +// Rename a file from old path to new path +// invoke('rename_file', { oldPath: '/path/to/old.png', newPath: '/path/to/new.png' }) +#[tauri::command] +pub async fn rename_file(old_path: &str, new_path: &str) -> Result<()> { + let old_file_path = std::path::Path::new(old_path); + let new_file_path = std::path::Path::new(new_path); + + // Check if the old file exists + if !old_file_path.exists() { + return Err(theseus::Error::from(theseus::ErrorKind::FSError( + "Source file does not exist".to_string(), + )) + .into()); + } + + // Check if the new file already exists + if new_file_path.exists() { + return Err(theseus::Error::from(theseus::ErrorKind::FSError( + "Target file already exists".to_string(), + )) + .into()); + } + + // Ensure the parent directory exists for the new path + if let Some(parent_dir) = new_file_path.parent() { + if !parent_dir.exists() { + std::fs::create_dir_all(parent_dir).map_err(|e| { + theseus::Error::from(theseus::ErrorKind::FSError(format!( + "Failed to create parent directory: {}", + e + ))) + })?; + } + } + + // Rename/move the file + std::fs::rename(old_path, new_path).map_err(|e| { + theseus::Error::from(theseus::ErrorKind::FSError(format!( + "Failed to rename file: {}", + e + ))) + })?; + + Ok(()) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Screenshot { + pub filename: String, + pub path: String, + pub size: u64, + pub modified: u64, + pub created: u64, + pub profile_path: String, +} diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json index 724e536d85..6459b73a3f 100644 --- a/apps/app/tauri.conf.json +++ b/apps/app/tauri.conf.json @@ -12,7 +12,12 @@ "copyright": "", "targets": "all", "externalBin": [], - "icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], + "icon": [ + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], "windows": { "nsis": { "installMode": "perMachine", @@ -35,7 +40,9 @@ }, "fileAssociations": [ { - "ext": ["mrpack"], + "ext": [ + "mrpack" + ], "mimeType": "application/x-modrinth-modpack+zip" } ] @@ -47,7 +54,9 @@ "plugins": { "deep-link": { "desktop": { - "schemes": ["modrinth"] + "schemes": [ + "modrinth" + ] }, "mobile": [] } @@ -79,15 +88,24 @@ "$CONFIG/caches/icons/*", "$APPDATA/profiles/*/saves/*/icon.png", "$APPCONFIG/profiles/*/saves/*/icon.png", - "$CONFIG/profiles/*/saves/*/icon.png" + "$CONFIG/profiles/*/saves/*/icon.png", + "$APPDATA/com.modrinth.theseus/profiles/*/screenshots/*", + "$APPCONFIG/com.modrinth.theseus/profiles/*/screenshots/*", + "$CONFIG/com.modrinth.theseus/profiles/*/screenshots/*" ], "enable": true }, - "capabilities": ["ads", "core", "plugins"], + "capabilities": [ + "ads", + "core", + "plugins" + ], "csp": { "default-src": "'self' customprotocol: asset:", "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:", - "font-src": ["https://cdn-raw.modrinth.com/fonts/"], + "font-src": [ + "https://cdn-raw.modrinth.com/fonts/" + ], "img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:", "style-src": "'unsafe-inline' 'self'", "script-src": "https://*.posthog.com 'self'", @@ -96,4 +114,4 @@ } } } -} +} \ No newline at end of file From 37d0628670baf09c78af6888e8414a7cd4cad617 Mon Sep 17 00:00:00 2001 From: Heru Date: Sun, 13 Jul 2025 11:50:59 -0700 Subject: [PATCH 2/3] added search to GSSM --- .../src/components/ui/SearchBar.vue | 32 ++++++++++++++ .../src/composables/useScreenshots.js | 42 +++++++++++++++++-- .../src/pages/screenshots/Custom.vue | 32 ++++++++++---- .../src/pages/screenshots/Downloaded.vue | 2 + .../src/pages/screenshots/Overview.vue | 2 + 5 files changed, 100 insertions(+), 10 deletions(-) create mode 100644 apps/app-frontend/src/components/ui/SearchBar.vue diff --git a/apps/app-frontend/src/components/ui/SearchBar.vue b/apps/app-frontend/src/components/ui/SearchBar.vue new file mode 100644 index 0000000000..6f539b6809 --- /dev/null +++ b/apps/app-frontend/src/components/ui/SearchBar.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/app-frontend/src/composables/useScreenshots.js b/apps/app-frontend/src/composables/useScreenshots.js index dd415fd145..2e85452569 100644 --- a/apps/app-frontend/src/composables/useScreenshots.js +++ b/apps/app-frontend/src/composables/useScreenshots.js @@ -1,4 +1,4 @@ -import { ref, computed, onMounted, onUnmounted } from 'vue' +import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import { list, getAllScreenshots, showInFolder } from '@/helpers/profile.js' import { handleError } from '@/store/notifications.js' import { convertFileSrc } from '@tauri-apps/api/core' @@ -18,13 +18,38 @@ export function useScreenshots({ filterScreenshots, defaultGrouping = false } = const groupByInstance = ref(defaultGrouping) const collapsedInstances = ref(new Set()) const renameModal = ref(null) + const searchQuery = ref('') + const debouncedSearchQuery = ref('') + + // Debounce search query to improve performance + let searchTimeout = null + watch(searchQuery, (newQuery) => { + if (searchTimeout) { + clearTimeout(searchTimeout) + } + searchTimeout = setTimeout(() => { + debouncedSearchQuery.value = newQuery + }, 300) // 300ms debounce + }, { immediate: true }) // Computed property to organize screenshots based on grouping preference const organizedScreenshots = computed(() => { + // Filter screenshots based on debounced search query + let filteredScreenshots = screenshots.value + if (debouncedSearchQuery.value.trim()) { + const query = debouncedSearchQuery.value.toLowerCase().trim() + filteredScreenshots = screenshots.value.filter(screenshot => { + return ( + screenshot.filename.toLowerCase().includes(query) || + screenshot.profile_path.toLowerCase().includes(query) + ) + }) + } + if (groupByInstance.value) { // Group screenshots by instance const grouped = {} - screenshots.value.forEach((screenshot) => { + filteredScreenshots.forEach((screenshot) => { const instancePath = screenshot.profile_path if (!grouped[instancePath]) { grouped[instancePath] = [] @@ -40,7 +65,7 @@ export function useScreenshots({ filterScreenshots, defaultGrouping = false } = return grouped } else { // Return flat array sorted by newest first - return [...screenshots.value].sort((a, b) => b.created - a.created) + return [...filteredScreenshots].sort((a, b) => b.created - a.created) } }) @@ -188,6 +213,15 @@ export function useScreenshots({ filterScreenshots, defaultGrouping = false } = } } + const clearSearch = () => { + searchQuery.value = '' + debouncedSearchQuery.value = '' + if (searchTimeout) { + clearTimeout(searchTimeout) + searchTimeout = null + } + } + const loadScreenshots = async () => { try { // Load instances and screenshots @@ -225,6 +259,7 @@ export function useScreenshots({ filterScreenshots, defaultGrouping = false } = groupByInstance, collapsedInstances, renameModal, + searchQuery, // Computed organizedScreenshots, @@ -245,5 +280,6 @@ export function useScreenshots({ filterScreenshots, defaultGrouping = false } = showRenameModal, onFileRenamed, loadScreenshots, + clearSearch, } } diff --git a/apps/app-frontend/src/pages/screenshots/Custom.vue b/apps/app-frontend/src/pages/screenshots/Custom.vue index b1d8f5e4ac..8e1474ee02 100644 --- a/apps/app-frontend/src/pages/screenshots/Custom.vue +++ b/apps/app-frontend/src/pages/screenshots/Custom.vue @@ -4,6 +4,7 @@ import { useScreenshots } from '@/composables/useScreenshots.js' import RenameFileModal from '@/components/ui/RenameFileModal.vue' import ScreenshotGrid from '@/components/ui/ScreenshotGrid.vue' import ScreenshotModal from '@/components/ui/ScreenshotModal.vue' +import SearchBar from '@/components/ui/SearchBar.vue' // Use the composable with a filter for custom instances only const { @@ -14,6 +15,7 @@ const { showModal, groupByInstance, renameModal, + searchQuery, hasPrevious, hasNext, toggleGrouping, @@ -28,6 +30,7 @@ const { showInExplorer, showRenameModal, onFileRenamed, + clearSearch, } = useScreenshots({ filterScreenshots: (allScreenshots, instances) => { const customInstances = instances.filter((i) => !i.linked_data) @@ -48,13 +51,20 @@ const hasCustomInstances = computed(() => instances.value.some((i) => !i.linked_

Screenshots from Custom Instances

- +
+ + +
instances.value.some((i) => !i.linked_ justify-content: space-between; align-items: center; margin-bottom: var(--gap-lg); + gap: var(--gap-lg); +} + +.header-controls { + display: flex; + align-items: center; + gap: var(--gap-md); + flex: 1; } .group-toggle-btn { diff --git a/apps/app-frontend/src/pages/screenshots/Downloaded.vue b/apps/app-frontend/src/pages/screenshots/Downloaded.vue index 8d4524eaf7..a4d7e4ff36 100644 --- a/apps/app-frontend/src/pages/screenshots/Downloaded.vue +++ b/apps/app-frontend/src/pages/screenshots/Downloaded.vue @@ -14,6 +14,7 @@ const { showModal, groupByInstance, renameModal, + searchQuery, hasPrevious, hasNext, toggleGrouping, @@ -28,6 +29,7 @@ const { showInExplorer, showRenameModal, onFileRenamed, + clearSearch, } = useScreenshots({ filterScreenshots: (allScreenshots, instances) => { const downloadedInstances = instances.filter((i) => i.linked_data) diff --git a/apps/app-frontend/src/pages/screenshots/Overview.vue b/apps/app-frontend/src/pages/screenshots/Overview.vue index 450f2212b8..332c83ea69 100644 --- a/apps/app-frontend/src/pages/screenshots/Overview.vue +++ b/apps/app-frontend/src/pages/screenshots/Overview.vue @@ -13,6 +13,7 @@ const { showModal, groupByInstance, renameModal, + searchQuery, hasPrevious, hasNext, toggleGrouping, @@ -27,6 +28,7 @@ const { showInExplorer, showRenameModal, onFileRenamed, + clearSearch, } = useScreenshots({ filterScreenshots: (allScreenshots) => allScreenshots.sort((a, b) => b.created - a.created).slice(0, 12), From 0012803456a35d9df6081466e6df5ef75d23b447 Mon Sep 17 00:00:00 2001 From: Heru Date: Sun, 13 Jul 2025 14:32:02 -0700 Subject: [PATCH 3/3] ran fix and resolved those issues --- .../src/components/ui/RenameFileModal.vue | 2 +- .../src/components/ui/ScreenshotGrid.vue | 58 ++++++++++--------- .../src/components/ui/ScreenshotModal.vue | 8 +-- .../src/components/ui/SearchBar.vue | 2 +- .../src/composables/useScreenshots.js | 22 ++++--- .../src/pages/screenshots/Downloaded.vue | 4 +- .../src/pages/screenshots/Index.vue | 8 ++- .../src/pages/screenshots/Overview.vue | 4 +- apps/app/src/api/profile.rs | 6 +- 9 files changed, 61 insertions(+), 53 deletions(-) diff --git a/apps/app-frontend/src/components/ui/RenameFileModal.vue b/apps/app-frontend/src/components/ui/RenameFileModal.vue index e86f15b0d4..18468b991c 100644 --- a/apps/app-frontend/src/components/ui/RenameFileModal.vue +++ b/apps/app-frontend/src/components/ui/RenameFileModal.vue @@ -73,7 +73,7 @@ const isScreenshot = ref(true) const emit = defineEmits(['renamed', 'cancelled']) // Extract just the filename without extension -const filenameWithoutExt = computed(() => { +const __filenameWithoutExt = computed(() => { const filename = newFilename.value || originalFilename.value const lastDotIndex = filename.lastIndexOf('.') return lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename diff --git a/apps/app-frontend/src/components/ui/ScreenshotGrid.vue b/apps/app-frontend/src/components/ui/ScreenshotGrid.vue index f12cae7e05..d357d1b567 100644 --- a/apps/app-frontend/src/components/ui/ScreenshotGrid.vue +++ b/apps/app-frontend/src/components/ui/ScreenshotGrid.vue @@ -1,25 +1,29 @@