chore(lint): resolve launcher lint issues
Format files and fix unused code, reactive collection usage, redundant mustaches, and missing each-block keys.
This commit is contained in:
1 parent
3ef59f94ee
commit
93536af6a6
10 files changed
+269
-172
No files matched your search
@@ -1,7 +1,3 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import * as path from '@tauri-apps/api/path';
|
|
||||||
import * as fs from '@tauri-apps/plugin-fs';
|
|
||||||
import { config } from './config';
|
import { config } from './config';
|
||||||
import {
|
import {
|
||||||
reloadThemes,
|
reloadThemes,
|
||||||
@@ -9,6 +5,10 @@ import {
|
|||||||
updateInstalledThemeTags,
|
updateInstalledThemeTags,
|
||||||
updateInstalledThemeWorkshopId,
|
updateInstalledThemeWorkshopId,
|
||||||
} from './themes';
|
} from './themes';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import * as path from '@tauri-apps/api/path';
|
||||||
|
import * as fs from '@tauri-apps/plugin-fs';
|
||||||
|
|
||||||
export interface CommunityThemeAuthor {
|
export interface CommunityThemeAuthor {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -159,7 +159,9 @@ const DEFAULT_THEMES_SERVICE_URL = 'https://themes.ez-pp.farm';
|
|||||||
|
|
||||||
export async function getThemesServiceUrl(): Promise<string> {
|
export async function getThemesServiceUrl(): Promise<string> {
|
||||||
const customUrl = await config.value<string>('themes_service_url').get('');
|
const customUrl = await config.value<string>('themes_service_url').get('');
|
||||||
return customUrl.trim().length > 0 ? customUrl.trim().replace(/\/+$/, '') : DEFAULT_THEMES_SERVICE_URL;
|
return customUrl.trim().length > 0
|
||||||
|
? customUrl.trim().replace(/\/+$/, '')
|
||||||
|
: DEFAULT_THEMES_SERVICE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,7 +275,9 @@ export async function fetchCommunityThemeDetails(themeId: string): Promise<Commu
|
|||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleCommunityThemeLike(themeId: string): Promise<{ liked: boolean; likeCount: number }> {
|
export async function toggleCommunityThemeLike(
|
||||||
|
themeId: string
|
||||||
|
): Promise<{ liked: boolean; likeCount: number }> {
|
||||||
const baseUrl = await getThemesServiceUrl();
|
const baseUrl = await getThemesServiceUrl();
|
||||||
const headers = await getAuthHeaders();
|
const headers = await getAuthHeaders();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Bell, CheckCheck, LoaderCircle, Megaphone, RefreshCw, ShieldAlert, X } from '@lucide/svelte';
|
|
||||||
import { Portal } from 'bits-ui';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { currentUser } from '$lib/userAuthentication';
|
|
||||||
import {
|
import {
|
||||||
fetchNotifications,
|
fetchNotifications,
|
||||||
fetchUnreadNotificationCount,
|
fetchUnreadNotificationCount,
|
||||||
@@ -10,6 +7,17 @@
|
|||||||
markNotificationRead,
|
markNotificationRead,
|
||||||
type UserNotification,
|
type UserNotification,
|
||||||
} from '$lib/notifications';
|
} from '$lib/notifications';
|
||||||
|
import { currentUser } from '$lib/userAuthentication';
|
||||||
|
import {
|
||||||
|
Bell,
|
||||||
|
CheckCheck,
|
||||||
|
LoaderCircle,
|
||||||
|
Megaphone,
|
||||||
|
RefreshCw,
|
||||||
|
ShieldAlert,
|
||||||
|
X,
|
||||||
|
} from '@lucide/svelte';
|
||||||
|
import { Portal } from 'bits-ui';
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
@@ -80,7 +88,10 @@
|
|||||||
beforeId: notifications[notifications.length - 1].id,
|
beforeId: notifications[notifications.length - 1].id,
|
||||||
});
|
});
|
||||||
const known = new Set(notifications.map((item) => item.id));
|
const known = new Set(notifications.map((item) => item.id));
|
||||||
notifications = [...notifications, ...data.notifications.filter((item) => !known.has(item.id))];
|
notifications = [
|
||||||
|
...notifications,
|
||||||
|
...data.notifications.filter((item) => !known.has(item.id)),
|
||||||
|
];
|
||||||
unreadCount = data.unreadCount;
|
unreadCount = data.unreadCount;
|
||||||
hasMore = data.hasMore;
|
hasMore = data.hasMore;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -164,7 +175,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="notification-hub-title" class="flex items-center gap-2 text-sm font-semibold text-white">
|
<h2
|
||||||
|
id="notification-hub-title"
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
<Bell class="size-4 text-primary" />
|
<Bell class="size-4 text-primary" />
|
||||||
Notifications
|
Notifications
|
||||||
{#if unreadCount > 0}
|
{#if unreadCount > 0}
|
||||||
@@ -173,7 +187,9 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-[11px] text-muted-foreground">Moderation updates and system messages.</p>
|
<p class="mt-1 text-[11px] text-muted-foreground">
|
||||||
|
Moderation updates and system messages.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
@@ -220,7 +236,9 @@
|
|||||||
|
|
||||||
<div class="min-h-0 flex-1 overflow-y-auto p-2">
|
<div class="min-h-0 flex-1 overflow-y-auto p-2">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="m-2 rounded-lg border border-red-900/40 bg-red-950/20 p-3 text-[11px] text-red-300">
|
<div
|
||||||
|
class="m-2 rounded-lg border border-red-900/40 bg-red-950/20 p-3 text-[11px] text-red-300"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import DefaultThemePreview from '$assets/default_preview.png';
|
||||||
import {
|
import {
|
||||||
fetchCommunityTags,
|
fetchCommunityTags,
|
||||||
fetchCommunityThemes,
|
fetchCommunityThemes,
|
||||||
@@ -28,12 +28,6 @@
|
|||||||
type ThemeReportQuery,
|
type ThemeReportQuery,
|
||||||
type ThemeReportStatus,
|
type ThemeReportStatus,
|
||||||
} from '$lib/communityThemes';
|
} from '$lib/communityThemes';
|
||||||
import { active_custom_theme, custom_themes } from '$lib/global';
|
|
||||||
import { reduceAnimations } from '$lib/userSettings';
|
|
||||||
import { currentUser } from '$lib/userAuthentication';
|
|
||||||
import { isThemeTrusted } from '$lib/themeTrust';
|
|
||||||
import { reloadThemes, updateInstalledThemeTags, type Theme } from '$lib/themes';
|
|
||||||
import { normalizeThemeTags } from '$lib/themeV2';
|
|
||||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||||
@@ -41,13 +35,17 @@
|
|||||||
import * as Dialog from '$lib/components/ui/dialog';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import * as Popover from '$lib/components/ui/popover';
|
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import * as Popover from '$lib/components/ui/popover';
|
||||||
import ScrollContainer from '$lib/components/ui/scroll-container/ScrollContainer.svelte';
|
import ScrollContainer from '$lib/components/ui/scroll-container/ScrollContainer.svelte';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import { sileo } from 'sileo';
|
|
||||||
import { sendSystemAnnouncement } from '$lib/notifications';
|
|
||||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||||
|
import { active_custom_theme, custom_themes } from '$lib/global';
|
||||||
|
import { sendSystemAnnouncement } from '$lib/notifications';
|
||||||
|
import { isThemeTrusted } from '$lib/themeTrust';
|
||||||
|
import { normalizeThemeTags } from '$lib/themeV2';
|
||||||
|
import { reloadThemes, updateInstalledThemeTags, type Theme } from '$lib/themes';
|
||||||
|
import { currentUser } from '$lib/userAuthentication';
|
||||||
import WorkshopTooltip from './WorkshopTooltip.svelte';
|
import WorkshopTooltip from './WorkshopTooltip.svelte';
|
||||||
import {
|
import {
|
||||||
Archive,
|
Archive,
|
||||||
@@ -80,7 +78,9 @@
|
|||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import DefaultThemePreview from '$assets/default_preview.png';
|
import { sileo } from 'sileo';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { SvelteMap } from 'svelte/reactivity';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
onApplyTheme,
|
onApplyTheme,
|
||||||
@@ -117,7 +117,6 @@
|
|||||||
|
|
||||||
let downloadingId = $state<string | null>(null);
|
let downloadingId = $state<string | null>(null);
|
||||||
let downloadProgress = $state(0);
|
let downloadProgress = $state(0);
|
||||||
let downloadStatus = $state('');
|
|
||||||
let forkingId = $state<string | null>(null);
|
let forkingId = $state<string | null>(null);
|
||||||
let deletingId = $state<string | null>(null);
|
let deletingId = $state<string | null>(null);
|
||||||
let moderatingId = $state<string | null>(null);
|
let moderatingId = $state<string | null>(null);
|
||||||
@@ -220,7 +219,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
const localThemeMap = $derived.by(() => {
|
const localThemeMap = $derived.by(() => {
|
||||||
const map = new Map<string, Theme>();
|
const map = new SvelteMap<string, Theme>();
|
||||||
for (const t of $custom_themes) {
|
for (const t of $custom_themes) {
|
||||||
map.set(t.folder_name.toLowerCase(), t);
|
map.set(t.folder_name.toLowerCase(), t);
|
||||||
map.set(t.name.toLowerCase(), t);
|
map.set(t.name.toLowerCase(), t);
|
||||||
@@ -229,7 +228,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tagOptions = $derived.by(() => {
|
const tagOptions = $derived.by(() => {
|
||||||
const counts = new Map<string, number>();
|
const counts = new SvelteMap<string, number>();
|
||||||
|
|
||||||
// The service returns every used tag. While it is unavailable or loading,
|
// The service returns every used tag. While it is unavailable or loading,
|
||||||
// fall back to tags visible on the current page so filtering still works.
|
// fall back to tags visible on the current page so filtering still works.
|
||||||
@@ -362,10 +361,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseWorkshopSort(value: string): WorkshopSort {
|
function parseWorkshopSort(value: string): WorkshopSort {
|
||||||
return value === 'relevance' ||
|
return value === 'relevance' || value === 'recent' || value === 'likes' || value === 'downloads'
|
||||||
value === 'recent' ||
|
|
||||||
value === 'likes' ||
|
|
||||||
value === 'downloads'
|
|
||||||
? value
|
? value
|
||||||
: 'popular';
|
: 'popular';
|
||||||
}
|
}
|
||||||
@@ -406,11 +402,9 @@
|
|||||||
|
|
||||||
downloadingId = theme.id;
|
downloadingId = theme.id;
|
||||||
downloadProgress = 10;
|
downloadProgress = 10;
|
||||||
downloadStatus = 'Downloading theme package...';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
||||||
downloadStatus = msg;
|
|
||||||
downloadProgress = progress;
|
downloadProgress = progress;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -440,15 +434,13 @@
|
|||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingId = null;
|
||||||
downloadProgress = 0;
|
downloadProgress = 0;
|
||||||
downloadStatus = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApplyExisting(theme: CommunityTheme) {
|
async function handleApplyExisting(theme: CommunityTheme) {
|
||||||
if (theme.deletedAt) return;
|
if (theme.deletedAt) return;
|
||||||
const installed =
|
const installed =
|
||||||
localThemeMap.get(theme.id.toLowerCase()) ||
|
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
||||||
localThemeMap.get(theme.name.toLowerCase());
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
await handleDownloadAndApply(theme);
|
await handleDownloadAndApply(theme);
|
||||||
return;
|
return;
|
||||||
@@ -465,16 +457,13 @@
|
|||||||
if (theme.deletedAt || downloadingId || forkingId || deletingId || moderatingId) return;
|
if (theme.deletedAt || downloadingId || forkingId || deletingId || moderatingId) return;
|
||||||
|
|
||||||
let installed =
|
let installed =
|
||||||
localThemeMap.get(theme.id.toLowerCase()) ||
|
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
||||||
localThemeMap.get(theme.name.toLowerCase());
|
|
||||||
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
downloadingId = theme.id;
|
downloadingId = theme.id;
|
||||||
downloadProgress = 10;
|
downloadProgress = 10;
|
||||||
downloadStatus = 'Downloading theme package...';
|
|
||||||
try {
|
try {
|
||||||
await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
||||||
downloadStatus = msg;
|
|
||||||
downloadProgress = progress;
|
downloadProgress = progress;
|
||||||
});
|
});
|
||||||
installed = $custom_themes.find(
|
installed = $custom_themes.find(
|
||||||
@@ -489,7 +478,6 @@
|
|||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingId = null;
|
||||||
downloadProgress = 0;
|
downloadProgress = 0;
|
||||||
downloadStatus = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,8 +490,7 @@
|
|||||||
if (theme.deletedAt || downloadingId || forkingId || deletingId || moderatingId) return;
|
if (theme.deletedAt || downloadingId || forkingId || deletingId || moderatingId) return;
|
||||||
|
|
||||||
const installed =
|
const installed =
|
||||||
localThemeMap.get(theme.id.toLowerCase()) ||
|
localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase());
|
||||||
localThemeMap.get(theme.name.toLowerCase());
|
|
||||||
if (!installed) {
|
if (!installed) {
|
||||||
await handleEditTheme(theme);
|
await handleEditTheme(theme);
|
||||||
return;
|
return;
|
||||||
@@ -520,10 +507,8 @@
|
|||||||
|
|
||||||
downloadingId = theme.id;
|
downloadingId = theme.id;
|
||||||
downloadProgress = 10;
|
downloadProgress = 10;
|
||||||
downloadStatus = 'Refreshing local theme...';
|
|
||||||
try {
|
try {
|
||||||
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
const res = await downloadAndInstallCommunityTheme(theme, (msg, progress) => {
|
||||||
downloadStatus = msg;
|
|
||||||
downloadProgress = progress;
|
downloadProgress = progress;
|
||||||
});
|
});
|
||||||
const refreshed = $custom_themes.find(
|
const refreshed = $custom_themes.find(
|
||||||
@@ -545,7 +530,6 @@
|
|||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingId = null;
|
||||||
downloadProgress = 0;
|
downloadProgress = 0;
|
||||||
downloadStatus = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,8 +650,7 @@
|
|||||||
themes = themes.map((item) => (item.id === updated.id ? updated : item));
|
themes = themes.map((item) => (item.id === updated.id ? updated : item));
|
||||||
|
|
||||||
const installed =
|
const installed =
|
||||||
localThemeMap.get(target.id.toLowerCase()) ||
|
localThemeMap.get(target.id.toLowerCase()) || localThemeMap.get(target.name.toLowerCase());
|
||||||
localThemeMap.get(target.name.toLowerCase());
|
|
||||||
let localSyncError: string | null = null;
|
let localSyncError: string | null = null;
|
||||||
if (installed) {
|
if (installed) {
|
||||||
try {
|
try {
|
||||||
@@ -746,9 +729,7 @@
|
|||||||
await loadWorkshopThemes();
|
await loadWorkshopThemes();
|
||||||
downloadingId = fork.id;
|
downloadingId = fork.id;
|
||||||
downloadProgress = 10;
|
downloadProgress = 10;
|
||||||
downloadStatus = 'Downloading your fork...';
|
|
||||||
const res = await downloadAndInstallCommunityTheme(fork, (msg, progress) => {
|
const res = await downloadAndInstallCommunityTheme(fork, (msg, progress) => {
|
||||||
downloadStatus = msg;
|
|
||||||
downloadProgress = progress;
|
downloadProgress = progress;
|
||||||
});
|
});
|
||||||
installed = $custom_themes.find(
|
installed = $custom_themes.find(
|
||||||
@@ -762,7 +743,6 @@
|
|||||||
} finally {
|
} finally {
|
||||||
downloadingId = null;
|
downloadingId = null;
|
||||||
downloadProgress = 0;
|
downloadProgress = 0;
|
||||||
downloadStatus = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (installed) {
|
if (installed) {
|
||||||
@@ -1106,7 +1086,9 @@
|
|||||||
const truncated = await downloadThemeReportsCsv(currentReportFilters());
|
const truncated = await downloadThemeReportsCsv(currentReportFilters());
|
||||||
sileo.success({
|
sileo.success({
|
||||||
title: 'Report export downloaded',
|
title: 'Report export downloaded',
|
||||||
description: truncated ? 'The export is capped at 5,000 rows. Narrow the date range for the rest.' : undefined,
|
description: truncated
|
||||||
|
? 'The export is capped at 5,000 rows. Narrow the date range for the rest.'
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sileo.error({
|
sileo.error({
|
||||||
@@ -1200,10 +1182,7 @@
|
|||||||
limit: 50,
|
limit: 50,
|
||||||
});
|
});
|
||||||
const known = new Set(historyEvents.map((event) => event.id));
|
const known = new Set(historyEvents.map((event) => event.id));
|
||||||
historyEvents = [
|
historyEvents = [...historyEvents, ...result.events.filter((event) => !known.has(event.id))];
|
||||||
...historyEvents,
|
|
||||||
...result.events.filter((event) => !known.has(event.id)),
|
|
||||||
];
|
|
||||||
historyPage = result.page;
|
historyPage = result.page;
|
||||||
historyTotalPages = result.totalPages;
|
historyTotalPages = result.totalPages;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1256,21 +1235,24 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tooltip.Provider delayDuration={220}>
|
<Tooltip.Provider delayDuration={220}>
|
||||||
<div class="flex h-full flex-col overflow-hidden">
|
<div class="flex h-full flex-col overflow-hidden">
|
||||||
<!-- Workshop Filter & Search Toolbar -->
|
<!-- Workshop Filter & Search Toolbar -->
|
||||||
<div class="flex flex-col gap-2.5 pb-3">
|
<div class="flex flex-col gap-2.5 pb-3">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<!-- Tag Chips / Trash label -->
|
<!-- Tag Chips / Trash label -->
|
||||||
<div class="flex min-w-0 flex-wrap items-center gap-1">
|
<div class="flex min-w-0 flex-wrap items-center gap-1">
|
||||||
{#if selectedView === 'trash'}
|
{#if selectedView === 'trash'}
|
||||||
<span class="flex items-center gap-1 rounded-full border border-red-900/40 bg-red-950/40 px-2.5 py-1 text-[11px] font-medium text-red-300">
|
<span
|
||||||
|
class="flex items-center gap-1 rounded-full border border-red-900/40 bg-red-950/40 px-2.5 py-1 text-[11px] font-medium text-red-300"
|
||||||
|
>
|
||||||
<Archive class="size-3" />
|
<Archive class="size-3" />
|
||||||
Your trash
|
Your trash
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors {selectedTag === 'all'
|
class="rounded-full px-2.5 py-1 text-[11px] font-medium transition-colors {selectedTag ===
|
||||||
|
'all'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'bg-theme-900/60 text-muted-foreground hover:bg-theme-800/60 hover:text-white'}"
|
: 'bg-theme-900/60 text-muted-foreground hover:bg-theme-800/60 hover:text-white'}"
|
||||||
onclick={() => handleTagClick('all')}
|
onclick={() => handleTagClick('all')}
|
||||||
@@ -1407,14 +1389,20 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div class="relative w-48">
|
<div class="relative w-48">
|
||||||
<Search class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground" />
|
<Search
|
||||||
<WorkshopTooltip content="Typed search: tag:anime, author:name, name:night, id:my-theme">
|
class="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<WorkshopTooltip
|
||||||
|
content="Typed search: tag:anime, author:name, name:night, id:my-theme"
|
||||||
|
>
|
||||||
{#snippet trigger(props)}
|
{#snippet trigger(props)}
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
type="text"
|
type="text"
|
||||||
class="h-7.5 border-theme-800 bg-theme-950/80 pl-8 text-[11px]"
|
class="h-7.5 border-theme-800 bg-theme-950/80 pl-8 text-[11px]"
|
||||||
placeholder={selectedView === 'trash' ? 'Search trash...' : 'Search themes, tags, authors...'}
|
placeholder={selectedView === 'trash'
|
||||||
|
? 'Search trash...'
|
||||||
|
: 'Search themes, tags, authors...'}
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
oninput={handleSearchInput}
|
oninput={handleSearchInput}
|
||||||
/>
|
/>
|
||||||
@@ -1494,7 +1482,12 @@
|
|||||||
{error}. Ensure the themes microservice container is running.
|
{error}. Ensure the themes microservice container is running.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" size="sm" class="mt-2 text-xs" onclick={() => void loadWorkshopThemes()}>
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
class="mt-2 text-xs"
|
||||||
|
onclick={() => void loadWorkshopThemes()}
|
||||||
|
>
|
||||||
<RefreshCw class="mr-1.5 size-3.5" />
|
<RefreshCw class="mr-1.5 size-3.5" />
|
||||||
Try Again
|
Try Again
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1507,12 +1500,18 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-3 gap-3 pb-4">
|
<div class="grid grid-cols-3 gap-3 pb-4">
|
||||||
{#each themes as theme (theme.id)}
|
{#each themes as theme (theme.id)}
|
||||||
{@const localTheme = localThemeMap.get(theme.id.toLowerCase()) || localThemeMap.get(theme.name.toLowerCase())}
|
{@const localTheme =
|
||||||
|
localThemeMap.get(theme.id.toLowerCase()) ||
|
||||||
|
localThemeMap.get(theme.name.toLowerCase())}
|
||||||
{@const isInstalled = Boolean(localTheme)}
|
{@const isInstalled = Boolean(localTheme)}
|
||||||
{@const isActive = Boolean(localTheme && $active_custom_theme?.folder_name === localTheme.folder_name)}
|
{@const isActive = Boolean(
|
||||||
|
localTheme && $active_custom_theme?.folder_name === localTheme.folder_name
|
||||||
|
)}
|
||||||
{@const isDownloading = downloadingId === theme.id}
|
{@const isDownloading = downloadingId === theme.id}
|
||||||
{@const isDeleted = Boolean(theme.deletedAt)}
|
{@const isDeleted = Boolean(theme.deletedAt)}
|
||||||
{@const hasUpdate = Boolean(!isDeleted && localTheme && localTheme.version !== theme.version)}
|
{@const hasUpdate = Boolean(
|
||||||
|
!isDeleted && localTheme && localTheme.version !== theme.version
|
||||||
|
)}
|
||||||
{@const owned = isOwnedByCurrentUser(theme)}
|
{@const owned = isOwnedByCurrentUser(theme)}
|
||||||
{@const isLegacy =
|
{@const isLegacy =
|
||||||
theme.apiVersion === '1.0' || Boolean(theme.tags?.includes('legacy'))}
|
theme.apiVersion === '1.0' || Boolean(theme.tags?.includes('legacy'))}
|
||||||
@@ -1537,7 +1536,9 @@
|
|||||||
(e.currentTarget as HTMLImageElement).src = DefaultThemePreview;
|
(e.currentTarget as HTMLImageElement).src = DefaultThemePreview;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="pointer-events-none absolute inset-0 bg-gradient-to-t from-theme-950/90 via-transparent to-transparent"></div>
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 bg-gradient-to-t from-theme-950/90 via-transparent to-transparent"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- Top Left: Trust Badge -->
|
<!-- Top Left: Trust Badge -->
|
||||||
<div class="absolute top-2 left-2 flex items-center gap-1.5">
|
<div class="absolute top-2 left-2 flex items-center gap-1.5">
|
||||||
@@ -1622,13 +1623,17 @@
|
|||||||
|
|
||||||
<!-- Top Right: Version -->
|
<!-- Top Right: Version -->
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<span class="rounded-md bg-black/60 px-1.5 py-0.5 text-[9px] font-medium text-white/90 backdrop-blur-sm">
|
<span
|
||||||
|
class="rounded-md bg-black/60 px-1.5 py-0.5 text-[9px] font-medium text-white/90 backdrop-blur-sm"
|
||||||
|
>
|
||||||
v{theme.version}
|
v{theme.version}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom: Author Info -->
|
<!-- Bottom: Author Info -->
|
||||||
<div class="absolute right-2 bottom-2 left-2 flex items-center justify-between gap-2">
|
<div
|
||||||
|
class="absolute right-2 bottom-2 left-2 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
<div class="flex min-w-0 items-center gap-1.5">
|
<div class="flex min-w-0 items-center gap-1.5">
|
||||||
<img
|
<img
|
||||||
src={theme.author.avatarUrl}
|
src={theme.author.avatarUrl}
|
||||||
@@ -1692,13 +1697,17 @@
|
|||||||
<div class="flex flex-1 flex-col justify-between gap-2 p-3">
|
<div class="flex flex-1 flex-col justify-between gap-2 p-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<h3 class="truncate text-[13px] font-bold text-white group-hover:text-primary transition-colors">
|
<h3
|
||||||
|
class="truncate text-[13px] font-bold text-white group-hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
{theme.name}
|
{theme.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<!-- Interactive Like Button -->
|
<!-- Interactive Like Button -->
|
||||||
{#if isDeleted}
|
{#if isDeleted}
|
||||||
<span class="flex items-center gap-1 rounded-full bg-red-950/40 px-2 py-0.5 text-[10px] text-red-300">
|
<span
|
||||||
|
class="flex items-center gap-1 rounded-full bg-red-950/40 px-2 py-0.5 text-[10px] text-red-300"
|
||||||
|
>
|
||||||
<Trash2 class="size-3" /> Deleted
|
<Trash2 class="size-3" /> Deleted
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -1712,7 +1721,9 @@
|
|||||||
: 'bg-theme-800/40 text-muted-foreground hover:bg-theme-800/80 hover:text-white'}"
|
: 'bg-theme-800/40 text-muted-foreground hover:bg-theme-800/80 hover:text-white'}"
|
||||||
onclick={(e) => handleLike(theme, e)}
|
onclick={(e) => handleLike(theme, e)}
|
||||||
>
|
>
|
||||||
<Heart class="size-3 {theme.isLiked ? 'fill-rose-400 text-rose-400' : ''}" />
|
<Heart
|
||||||
|
class="size-3 {theme.isLiked ? 'fill-rose-400 text-rose-400' : ''}"
|
||||||
|
/>
|
||||||
<span class="tabular-nums text-[10px]">{theme.likeCount}</span>
|
<span class="tabular-nums text-[10px]">{theme.likeCount}</span>
|
||||||
</button>
|
</button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -1721,7 +1732,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if theme.description}
|
{#if theme.description}
|
||||||
<p class="mt-1 line-clamp-2 text-[10px] leading-relaxed text-muted-foreground">
|
<p
|
||||||
|
class="mt-1 line-clamp-2 text-[10px] leading-relaxed text-muted-foreground"
|
||||||
|
>
|
||||||
{theme.description}
|
{theme.description}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -1750,7 +1763,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if theme.isHidden || theme.isQuarantined}
|
{#if theme.isHidden || theme.isQuarantined}
|
||||||
<div class="mt-2 space-y-1 rounded-md border border-red-900/40 bg-red-950/15 px-2 py-1.5 text-[9px] leading-relaxed">
|
<div
|
||||||
|
class="mt-2 space-y-1 rounded-md border border-red-900/40 bg-red-950/15 px-2 py-1.5 text-[9px] leading-relaxed"
|
||||||
|
>
|
||||||
{#if theme.isHidden}
|
{#if theme.isHidden}
|
||||||
<p class="text-red-200/90">
|
<p class="text-red-200/90">
|
||||||
<EyeOff class="mr-1 inline size-3 align-[-2px]" />
|
<EyeOff class="mr-1 inline size-3 align-[-2px]" />
|
||||||
@@ -1780,7 +1795,9 @@
|
|||||||
|
|
||||||
{#if theme.forkedFrom}
|
{#if theme.forkedFrom}
|
||||||
{@const forkedFrom = theme.forkedFrom}
|
{@const forkedFrom = theme.forkedFrom}
|
||||||
<WorkshopTooltip content={`Open ${forkedFrom.name} by ${forkedFrom.authorName}`}>
|
<WorkshopTooltip
|
||||||
|
content={`Open ${forkedFrom.name} by ${forkedFrom.authorName}`}
|
||||||
|
>
|
||||||
{#snippet trigger(props)}
|
{#snippet trigger(props)}
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
@@ -2053,9 +2070,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Dialog.Root bind:open={showModerationPanel}>
|
<Dialog.Root bind:open={showModerationPanel}>
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="top-12 bottom-4 flex h-auto w-[calc(100%_-_2rem)] max-w-6xl translate-y-0 flex-col gap-0 overflow-hidden rounded-xl border-theme-800 bg-theme-950 p-0 text-white shadow-2xl sm:rounded-xl"
|
class="top-12 bottom-4 flex h-auto w-[calc(100%_-_2rem)] max-w-6xl translate-y-0 flex-col gap-0 overflow-hidden rounded-xl border-theme-800 bg-theme-950 p-0 text-white shadow-2xl sm:rounded-xl"
|
||||||
>
|
>
|
||||||
@@ -2281,7 +2298,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<section>
|
<section>
|
||||||
<h3 class="flex items-center gap-2 text-[11px] font-semibold tracking-wide text-white/80 uppercase">
|
<h3
|
||||||
|
class="flex items-center gap-2 text-[11px] font-semibold tracking-wide text-white/80 uppercase"
|
||||||
|
>
|
||||||
<Flag class="size-3.5 text-amber-300" />
|
<Flag class="size-3.5 text-amber-300" />
|
||||||
Reports
|
Reports
|
||||||
<Badge
|
<Badge
|
||||||
@@ -2303,7 +2322,9 @@
|
|||||||
</Label>
|
</Label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if moderationReports.length === 0}
|
{#if moderationReports.length === 0}
|
||||||
<p class="mt-2 rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground">
|
<p
|
||||||
|
class="mt-2 rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground"
|
||||||
|
>
|
||||||
No reports match these filters.
|
No reports match these filters.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -2323,7 +2344,9 @@
|
|||||||
<div class="flex items-start justify-between gap-3">
|
<div class="flex items-start justify-between gap-3">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex flex-wrap items-center gap-1.5">
|
<div class="flex flex-wrap items-center gap-1.5">
|
||||||
<p class="truncate text-xs font-semibold text-white">{report.themeName}</p>
|
<p class="truncate text-xs font-semibold text-white">
|
||||||
|
{report.themeName}
|
||||||
|
</p>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="border-theme-700/70 bg-theme-800/60 px-1.5 py-0.5 text-[9px] text-muted-foreground"
|
class="border-theme-700/70 bg-theme-800/60 px-1.5 py-0.5 text-[9px] text-muted-foreground"
|
||||||
@@ -2354,7 +2377,9 @@
|
|||||||
{formatModerationDate(report.createdAt)}
|
{formatModerationDate(report.createdAt)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-[11px] leading-relaxed text-white/80">{report.reason}</p>
|
<p class="mt-2 text-[11px] leading-relaxed text-white/80">
|
||||||
|
{report.reason}
|
||||||
|
</p>
|
||||||
{#if report.status !== 'open'}
|
{#if report.status !== 'open'}
|
||||||
<p class="mt-2 text-[10px] text-muted-foreground">
|
<p class="mt-2 text-[10px] text-muted-foreground">
|
||||||
{report.status === 'resolved' ? 'Resolved' : 'Dismissed'} by
|
{report.status === 'resolved' ? 'Resolved' : 'Dismissed'} by
|
||||||
@@ -2364,7 +2389,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{#if report.resolutionNote}
|
{#if report.resolutionNote}
|
||||||
<p class="mt-1 rounded border border-theme-800/60 bg-theme-950/40 px-2 py-1 text-[10px] text-muted-foreground">
|
<p
|
||||||
|
class="mt-1 rounded border border-theme-800/60 bg-theme-950/40 px-2 py-1 text-[10px] text-muted-foreground"
|
||||||
|
>
|
||||||
Note: {report.resolutionNote}
|
Note: {report.resolutionNote}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2438,7 +2465,9 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<h3 class="flex items-center gap-2 text-[11px] font-semibold tracking-wide text-white/80 uppercase">
|
<h3
|
||||||
|
class="flex items-center gap-2 text-[11px] font-semibold tracking-wide text-white/80 uppercase"
|
||||||
|
>
|
||||||
<History class="size-3.5 text-cyan-300" />
|
<History class="size-3.5 text-cyan-300" />
|
||||||
Recent activity
|
Recent activity
|
||||||
</h3>
|
</h3>
|
||||||
@@ -2498,7 +2527,9 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{#if moderationEvents.length === 0}
|
{#if moderationEvents.length === 0}
|
||||||
<p class="mt-2 rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground">
|
<p
|
||||||
|
class="mt-2 rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground"
|
||||||
|
>
|
||||||
No moderation events yet.
|
No moderation events yet.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -2508,9 +2539,9 @@
|
|||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<p class="truncate text-[11px] text-white/80">
|
<p class="truncate text-[11px] text-white/80">
|
||||||
<span class="font-semibold text-white">{event.actorName}</span>
|
<span class="font-semibold text-white">{event.actorName}</span>
|
||||||
{' · '}
|
·
|
||||||
{moderationActionLabel(event.action)}
|
{moderationActionLabel(event.action)}
|
||||||
{' · '}
|
·
|
||||||
<span class="text-white/60">{event.themeName}</span>
|
<span class="text-white/60">{event.themeName}</span>
|
||||||
</p>
|
</p>
|
||||||
<span class="shrink-0 text-[9px] text-muted-foreground">
|
<span class="shrink-0 text-[9px] text-muted-foreground">
|
||||||
@@ -2520,7 +2551,11 @@
|
|||||||
{#if event.reason}
|
{#if event.reason}
|
||||||
<WorkshopTooltip content={event.reason}>
|
<WorkshopTooltip content={event.reason}>
|
||||||
{#snippet trigger(props)}
|
{#snippet trigger(props)}
|
||||||
<p {...props} tabindex="-1" class="mt-1 truncate text-[10px] text-muted-foreground">
|
<p
|
||||||
|
{...props}
|
||||||
|
tabindex="-1"
|
||||||
|
class="mt-1 truncate text-[10px] text-muted-foreground"
|
||||||
|
>
|
||||||
{event.reason}
|
{event.reason}
|
||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -2547,9 +2582,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
</Dialog.Root>
|
</Dialog.Root>
|
||||||
|
|
||||||
{#if announcementOpen}
|
{#if announcementOpen}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2567,7 +2602,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="announcement-title" class="flex items-center gap-2 text-sm font-semibold text-white">
|
<h2
|
||||||
|
id="announcement-title"
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
<Megaphone class="size-4 text-cyan-300" /> Send announcement
|
<Megaphone class="size-4 text-cyan-300" /> Send announcement
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-[11px] text-muted-foreground">
|
<p class="mt-1 text-[11px] text-muted-foreground">
|
||||||
@@ -2592,7 +2630,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 p-4">
|
<div class="space-y-3 p-4">
|
||||||
<label class="block space-y-1.5">
|
<label class="block space-y-1.5">
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase">Title</span>
|
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
|
||||||
|
>Title</span
|
||||||
|
>
|
||||||
<Input
|
<Input
|
||||||
class="h-8 border-theme-800 bg-theme-900/60 text-[11px]"
|
class="h-8 border-theme-800 bg-theme-900/60 text-[11px]"
|
||||||
maxlength={120}
|
maxlength={120}
|
||||||
@@ -2601,7 +2641,9 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="block space-y-1.5">
|
<label class="block space-y-1.5">
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase">Message</span>
|
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
|
||||||
|
>Message</span
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
class="min-h-28 w-full resize-y rounded-md border border-theme-800 bg-theme-900/60 p-2.5 text-[11px] leading-relaxed text-white outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
|
class="min-h-28 w-full resize-y rounded-md border border-theme-800 bg-theme-900/60 p-2.5 text-[11px] leading-relaxed text-white outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
|
||||||
rows="5"
|
rows="5"
|
||||||
@@ -2614,7 +2656,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="block space-y-1.5">
|
<label class="block space-y-1.5">
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase">Audience</span>
|
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
|
||||||
|
>Audience</span
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50"
|
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50"
|
||||||
bind:value={announcementAudience}
|
bind:value={announcementAudience}
|
||||||
@@ -2624,7 +2668,9 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{#if announcementError}
|
{#if announcementError}
|
||||||
<p class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300">
|
<p
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300"
|
||||||
|
>
|
||||||
{announcementError}
|
{announcementError}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2654,9 +2700,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sourceDialogOpen}
|
{#if sourceDialogOpen}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2672,7 +2718,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="fork-source-title" class="flex items-center gap-2 text-sm font-semibold text-white">
|
<h2
|
||||||
|
id="fork-source-title"
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
<GitFork class="size-4 text-cyan-300" /> Fork origin
|
<GitFork class="size-4 text-cyan-300" /> Fork origin
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-[11px] text-muted-foreground">The immediate source of this fork.</p>
|
<p class="mt-1 text-[11px] text-muted-foreground">The immediate source of this fork.</p>
|
||||||
@@ -2697,7 +2746,9 @@
|
|||||||
<LoaderCircle class="size-4 animate-spin text-primary" /> Loading original...
|
<LoaderCircle class="size-4 animate-spin text-primary" /> Loading original...
|
||||||
</div>
|
</div>
|
||||||
{:else if sourceError}
|
{:else if sourceError}
|
||||||
<p class="rounded-lg border border-amber-900/40 bg-amber-950/20 p-3 text-[11px] text-amber-200">
|
<p
|
||||||
|
class="rounded-lg border border-amber-900/40 bg-amber-950/20 p-3 text-[11px] text-amber-200"
|
||||||
|
>
|
||||||
{sourceError}
|
{sourceError}
|
||||||
</p>
|
</p>
|
||||||
{:else if sourceTheme}
|
{:else if sourceTheme}
|
||||||
@@ -2727,23 +2778,26 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if sourceTheme.tags.length > 0}
|
{#if sourceTheme.tags.length > 0}
|
||||||
<div class="mt-3 flex flex-wrap gap-1">
|
<div class="mt-3 flex flex-wrap gap-1">
|
||||||
{#each sourceTheme.tags as tag}
|
{#each sourceTheme.tags as tag (tag)}
|
||||||
<span class="rounded bg-theme-800/50 px-1.5 py-0.5 text-[9px] text-muted-foreground">
|
<span
|
||||||
|
class="rounded bg-theme-800/50 px-1.5 py-0.5 text-[9px] text-muted-foreground"
|
||||||
|
>
|
||||||
#{tag}
|
#{tag}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="mt-4 border-t border-theme-800/60 pt-3 text-[10px] text-muted-foreground">
|
<p class="mt-4 border-t border-theme-800/60 pt-3 text-[10px] text-muted-foreground">
|
||||||
This fork keeps a snapshot of the source at fork time. The original may have changed since.
|
This fork keeps a snapshot of the source at fork time. The original may have changed
|
||||||
|
since.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if tagEditorTarget}
|
{#if tagEditorTarget}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2875,9 +2929,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if reportTarget}
|
{#if reportTarget}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2895,7 +2949,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="report-theme-title" class="flex items-center gap-2 text-sm font-semibold text-white">
|
<h2
|
||||||
|
id="report-theme-title"
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
<Flag class="size-4 text-amber-300" /> Report theme
|
<Flag class="size-4 text-amber-300" /> Report theme
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 text-[11px] text-muted-foreground">
|
<p class="mt-1 text-[11px] text-muted-foreground">
|
||||||
@@ -2921,18 +2978,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 p-4">
|
<div class="space-y-3 p-4">
|
||||||
<label class="block space-y-1.5">
|
<label class="block space-y-1.5">
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase">Category</span>
|
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
|
||||||
|
>Category</span
|
||||||
|
>
|
||||||
<select
|
<select
|
||||||
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50"
|
class="h-8 w-full rounded-md border border-theme-800 bg-theme-900/60 px-2 text-[11px] text-white outline-none focus:border-primary/50"
|
||||||
bind:value={reportCategory}
|
bind:value={reportCategory}
|
||||||
>
|
>
|
||||||
{#each REPORT_CATEGORIES as category}
|
{#each REPORT_CATEGORIES as category (category.id)}
|
||||||
<option value={category.id}>{category.label}</option>
|
<option value={category.id}>{category.label}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="block space-y-1.5">
|
<label class="block space-y-1.5">
|
||||||
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase">Details</span>
|
<span class="text-[10px] font-semibold tracking-wide text-white/70 uppercase"
|
||||||
|
>Details</span
|
||||||
|
>
|
||||||
<textarea
|
<textarea
|
||||||
class="min-h-28 w-full resize-y rounded-md border border-theme-800 bg-theme-900/60 p-2.5 text-[11px] leading-relaxed text-white outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
|
class="min-h-28 w-full resize-y rounded-md border border-theme-800 bg-theme-900/60 p-2.5 text-[11px] leading-relaxed text-white outline-none placeholder:text-muted-foreground/60 focus:border-primary/50"
|
||||||
rows="5"
|
rows="5"
|
||||||
@@ -2945,7 +3006,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{#if reportError}
|
{#if reportError}
|
||||||
<p class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300">
|
<p
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300"
|
||||||
|
>
|
||||||
{reportError}
|
{reportError}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -2975,9 +3038,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if historyOpen && historyTarget}
|
{#if historyOpen && historyTarget}
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -2993,7 +3056,10 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
<div class="flex items-start justify-between gap-3 border-b border-theme-800/70 p-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 id="theme-history-title" class="flex items-center gap-2 text-sm font-semibold text-white">
|
<h2
|
||||||
|
id="theme-history-title"
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
<History class="size-4 text-cyan-300" /> Moderation history
|
<History class="size-4 text-cyan-300" /> Moderation history
|
||||||
</h2>
|
</h2>
|
||||||
<p class="mt-1 truncate text-[11px] text-muted-foreground">{historyTarget.name}</p>
|
<p class="mt-1 truncate text-[11px] text-muted-foreground">{historyTarget.name}</p>
|
||||||
@@ -3018,11 +3084,15 @@
|
|||||||
<LoaderCircle class="size-4 animate-spin text-primary" /> Loading history...
|
<LoaderCircle class="size-4 animate-spin text-primary" /> Loading history...
|
||||||
</div>
|
</div>
|
||||||
{:else if historyError}
|
{:else if historyError}
|
||||||
<p class="rounded-lg border border-red-900/40 bg-red-950/20 p-3 text-[11px] text-red-300">
|
<p
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/20 p-3 text-[11px] text-red-300"
|
||||||
|
>
|
||||||
{historyError}
|
{historyError}
|
||||||
</p>
|
</p>
|
||||||
{:else if historyEvents.length === 0}
|
{:else if historyEvents.length === 0}
|
||||||
<p class="rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground">
|
<p
|
||||||
|
class="rounded-lg border border-theme-800/60 bg-theme-900/20 p-3 text-[11px] text-muted-foreground"
|
||||||
|
>
|
||||||
No moderation events for this theme yet.
|
No moderation events for this theme yet.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
@@ -3032,7 +3102,7 @@
|
|||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<p class="truncate text-[11px] text-white/80">
|
<p class="truncate text-[11px] text-white/80">
|
||||||
<span class="font-semibold text-white">{event.actorName}</span>
|
<span class="font-semibold text-white">{event.actorName}</span>
|
||||||
{' · '}
|
·
|
||||||
{moderationActionLabel(event.action)}
|
{moderationActionLabel(event.action)}
|
||||||
</p>
|
</p>
|
||||||
<span class="shrink-0 text-[9px] text-muted-foreground">
|
<span class="shrink-0 text-[9px] text-muted-foreground">
|
||||||
@@ -3062,5 +3132,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Tooltip.Provider>
|
</Tooltip.Provider>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Logo from '$assets/logo.png';
|
import Logo from '$assets/logo.png';
|
||||||
|
import { publishThemeToWorkshop } from '$lib/communityThemes';
|
||||||
import FontPreview from '$lib/components/theme/FontPreview.svelte';
|
import FontPreview from '$lib/components/theme/FontPreview.svelte';
|
||||||
import ThemeBehaviorInspector from '$lib/components/theme/ThemeBehaviorInspector.svelte';
|
import ThemeBehaviorInspector from '$lib/components/theme/ThemeBehaviorInspector.svelte';
|
||||||
import ThemeElementInspector from '$lib/components/theme/ThemeElementInspector.svelte';
|
import ThemeElementInspector from '$lib/components/theme/ThemeElementInspector.svelte';
|
||||||
@@ -102,7 +103,6 @@
|
|||||||
toSafeName,
|
toSafeName,
|
||||||
updateInstalledThemeWorkshopId,
|
updateInstalledThemeWorkshopId,
|
||||||
} from '$lib/themes';
|
} from '$lib/themes';
|
||||||
import { publishThemeToWorkshop } from '$lib/communityThemes';
|
|
||||||
import type { EZPPUser, EZPPUserInfo } from '$lib/types';
|
import type { EZPPUser, EZPPUserInfo } from '$lib/types';
|
||||||
import { currentUser } from '$lib/userAuthentication';
|
import { currentUser } from '$lib/userAuthentication';
|
||||||
import { osuInstallationPath } from '$lib/userSettings';
|
import { osuInstallationPath } from '$lib/userSettings';
|
||||||
@@ -5091,8 +5091,8 @@
|
|||||||
Allow forking
|
Allow forking
|
||||||
</Label>
|
</Label>
|
||||||
<p class="text-[10px] leading-relaxed text-muted-foreground">
|
<p class="text-[10px] leading-relaxed text-muted-foreground">
|
||||||
Let other creators fork this theme from the workshop. Takes effect when you publish
|
Let other creators fork this theme from the workshop. Takes effect when you
|
||||||
or update it; existing forks are not affected.
|
publish or update it; existing forks are not affected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -6900,7 +6900,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $currentUser}
|
{#if $currentUser}
|
||||||
<div class="mt-4 flex items-center justify-between rounded-xl border border-theme-800/70 bg-theme-900/40 px-3.5 py-2.5">
|
<div
|
||||||
|
class="mt-4 flex items-center justify-between rounded-xl border border-theme-800/70 bg-theme-900/40 px-3.5 py-2.5"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<img
|
<img
|
||||||
src={`https://a.ez-pp.farm/${$currentUser.id}`}
|
src={`https://a.ez-pp.farm/${$currentUser.id}`}
|
||||||
@@ -6914,7 +6916,9 @@
|
|||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<span class="text-xs font-semibold text-white">{$currentUser.name}</span>
|
<span class="text-xs font-semibold text-white">{$currentUser.name}</span>
|
||||||
{#if $currentUser.donor}
|
{#if $currentUser.donor}
|
||||||
<span class="rounded bg-amber-500/20 px-1 py-0.2 text-[8px] font-bold text-amber-300 border border-amber-500/30">
|
<span
|
||||||
|
class="rounded bg-amber-500/20 px-1 py-0.2 text-[8px] font-bold text-amber-300 border border-amber-500/30"
|
||||||
|
>
|
||||||
Supporter
|
Supporter
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -6953,7 +6957,9 @@
|
|||||||
{#if publishThemeId}
|
{#if publishThemeId}
|
||||||
<p class="mt-1 truncate font-mono text-[11px] text-theme-100">{publishThemeId}</p>
|
<p class="mt-1 truncate font-mono text-[11px] text-theme-100">{publishThemeId}</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="mt-1 text-[11px] text-muted-foreground">Assigned by EZPPFarm on publish</p>
|
<p class="mt-1 text-[11px] text-muted-foreground">
|
||||||
|
Assigned by EZPPFarm on publish
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -6962,7 +6968,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{#if (definition.tags ?? []).length > 0}
|
{#if (definition.tags ?? []).length > 0}
|
||||||
<div class="mt-1 flex flex-wrap gap-1">
|
<div class="mt-1 flex flex-wrap gap-1">
|
||||||
{#each definition.tags ?? [] as tag}
|
{#each definition.tags ?? [] as tag (tag)}
|
||||||
<span class="rounded bg-theme-800 px-1.5 py-0.5 text-[10px] text-theme-100">
|
<span class="rounded bg-theme-800 px-1.5 py-0.5 text-[10px] text-theme-100">
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Flag, LoaderCircle, X } from '@lucide/svelte';
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import * as Dialog from '$lib/components/ui/dialog';
|
|
||||||
import { Label } from '$lib/components/ui/label';
|
|
||||||
import * as Select from '$lib/components/ui/select';
|
|
||||||
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
|
|
||||||
import {
|
import {
|
||||||
reportCommunityTheme,
|
reportCommunityTheme,
|
||||||
type CommunityTheme,
|
type CommunityTheme,
|
||||||
type ThemeReportCategory,
|
type ThemeReportCategory,
|
||||||
} from '$lib/communityThemes';
|
} from '$lib/communityThemes';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import * as Select from '$lib/components/ui/select';
|
||||||
|
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
|
||||||
|
import { Flag, LoaderCircle, X } from '@lucide/svelte';
|
||||||
import { sileo } from 'sileo';
|
import { sileo } from 'sileo';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
<Flag class="size-4 text-amber-300" /> Report theme
|
<Flag class="size-4 text-amber-300" /> Report theme
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<Dialog.Description class="mt-1 text-[11px] text-muted-foreground">
|
<Dialog.Description class="mt-1 text-[11px] text-muted-foreground">
|
||||||
Reporting <span class="text-white/80">{theme.name}</span> by {theme.author.name}. Reports
|
Reporting <span class="text-white/80">{theme.name}</span> by {theme.author.name}.
|
||||||
are visible to moderators only.
|
Reports are visible to moderators only.
|
||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -97,7 +97,10 @@
|
|||||||
|
|
||||||
<div class="space-y-3 p-4">
|
<div class="space-y-3 p-4">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="installed-report-category" class="text-[10px] uppercase tracking-wide text-white/70">
|
<Label
|
||||||
|
for="installed-report-category"
|
||||||
|
class="text-[10px] uppercase tracking-wide text-white/70"
|
||||||
|
>
|
||||||
Category
|
Category
|
||||||
</Label>
|
</Label>
|
||||||
<Select.Root
|
<Select.Root
|
||||||
@@ -122,7 +125,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<Label for="installed-report-details" class="text-[10px] uppercase tracking-wide text-white/70">
|
<Label
|
||||||
|
for="installed-report-details"
|
||||||
|
class="text-[10px] uppercase tracking-wide text-white/70"
|
||||||
|
>
|
||||||
Details
|
Details
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -141,7 +147,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300">
|
<p
|
||||||
|
class="rounded-lg border border-red-900/40 bg-red-950/20 p-2.5 text-[11px] text-red-300"
|
||||||
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from 'bits-ui';
|
||||||
|
|
||||||
let {
|
let { open = $bindable(false), ...restProps }: WithoutChildrenOrChild<DialogPrimitive.RootProps> =
|
||||||
open = $bindable(false),
|
$props();
|
||||||
...restProps
|
|
||||||
}: WithoutChildrenOrChild<DialogPrimitive.RootProps> = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DialogPrimitive.Root bind:open {...restProps} />
|
<DialogPrimitive.Root bind:open {...restProps} />
|
||||||
@@ -13,10 +13,10 @@
|
|||||||
* theme videos and videos picked in the editor (served as /preview/ URLs) are
|
* theme videos and videos picked in the editor (served as /preview/ URLs) are
|
||||||
* transcoded to a cached H.264 copy by the Rust backend before playback.
|
* transcoded to a cached H.264 copy by the Rust backend before playback.
|
||||||
*/
|
*/
|
||||||
|
import { custom_theme_volume } from './global';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { get, writable } from 'svelte/store';
|
import { get, writable } from 'svelte/store';
|
||||||
import { custom_theme_volume } from './global';
|
|
||||||
|
|
||||||
type MediaCleanup = () => void;
|
type MediaCleanup = () => void;
|
||||||
|
|
||||||
|
|||||||
+7
-20
@@ -506,7 +506,9 @@ export const reloadThemes = async () => {
|
|||||||
const ACTIVE_STATUSES = new Set(['downloading', 'extracting', 'deleting']);
|
const ACTIVE_STATUSES = new Set(['downloading', 'extracting', 'deleting']);
|
||||||
|
|
||||||
const updatedThemes = storeThemes
|
const updatedThemes = storeThemes
|
||||||
.filter((storeTheme) => installedMap.has(storeTheme.name) || ACTIVE_STATUSES.has(storeTheme.status))
|
.filter(
|
||||||
|
(storeTheme) => installedMap.has(storeTheme.name) || ACTIVE_STATUSES.has(storeTheme.status)
|
||||||
|
)
|
||||||
.map((storeTheme) => {
|
.map((storeTheme) => {
|
||||||
if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme;
|
if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme;
|
||||||
const theme = installedMap.get(storeTheme.name);
|
const theme = installedMap.get(storeTheme.name);
|
||||||
@@ -564,10 +566,7 @@ export const saveThemeProject = async (
|
|||||||
return themeFolder;
|
return themeFolder;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateInstalledThemeManifest = async (
|
const updateInstalledThemeManifest = async (folderName: string, patch: Record<string, unknown>) => {
|
||||||
folderName: string,
|
|
||||||
patch: Record<string, unknown>
|
|
||||||
) => {
|
|
||||||
const themeFolder = await path.join(await themesDirectory(), folderName);
|
const themeFolder = await path.join(await themesDirectory(), folderName);
|
||||||
const configPath = await path.join(themeFolder, 'theme.json');
|
const configPath = await path.join(themeFolder, 'theme.json');
|
||||||
if (!(await fs.exists(configPath))) {
|
if (!(await fs.exists(configPath))) {
|
||||||
@@ -585,10 +584,7 @@ const updateInstalledThemeManifest = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updated = { ...(parsed as Record<string, unknown>), ...patch };
|
const updated = { ...(parsed as Record<string, unknown>), ...patch };
|
||||||
await fs.writeFile(
|
await fs.writeFile(configPath, new TextEncoder().encode(`${JSON.stringify(updated, null, 2)}\n`));
|
||||||
configPath,
|
|
||||||
new TextEncoder().encode(`${JSON.stringify(updated, null, 2)}\n`)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Write workshop tags into an installed theme's manifest without dropping other fields. */
|
/** Write workshop tags into an installed theme's manifest without dropping other fields. */
|
||||||
@@ -596,10 +592,8 @@ export const updateInstalledThemeTags = async (folderName: string, tags: string[
|
|||||||
updateInstalledThemeManifest(folderName, { tags });
|
updateInstalledThemeManifest(folderName, { tags });
|
||||||
|
|
||||||
/** Remember the server-assigned workshop ID so later publishes update the same theme. */
|
/** Remember the server-assigned workshop ID so later publishes update the same theme. */
|
||||||
export const updateInstalledThemeWorkshopId = async (
|
export const updateInstalledThemeWorkshopId = async (folderName: string, workshopId: string) =>
|
||||||
folderName: string,
|
updateInstalledThemeManifest(folderName, { workshopId });
|
||||||
workshopId: string
|
|
||||||
) => updateInstalledThemeManifest(folderName, { workshopId });
|
|
||||||
|
|
||||||
export const exportThemeFolder = async (folderName: string, destinationPath: string) => {
|
export const exportThemeFolder = async (folderName: string, destinationPath: string) => {
|
||||||
const themeFolder = await path.join(await themesDirectory(), folderName);
|
const themeFolder = await path.join(await themesDirectory(), folderName);
|
||||||
@@ -632,10 +626,3 @@ export const toSafeName = (name: string) => {
|
|||||||
.replace(/[. ]+$/g, '_')
|
.replace(/[. ]+$/g, '_')
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateRandomString = (length: number) => {
|
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
||||||
return Array.from({ length }, () =>
|
|
||||||
characters.charAt(Math.floor(Math.random() * characters.length))
|
|
||||||
).join('');
|
|
||||||
};
|
|
||||||
+1
-3
@@ -266,9 +266,7 @@ export function trackMediaContainer(node: HTMLElement) {
|
|||||||
const media = new Set<HTMLMediaElement>();
|
const media = new Set<HTMLMediaElement>();
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
custom_theme_audio_playing.set(
|
custom_theme_audio_playing.set(Array.from(media).some((el) => !el.paused));
|
||||||
Array.from(media).some((el) => !el.paused)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function register(el: HTMLMediaElement) {
|
function register(el: HTMLMediaElement) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import LogViewer from '$lib/components/logs/LogViewer.svelte';
|
import LogViewer from '$lib/components/logs/LogViewer.svelte';
|
||||||
import NotificationHub from '$lib/components/notifications/NotificationHub.svelte';
|
import NotificationHub from '$lib/components/notifications/NotificationHub.svelte';
|
||||||
import DiscordPresenceEditor from '$lib/components/presence/DiscordPresenceEditor.svelte';
|
import DiscordPresenceEditor from '$lib/components/presence/DiscordPresenceEditor.svelte';
|
||||||
|
import CommunityWorkshopView from '$lib/components/theme/CommunityWorkshopView.svelte';
|
||||||
import ThemeIcon from '$lib/components/theme/ThemeIcon.svelte';
|
import ThemeIcon from '$lib/components/theme/ThemeIcon.svelte';
|
||||||
import ThemeReportDialog from '$lib/components/theme/ThemeReportDialog.svelte';
|
import ThemeReportDialog from '$lib/components/theme/ThemeReportDialog.svelte';
|
||||||
import ThemeV2Elements from '$lib/components/theme/ThemeV2Elements.svelte';
|
import ThemeV2Elements from '$lib/components/theme/ThemeV2Elements.svelte';
|
||||||
@@ -26,7 +27,6 @@
|
|||||||
import Progress from '$lib/components/ui/progress/progress.svelte';
|
import Progress from '$lib/components/ui/progress/progress.svelte';
|
||||||
import ScrollContainer from '$lib/components/ui/scroll-container/ScrollContainer.svelte';
|
import ScrollContainer from '$lib/components/ui/scroll-container/ScrollContainer.svelte';
|
||||||
import * as Select from '$lib/components/ui/select';
|
import * as Select from '$lib/components/ui/select';
|
||||||
import CommunityWorkshopView from '$lib/components/theme/CommunityWorkshopView.svelte';
|
|
||||||
import { config } from '$lib/config';
|
import { config } from '$lib/config';
|
||||||
import { currentUserInfo } from '$lib/data';
|
import { currentUserInfo } from '$lib/data';
|
||||||
import {
|
import {
|
||||||
@@ -180,6 +180,7 @@
|
|||||||
import { animate } from 'animejs';
|
import { animate } from 'animejs';
|
||||||
import { sileo } from 'sileo';
|
import { sileo } from 'sileo';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import { SvelteSet } from 'svelte/reactivity';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { fade, fly, scale } from 'svelte/transition';
|
import { fade, fly, scale } from 'svelte/transition';
|
||||||
import { TextMorph } from 'torph/svelte';
|
import { TextMorph } from 'torph/svelte';
|
||||||
@@ -355,7 +356,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const currentUserAliases = $derived.by(() => {
|
const currentUserAliases = $derived.by(() => {
|
||||||
const aliases = new Set<string>();
|
const aliases = new SvelteSet<string>();
|
||||||
const add = (value: string | undefined) => {
|
const add = (value: string | undefined) => {
|
||||||
const normalized = value?.trim().toLowerCase();
|
const normalized = value?.trim().toLowerCase();
|
||||||
if (normalized) aliases.add(normalized);
|
if (normalized) aliases.add(normalized);
|
||||||
@@ -371,8 +372,8 @@
|
|||||||
const loadOwnedWorkshopThemes = async (userId: number) => {
|
const loadOwnedWorkshopThemes = async (userId: number) => {
|
||||||
const requestId = ++ownedThemesRequestId;
|
const requestId = ++ownedThemesRequestId;
|
||||||
ownedWorkshopLoaded = false;
|
ownedWorkshopLoaded = false;
|
||||||
const ids = new Set<string>();
|
const ids = new SvelteSet<string>();
|
||||||
const names = new Set<string>();
|
const names = new SvelteSet<string>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let page = 1;
|
let page = 1;
|
||||||
@@ -3765,10 +3766,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tab Switcher: Installed vs Community Workshop -->
|
<!-- Tab Switcher: Installed vs Community Workshop -->
|
||||||
<div class="flex items-center rounded-lg border border-theme-800/60 bg-theme-900/50 p-0.5">
|
<div
|
||||||
|
class="flex items-center rounded-lg border border-theme-800/60 bg-theme-900/50 p-0.5"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors {activeThemesTab === 'library'
|
class="flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors {activeThemesTab ===
|
||||||
|
'library'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-white'}"
|
: 'text-muted-foreground hover:text-white'}"
|
||||||
onclick={() => (activeThemesTab = 'library')}
|
onclick={() => (activeThemesTab = 'library')}
|
||||||
@@ -3781,7 +3785,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors {activeThemesTab === 'workshop'
|
class="flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors {activeThemesTab ===
|
||||||
|
'workshop'
|
||||||
? 'bg-primary text-primary-foreground shadow-sm'
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
: 'text-muted-foreground hover:text-white'}"
|
: 'text-muted-foreground hover:text-white'}"
|
||||||
onclick={() => (activeThemesTab = 'workshop')}
|
onclick={() => (activeThemesTab = 'workshop')}
|
||||||
@@ -3895,7 +3900,8 @@
|
|||||||
{#key themeFilter}
|
{#key themeFilter}
|
||||||
<div class="grid grid-cols-3 gap-3 pb-4 pr-1">
|
<div class="grid grid-cols-3 gap-3 pb-4 pr-1">
|
||||||
{#each filteredThemes as theme, index (theme.folder_name)}
|
{#each filteredThemes as theme, index (theme.folder_name)}
|
||||||
{@const isActive = $active_custom_theme?.folder_name === theme.folder_name}
|
{@const isActive =
|
||||||
|
$active_custom_theme?.folder_name === theme.folder_name}
|
||||||
<div
|
<div
|
||||||
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
|
class="group flex flex-col overflow-hidden rounded-xl border bg-theme-900/30 {isActive
|
||||||
? 'border-primary/60 ring-1 ring-primary/15'
|
? 'border-primary/60 ring-1 ring-primary/15'
|
||||||
@@ -4052,7 +4058,9 @@
|
|||||||
></span>
|
></span>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<span class="relative z-10 flex items-center justify-center gap-2">
|
<span
|
||||||
|
class="relative z-10 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
{#if theme.status === 'extracting'}
|
{#if theme.status === 'extracting'}
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
name="loader"
|
name="loader"
|
||||||
|
|||||||
Reference in new issue
Block a user