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 @@ + + + + + + + + No screenshots yet + + Screenshots taken in-game will appear here + + + + + + + + + Today + + + + You haven't taken any screenshots today. + + + + + + + + {{ date }} + + + + + + + + + + + + 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 }} - - - - - - - - - - - - - - - - - - - - - - - - - + - + = this.project.gallery.length) { - this.expandedGalleryIndex = 0; - } - this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]; + openImage(item, index) { + const src = item.raw_url || item.url; + const alt = item.title || "gallery-image"; + this.$refs.imageModal.show(src, alt, { + index, + title: item.title, + description: item.description, + }); }, - previousImage() { - this.expandedGalleryIndex--; - if (this.expandedGalleryIndex < 0) { - this.expandedGalleryIndex = this.project.gallery.length - 1; - } - this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]; + getGalleryEntry(key, offset) { + const gallery = this.project.gallery; + const len = gallery.length; + const i = (key.index + offset + len) % len; + const item = gallery[i]; + + return { + src: item.raw_url || item.url, + alt: item.title || "gallery-image", + key: { + index: i, + title: item.title, + description: item.description, + }, + }; + }, + getNextEntry(key) { + return this.getGalleryEntry(key, 1); + }, + getPrevEntry(key) { + return this.getGalleryEntry(key, -1); }, - expandImage(item, index) { - this.expandedGalleryItem = item; - this.expandedGalleryIndex = index; - this.zoomedIn = false; + openExternally(src) { + window.open(src, "_blank"); }, resetEdit() { this.editIndex = -1; @@ -539,137 +473,6 @@ export default defineNuxtComponent({ } } -.expanded-image-modal { - position: fixed; - z-index: 20; - overflow: auto; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #000000; - background-color: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - - .content { - position: relative; - width: calc(100vw - 2 * var(--spacing-card-lg)); - height: calc(100vh - 2 * var(--spacing-card-lg)); - - .circle-button { - padding: 0.5rem; - line-height: 1; - display: flex; - max-width: 2rem; - color: var(--color-button-text); - background-color: var(--color-button-bg); - border-radius: var(--size-rounded-max); - margin: 0; - box-shadow: inset 0px -1px 1px rgb(17 24 39 / 10%); - - &:not(:last-child) { - margin-right: 0.5rem; - } - - &:hover { - background-color: var(--color-button-bg-hover) !important; - - svg { - color: var(--color-button-text-hover) !important; - } - } - - &:active { - background-color: var(--color-button-bg-active) !important; - - svg { - color: var(--color-button-text-active) !important; - } - } - - svg { - height: 1rem; - width: 1rem; - } - } - - .image { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - max-width: calc(100vw - 2 * var(--spacing-card-lg)); - max-height: calc(100vh - 2 * var(--spacing-card-lg)); - border-radius: var(--size-rounded-card); - - &.zoomed-in { - object-fit: cover; - width: auto; - height: calc(100vh - 2 * var(--spacing-card-lg)); - max-width: calc(100vw - 2 * var(--spacing-card-lg)); - } - } - .floating { - position: absolute; - left: 50%; - transform: translateX(-50%); - bottom: var(--spacing-card-md); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-card-sm); - transition: opacity 0.25s ease-in-out; - opacity: 1; - padding: 2rem 2rem 0 2rem; - - &:not(&:hover) { - opacity: 0.4; - .text { - transform: translateY(2.5rem) scale(0.8); - opacity: 0; - } - .controls { - transform: translateY(0.25rem) scale(0.9); - } - } - - .text { - display: flex; - flex-direction: column; - max-width: 40rem; - transition: - opacity 0.25s ease-in-out, - transform 0.25s ease-in-out; - text-shadow: 1px 1px 10px #000000d4; - margin-bottom: 0.25rem; - gap: 0.5rem; - - h2 { - color: var(--dark-color-text-dark); - font-size: 1.25rem; - text-align: center; - margin: 0; - } - - p { - color: var(--dark-color-text); - margin: 0; - } - } - .controls { - background-color: var(--color-raised-bg); - padding: var(--spacing-card-md); - border-radius: var(--size-rounded-card); - transition: - opacity 0.25s ease-in-out, - transform 0.25s ease-in-out; - } - } - } -} - .buttons { display: flex; diff --git a/packages/app-lib/src/api/mod.rs b/packages/app-lib/src/api/mod.rs index 421d805c1f..169fc40c29 100644 --- a/packages/app-lib/src/api/mod.rs +++ b/packages/app-lib/src/api/mod.rs @@ -10,6 +10,7 @@ pub mod mr_auth; pub mod pack; pub mod process; pub mod profile; +pub mod screenshots; pub mod settings; pub mod tags; pub mod worlds; diff --git a/packages/app-lib/src/api/screenshots.rs b/packages/app-lib/src/api/screenshots.rs new file mode 100644 index 0000000000..0613ce93ea --- /dev/null +++ b/packages/app-lib/src/api/screenshots.rs @@ -0,0 +1,128 @@ +use crate::profile::get_full_path; +use crate::util::io::{metadata, read_dir}; +use base64::{Engine, engine::general_purpose::STANDARD}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; +use tokio::fs::{canonicalize, read, remove_file}; + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Screenshot { + pub path: String, + pub creation_date: DateTime, +} + +pub async fn get_all_profile_screenshots( + profile_path: &str, +) -> crate::Result> { + let full = get_full_path(profile_path).await?; + get_all_screenshots_in_profile(&full).await +} + +pub async fn delete_profile_screenshot( + profile_path: &str, + screenshot: &Screenshot, +) -> crate::Result { + let full = get_full_path(profile_path).await?; + delete_screenshot_in_profile(&full, screenshot).await +} + +async fn delete_screenshot_in_profile( + profile_dir: &Path, + screenshot: &Screenshot, +) -> crate::Result { + if let Some(path) = + get_valid_screenshot_path(profile_dir, screenshot).await? + { + remove_file(path).await?; + Ok(true) + } else { + Ok(false) + } +} + +async fn get_all_screenshots_in_profile( + profile_dir: &Path, +) -> crate::Result> { + let screenshots_dir = profile_dir.join("screenshots"); + if metadata(&screenshots_dir).await.is_err() { + return Ok(Vec::new()); + } + + let mut dir = read_dir(&screenshots_dir).await?; + let mut screenshots = Vec::new(); + + while let Some(entry) = dir.next_entry().await? { + if !entry.file_type().await?.is_file() { + continue; + } + + let path = entry.path(); + if path + .extension() + .and_then(OsStr::to_str) + .map(|ext| ext.eq_ignore_ascii_case("png")) + != Some(true) + { + continue; + } + + let abs_path: PathBuf = canonicalize(&path).await?; + let full_path = abs_path.to_string_lossy().into_owned(); + + let meta = entry.metadata().await?; + let created_time = meta.created().unwrap_or(meta.modified()?); + let creation_date = DateTime::::from(created_time); + + screenshots.push(Screenshot { + path: full_path, + creation_date, + }); + } + + screenshots.sort_by_key(|s| s.creation_date); + Ok(screenshots) +} + +pub async fn get_screenshot_data( + profile_dir: &Path, + screenshot: &Screenshot, +) -> crate::Result> { + if let Some(valid_path) = + get_valid_screenshot_path(profile_dir, screenshot).await? + { + let bytes = read(&valid_path).await?; + let encoded = STANDARD.encode(&bytes); + Ok(Some(encoded)) + } else { + Ok(None) + } +} + +pub async fn get_valid_screenshot_path( + profile_dir: &Path, + screenshot: &Screenshot, +) -> crate::Result> { + let screenshots_dir = profile_dir.join("screenshots"); + if metadata(&screenshots_dir).await.is_err() { + return Ok(None); + } + + let canonical_dir = match canonicalize(&screenshots_dir).await { + Ok(d) => d, + Err(_) => return Ok(None), + }; + + let requested = PathBuf::from(&screenshot.path); + let canonical_req = match canonicalize(&requested).await { + Ok(p) => p, + Err(_) => return Ok(None), + }; + + if canonical_req.starts_with(&canonical_dir) { + Ok(Some(canonical_req)) + } else { + Ok(None) + } +} diff --git a/packages/app-lib/src/event/mod.rs b/packages/app-lib/src/event/mod.rs index 0c2b22df8b..b72ce89275 100644 --- a/packages/app-lib/src/event/mod.rs +++ b/packages/app-lib/src/event/mod.rs @@ -1,4 +1,5 @@ //! Theseus state management system +use crate::screenshots::Screenshot; use ariadne::users::{UserId, UserStatus}; use chrono::{DateTime, Utc}; use dashmap::DashMap; @@ -252,6 +253,10 @@ pub enum ProfilePayloadType { port: u16, timestamp: DateTime, }, + ScreenshotChanged { + file_exists: bool, + screenshot: Screenshot, + }, Edited, Removed, } diff --git a/packages/app-lib/src/state/fs_watcher.rs b/packages/app-lib/src/state/fs_watcher.rs index 347a7a38e4..fbb27e45be 100644 --- a/packages/app-lib/src/state/fs_watcher.rs +++ b/packages/app-lib/src/state/fs_watcher.rs @@ -1,10 +1,13 @@ +use std::path::Path; use crate::State; use crate::event::ProfilePayloadType; use crate::event::emit::{emit_profile, emit_warning}; +use crate::screenshots::Screenshot; use crate::state::{ DirectoryInfo, ProfileInstallStage, ProjectType, attached_world_data, }; use crate::worlds::WorldType; +use chrono::{DateTime, Utc}; use futures::{SinkExt, StreamExt, channel::mpsc::channel}; use notify::{RecommendedWatcher, RecursiveMode}; use notify_debouncer_mini::{DebounceEventResult, Debouncer, new_debouncer}; @@ -116,6 +119,34 @@ pub async fn init_watcher() -> crate::Result { }); } Some(ProfilePayloadType::WorldUpdated { world }) + } else if first_file_name + .filter(|x| *x == "screenshots") + .is_some() + && e.path + .extension() + .and_then(|ext| ext.to_str()) + .filter(|s| *s == "png") + .is_some() + { + let path_str = e.path.to_string_lossy().into_owned(); + let file_exists = Path::new(&e.path).exists(); + + let creation_date = if file_exists { + let meta = std::fs::metadata(&e.path).unwrap(); + let created_time = meta.created() + .unwrap_or_else(|_| meta.modified().unwrap()); + DateTime::::from(created_time) + } else { + Utc::now() + }; + + Some(ProfilePayloadType::ScreenshotChanged { + file_exists, + screenshot: Screenshot { + path: path_str, + creation_date, + }, + }) } else if first_file_name .filter(|x| *x == "saves") .is_none() @@ -177,6 +208,7 @@ pub(crate) async fn watch_profile( for sub_path in ProjectType::iterator().map(|x| x.get_folder()).chain([ "crash-reports", "saves", + "screenshots", "servers.dat", ]) { let full_path = profile_path.join(sub_path); diff --git a/packages/ui/eslint.config.mjs b/packages/ui/eslint.config.mjs index 05f559424b..76db133ffb 100644 --- a/packages/ui/eslint.config.mjs +++ b/packages/ui/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/packages/ui/src/components/base/AutoLink.vue b/packages/ui/src/components/base/AutoLink.vue index 3c883fc32a..f6413205ec 100644 --- a/packages/ui/src/components/base/AutoLink.vue +++ b/packages/ui/src/components/base/AutoLink.vue @@ -12,7 +12,6 @@ + + + + + + + + + {{ key.title }} + + + {{ key.description }} + + + + + + + Close + + + + {{ openExternallyTooltip }} + + + + + + + Toggle zoom + + + + + + + Previous + + + + + + Next + + + + + + + + + diff --git a/packages/ui/src/components/modal/TabbedModal.vue b/packages/ui/src/components/modal/TabbedModal.vue index bc3f2f763b..a5cd099bba 100644 --- a/packages/ui/src/components/modal/TabbedModal.vue +++ b/packages/ui/src/components/modal/TabbedModal.vue @@ -12,7 +12,6 @@ export type Tab = { } defineProps<{ - // eslint-disable-next-line @typescript-eslint/no-explicit-any tabs: Tab[] }>() diff --git a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue index 1e99f94e43..7f64e9a022 100644 --- a/packages/ui/src/components/project/ProjectSidebarCompatibility.vue +++ b/packages/ui/src/components/project/ProjectSidebarCompatibility.vue @@ -94,7 +94,7 @@ defineProps<{ loaders: string[] client_side: EnvironmentValue server_side: EnvironmentValue - // eslint-disable-next-line @typescript-eslint/no-explicit-any + versions: any[] } tags: { diff --git a/packages/ui/src/components/search/BrowseFiltersPanel.vue b/packages/ui/src/components/search/BrowseFiltersPanel.vue index b2a6ec3df2..dc552b6198 100644 --- a/packages/ui/src/components/search/BrowseFiltersPanel.vue +++ b/packages/ui/src/components/search/BrowseFiltersPanel.vue @@ -69,7 +69,6 @@ const props = defineProps<{ }>() const filters = computed(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any const filters: FilterType[] = [ { id: 'platform', diff --git a/packages/ui/src/composables/how-ago.ts b/packages/ui/src/composables/how-ago.ts index a9ef7e5200..bf5a1a85d4 100644 --- a/packages/ui/src/composables/how-ago.ts +++ b/packages/ui/src/composables/how-ago.ts @@ -3,7 +3,6 @@ import type { IntlController } from '@vintl/vintl/controller' import { useVIntl } from '@vintl/vintl' import { computed } from 'vue' -/* eslint-disable @typescript-eslint/no-explicit-any */ const formatters = new WeakMap, Formatter>() export function useRelativeTime(): Formatter { diff --git a/packages/utils/utils.ts b/packages/utils/utils.ts index 56f34523a5..9a4d02fe79 100644 --- a/packages/utils/utils.ts +++ b/packages/utils/utils.ts @@ -341,3 +341,14 @@ export const getArrayOrString = (x: string[] | string): string[] => { return x } } + +export function pxOf(varName: string) { + const el = document.createElement('div') + el.style.visibility = 'hidden' + el.style.position = 'absolute' + el.style.marginLeft = `var(${varName})` + document.body.appendChild(el) + const px = parseFloat(getComputedStyle(el).marginLeft) + document.body.removeChild(el) + return px +}
You haven't taken any screenshots today.
- {{ expandedGalleryItem.description }} -
+ {{ key.description }} +