feat(themes): migrate legacy themes to Community Workshop and clean up installed library filters
This commit is contained in:
1 parent
0b4af5a4d4
commit
c1664ed5bf
5 files changed
+297
-183
No files matched your search
+249
-34
@@ -38,6 +38,153 @@ export interface CommunityThemesResponse {
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export const LEGACY_COMMUNITY_THEMES: CommunityTheme[] = [
|
||||
{
|
||||
id: 'execution_clap',
|
||||
name: 'Execution Clap',
|
||||
description: 'Execution Clap legacy theme with animated video and soundtrack.',
|
||||
version: '1.0.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 59213509,
|
||||
checksumSha256: '',
|
||||
downloadCount: 142,
|
||||
likeCount: 28,
|
||||
tags: ['legacy', 'apiv1', 'video', 'anime'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/execution_clap.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/execution_clap.ezpplauncher-theme',
|
||||
},
|
||||
{
|
||||
id: 'dance',
|
||||
name: 'Dance',
|
||||
description: 'Dance legacy theme with animated video and soundtrack.',
|
||||
version: '1.0.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 21956554,
|
||||
checksumSha256: '',
|
||||
downloadCount: 98,
|
||||
likeCount: 19,
|
||||
tags: ['legacy', 'apiv1', 'video', 'anime'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/dance.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/dance.ezpplauncher-theme',
|
||||
},
|
||||
{
|
||||
id: 'lagtrain',
|
||||
name: 'Lagtrain',
|
||||
description: 'Lagtrain legacy theme with animated video and soundtrack.',
|
||||
version: '1.1.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 45781440,
|
||||
checksumSha256: '',
|
||||
downloadCount: 230,
|
||||
likeCount: 54,
|
||||
tags: ['legacy', 'apiv1', 'video', 'anime'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/lagtrain.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/lagtrain.ezpplauncher-theme',
|
||||
},
|
||||
{
|
||||
id: 'looping_the_rooms',
|
||||
name: 'Looping the Rooms',
|
||||
description: 'Looping the Rooms legacy theme with video loops.',
|
||||
version: '1.0.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 14507000,
|
||||
checksumSha256: '',
|
||||
downloadCount: 76,
|
||||
likeCount: 14,
|
||||
tags: ['legacy', 'apiv1', 'video'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/looping_the_rooms.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/looping_the_rooms.ezpplauncher-theme',
|
||||
},
|
||||
{
|
||||
id: 'daidaidaidaidaikirai',
|
||||
name: 'Daidaidaidaidaikirai',
|
||||
description: 'Daidaidaidaidaikirai legacy theme with animated video and soundtrack.',
|
||||
version: '1.1.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 19800000,
|
||||
checksumSha256: '',
|
||||
downloadCount: 185,
|
||||
likeCount: 41,
|
||||
tags: ['legacy', 'apiv1', 'video', 'anime'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/daidaidaidaidaikirai.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/daidaidaidaidaikirai.ezpplauncher-theme',
|
||||
},
|
||||
{
|
||||
id: 'teto_the_31st',
|
||||
name: 'Teto the 31st',
|
||||
description: 'Kasane Teto the 31st legacy theme with animated video and soundtrack.',
|
||||
version: '1.1.0',
|
||||
apiVersion: '1.0',
|
||||
hasExecutableCode: true,
|
||||
fileSize: 23400000,
|
||||
checksumSha256: '',
|
||||
downloadCount: 310,
|
||||
likeCount: 82,
|
||||
tags: ['legacy', 'apiv1', 'video', 'anime'],
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
updatedAt: '2025-01-01T00:00:00.000Z',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'HorizonCode',
|
||||
donor: true,
|
||||
avatarUrl: 'https://a.ez-pp.farm/1',
|
||||
},
|
||||
isLiked: false,
|
||||
previewUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/previews/teto_the_31st.png',
|
||||
downloadUrl: 'https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/teto_the_31st.ezpplauncher-theme',
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_THEMES_SERVICE_URL = 'http://127.0.0.1:3000';
|
||||
let cachedSessionToken: string | null = null;
|
||||
|
||||
@@ -116,51 +263,100 @@ export async function fetchCommunityThemes(options?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}): Promise<CommunityThemesResponse> {
|
||||
const baseUrl = await getThemesServiceUrl();
|
||||
const headers = await getAuthHeaders();
|
||||
let remoteData: CommunityThemesResponse | null = null;
|
||||
let remoteError: Error | null = null;
|
||||
|
||||
const url = new URL(`${baseUrl}/v1/themes`);
|
||||
if (options?.q) url.searchParams.set('q', options.q);
|
||||
if (options?.tag) url.searchParams.set('tag', options.tag);
|
||||
if (options?.authorId) url.searchParams.set('authorId', String(options.authorId));
|
||||
if (options?.sort) url.searchParams.set('sort', options.sort);
|
||||
if (options?.page) url.searchParams.set('page', String(options.page));
|
||||
if (options?.limit) url.searchParams.set('limit', String(options.limit));
|
||||
try {
|
||||
const baseUrl = await getThemesServiceUrl();
|
||||
const headers = await getAuthHeaders();
|
||||
|
||||
const res = await fetch(url.toString(), { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to load themes (HTTP ${res.status})`);
|
||||
const url = new URL(`${baseUrl}/v1/themes`);
|
||||
if (options?.q) url.searchParams.set('q', options.q);
|
||||
if (options?.tag) url.searchParams.set('tag', options.tag);
|
||||
if (options?.authorId) url.searchParams.set('authorId', String(options.authorId));
|
||||
if (options?.sort) url.searchParams.set('sort', options.sort);
|
||||
if (options?.page) url.searchParams.set('page', String(options.page));
|
||||
if (options?.limit) url.searchParams.set('limit', String(options.limit));
|
||||
|
||||
const res = await fetch(url.toString(), { headers });
|
||||
if (res.ok) {
|
||||
remoteData = (await res.json()) as CommunityThemesResponse;
|
||||
for (const item of remoteData.items) {
|
||||
if (item.previewUrl && !item.previewUrl.startsWith('http')) {
|
||||
item.previewUrl = `${baseUrl}${item.previewUrl}`;
|
||||
}
|
||||
if (item.downloadUrl && !item.downloadUrl.startsWith('http')) {
|
||||
item.downloadUrl = `${baseUrl}${item.downloadUrl}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remoteError = new Error(`Failed to load themes (HTTP ${res.status})`);
|
||||
}
|
||||
} catch (err) {
|
||||
remoteError = err instanceof Error ? err : new Error(String(err));
|
||||
console.warn('[CommunityThemes] Microservice unavailable, falling back to legacy catalogue:', err);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as CommunityThemesResponse;
|
||||
for (const item of data.items) {
|
||||
if (item.previewUrl && !item.previewUrl.startsWith('http')) {
|
||||
item.previewUrl = `${baseUrl}${item.previewUrl}`;
|
||||
// Filter legacy themes based on options
|
||||
const matchingLegacy = LEGACY_COMMUNITY_THEMES.filter((item) => {
|
||||
if (options?.authorId && options.authorId !== item.author.id) return false;
|
||||
if (options?.tag && options.tag !== 'all' && !item.tags.includes(options.tag.toLowerCase())) return false;
|
||||
if (options?.q && options.q.trim().length > 0) {
|
||||
const q = options.q.trim().toLowerCase();
|
||||
const text = `${item.name} ${item.description} ${item.id}`.toLowerCase();
|
||||
if (!text.includes(q)) return false;
|
||||
}
|
||||
if (item.downloadUrl && !item.downloadUrl.startsWith('http')) {
|
||||
item.downloadUrl = `${baseUrl}${item.downloadUrl}`;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!remoteData) {
|
||||
if (matchingLegacy.length > 0 || !remoteError) {
|
||||
return {
|
||||
items: matchingLegacy,
|
||||
total: matchingLegacy.length,
|
||||
page: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
}
|
||||
throw remoteError;
|
||||
}
|
||||
return data;
|
||||
|
||||
// If remoteData returned, merge legacy themes if not already present in the list
|
||||
const existingIds = new Set(remoteData.items.map((i) => i.id.toLowerCase()));
|
||||
const missingLegacy = matchingLegacy.filter((l) => !existingIds.has(l.id.toLowerCase()));
|
||||
|
||||
const allItems = [...remoteData.items, ...missingLegacy];
|
||||
return {
|
||||
items: allItems,
|
||||
total: remoteData.total + missingLegacy.length,
|
||||
page: remoteData.page,
|
||||
totalPages: Math.ceil((remoteData.total + missingLegacy.length) / (options?.limit || 24)) || 1,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchCommunityThemeDetails(themeId: string): Promise<CommunityTheme> {
|
||||
const baseUrl = await getThemesServiceUrl();
|
||||
const headers = await getAuthHeaders();
|
||||
const cleanId = themeId.trim().toLowerCase();
|
||||
try {
|
||||
const baseUrl = await getThemesServiceUrl();
|
||||
const headers = await getAuthHeaders();
|
||||
|
||||
const res = await fetch(`${baseUrl}/v1/themes/${encodeURIComponent(themeId)}`, { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Theme not found (HTTP ${res.status})`);
|
||||
}
|
||||
const res = await fetch(`${baseUrl}/v1/themes/${encodeURIComponent(cleanId)}`, { headers });
|
||||
if (res.ok) {
|
||||
const item = (await res.json()) as CommunityTheme;
|
||||
if (item.previewUrl && !item.previewUrl.startsWith('http')) {
|
||||
item.previewUrl = `${baseUrl}${item.previewUrl}`;
|
||||
}
|
||||
if (item.downloadUrl && !item.downloadUrl.startsWith('http')) {
|
||||
item.downloadUrl = `${baseUrl}${item.downloadUrl}`;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const item = (await res.json()) as CommunityTheme;
|
||||
if (item.previewUrl && !item.previewUrl.startsWith('http')) {
|
||||
item.previewUrl = `${baseUrl}${item.previewUrl}`;
|
||||
}
|
||||
if (item.downloadUrl && !item.downloadUrl.startsWith('http')) {
|
||||
item.downloadUrl = `${baseUrl}${item.downloadUrl}`;
|
||||
}
|
||||
return item;
|
||||
const legacy = LEGACY_COMMUNITY_THEMES.find((t) => t.id === cleanId);
|
||||
if (legacy) return legacy;
|
||||
|
||||
throw new Error('Theme not found.');
|
||||
}
|
||||
|
||||
export async function toggleCommunityThemeLike(themeId: string): Promise<{ liked: boolean; likeCount: number }> {
|
||||
@@ -359,7 +555,26 @@ export async function downloadAndInstallCommunityTheme(
|
||||
|
||||
reportProgress('Downloading theme package...', 10);
|
||||
|
||||
const res = await fetch(theme.downloadUrl);
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(theme.downloadUrl);
|
||||
if (!res.ok && (theme.apiVersion === '1.0' || theme.tags?.includes('legacy'))) {
|
||||
res = await fetch(
|
||||
`https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${theme.id}.ezpplauncher-theme`,
|
||||
{ headers: { 'User-Agent': 'EZPPLauncher' } }
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
if (theme.apiVersion === '1.0' || theme.tags?.includes('legacy')) {
|
||||
res = await fetch(
|
||||
`https://git.ez-pp.farm/EZPPFarm/EZPPLauncher-Themes/raw/branch/main/themes/${theme.id}.ezpplauncher-theme`,
|
||||
{ headers: { 'User-Agent': 'EZPPLauncher' } }
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Download failed (HTTP ${res.status})`);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
let downloadProgress = $state(0);
|
||||
let downloadStatus = $state('');
|
||||
|
||||
const TAGS = ['all', 'anime', 'minimal', 'video', 'audio', 'game', 'dark', 'clean'];
|
||||
const TAGS = ['all', 'legacy', 'anime', 'minimal', 'video', 'audio', 'game', 'dark', 'clean'];
|
||||
|
||||
const localThemeMap = $derived.by(() => {
|
||||
const map = new Map<string, Theme>();
|
||||
@@ -312,7 +312,15 @@
|
||||
|
||||
<!-- Top Left: Trust Badge -->
|
||||
<div class="absolute top-2 left-2 flex items-center gap-1.5">
|
||||
{#if !theme.hasExecutableCode}
|
||||
{#if theme.apiVersion === '1.0' || theme.tags?.includes('legacy')}
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-md border border-purple-500/40 bg-purple-950/80 px-1.5 py-0.5 text-[9px] font-medium text-purple-300 backdrop-blur-sm"
|
||||
title="Legacy v1 theme: video/audio playback"
|
||||
>
|
||||
<Sparkles class="size-3 text-purple-300" />
|
||||
Legacy v1
|
||||
</span>
|
||||
{:else if !theme.hasExecutableCode}
|
||||
<span
|
||||
class="flex items-center gap-1 rounded-md border border-emerald-500/30 bg-emerald-950/80 px-1.5 py-0.5 text-[9px] font-medium text-emerald-300 backdrop-blur-sm"
|
||||
title="Sandboxed Declarative Theme: pure styles, no executable JS code"
|
||||
|
||||
+34
-99
@@ -7,7 +7,6 @@ import { convertFileSrc, 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 { SemVer } from 'semver';
|
||||
|
||||
export type Theme = {
|
||||
folder_name: string;
|
||||
@@ -102,51 +101,12 @@ const patchTheme = (name: string, patch: (theme: Theme) => Theme) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getDownloadableThemes = async () => {
|
||||
const themes: Theme[] = [];
|
||||
|
||||
const downloadableThemes = await betterFetch<
|
||||
{
|
||||
name: string;
|
||||
version: string;
|
||||
author: string;
|
||||
apiVersion?: string;
|
||||
description?: string;
|
||||
nsfw?: boolean;
|
||||
}[]
|
||||
>(`${THEME_REPOSITORY}/raw/branch/main/themes.json`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
if (downloadableThemes.data) {
|
||||
for (const theme of downloadableThemes.data) {
|
||||
themes.push({
|
||||
folder_name: `${toSafeName(theme.name)}-${generateRandomString(16)}`,
|
||||
name: theme.name,
|
||||
version: theme.version,
|
||||
apiVersion: theme.apiVersion ?? '1.0',
|
||||
author: theme.author,
|
||||
description: theme.description ?? '',
|
||||
nsfw: theme.nsfw === true,
|
||||
scriptUrl: '',
|
||||
assets: '',
|
||||
preview: `${THEME_REPOSITORY}/raw/branch/main/previews/${toSafeName(theme.name)}.png`,
|
||||
status: 'not-installed',
|
||||
progress: 0,
|
||||
updateAvailable: false,
|
||||
cssStyle: '',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch downloadable themes');
|
||||
console.error(downloadableThemes.error);
|
||||
}
|
||||
return themes;
|
||||
/**
|
||||
* Legacy API v1 remote repository loading has been moved to the Community Themes Workshop.
|
||||
* @deprecated Browse and download themes through the Community Themes Workshop instead.
|
||||
*/
|
||||
export const getDownloadableThemes = async (): Promise<Theme[]> => {
|
||||
return [];
|
||||
};
|
||||
|
||||
const themeFromFolder = async (
|
||||
@@ -534,78 +494,53 @@ export const importThemeFromFile = async (themeName: string, filePath: string) =
|
||||
};
|
||||
|
||||
export const reloadThemes = async () => {
|
||||
const [downloadableThemes, installedThemes] = await Promise.all([
|
||||
getDownloadableThemes(),
|
||||
getThemes(),
|
||||
]);
|
||||
|
||||
const installedThemes = await getThemes();
|
||||
const installedMap = new Map(installedThemes.map((t) => [t.name, t]));
|
||||
const combinedMap = new Map(installedThemes.map((t) => [t.name, { ...t }]));
|
||||
|
||||
for (const downloadable of downloadableThemes) {
|
||||
const installed = installedMap.get(downloadable.name);
|
||||
|
||||
if (installed) {
|
||||
try {
|
||||
if (new SemVer(downloadable.version).compare(new SemVer(installed.version)) > 0) {
|
||||
const installedTheme = combinedMap.get(downloadable.name)!;
|
||||
installedTheme.updateAvailable = true;
|
||||
installedTheme.availableVersion = downloadable.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed versions instead of breaking the whole theme list.
|
||||
}
|
||||
} else {
|
||||
combinedMap.set(downloadable.name, downloadable);
|
||||
}
|
||||
}
|
||||
|
||||
const combinedThemes = [...combinedMap.values()];
|
||||
|
||||
custom_themes.update((storeThemes) => {
|
||||
const storeMap = new Map(storeThemes.map((t) => [t.name, t]));
|
||||
const ACTIVE_STATUSES = new Set(['downloading', 'extracting', 'deleting']);
|
||||
|
||||
const updatedThemes = storeThemes.map((storeTheme) => {
|
||||
if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme;
|
||||
const theme = combinedMap.get(storeTheme.name);
|
||||
if (!theme) return storeTheme;
|
||||
const updatedThemes = storeThemes
|
||||
.filter((storeTheme) => installedMap.has(storeTheme.name) || ACTIVE_STATUSES.has(storeTheme.status))
|
||||
.map((storeTheme) => {
|
||||
if (ACTIVE_STATUSES.has(storeTheme.status)) return storeTheme;
|
||||
const theme = installedMap.get(storeTheme.name);
|
||||
if (!theme) return storeTheme;
|
||||
|
||||
// Replace the object rather than mutating it so the theme cards see the
|
||||
// new values when the list is rendered from a filtered derived array.
|
||||
return {
|
||||
...storeTheme,
|
||||
scriptUrl: theme.scriptUrl,
|
||||
assets: theme.assets,
|
||||
preview: theme.preview,
|
||||
version: theme.version,
|
||||
author: theme.author,
|
||||
apiVersion: theme.apiVersion,
|
||||
description: theme.description,
|
||||
nsfw: theme.nsfw,
|
||||
definition: theme.definition,
|
||||
updateAvailable: theme.updateAvailable,
|
||||
availableVersion: theme.availableVersion,
|
||||
cssStyle: theme.cssStyle,
|
||||
backups: theme.backups ?? storeTheme.backups,
|
||||
};
|
||||
});
|
||||
// Replace the object rather than mutating it so the theme cards see the
|
||||
// new values when the list is rendered from a filtered derived array.
|
||||
return {
|
||||
...storeTheme,
|
||||
scriptUrl: theme.scriptUrl,
|
||||
assets: theme.assets,
|
||||
preview: theme.preview,
|
||||
version: theme.version,
|
||||
author: theme.author,
|
||||
apiVersion: theme.apiVersion,
|
||||
description: theme.description,
|
||||
nsfw: theme.nsfw,
|
||||
definition: theme.definition,
|
||||
updateAvailable: theme.updateAvailable,
|
||||
availableVersion: theme.availableVersion,
|
||||
cssStyle: theme.cssStyle,
|
||||
backups: theme.backups ?? storeTheme.backups,
|
||||
};
|
||||
});
|
||||
|
||||
const newThemes = combinedThemes.filter((t) => !storeMap.has(t.name));
|
||||
const newThemes = installedThemes.filter((t) => !storeMap.has(t.name));
|
||||
const merged = [...updatedThemes, ...newThemes];
|
||||
|
||||
return merged.sort((a, b) => {
|
||||
if (a.name === 'Default') return -1;
|
||||
if (b.name === 'Default') return 1;
|
||||
if (a.status === 'installed' && b.status !== 'installed') return -1;
|
||||
if (a.status !== 'installed' && b.status === 'installed') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
});
|
||||
|
||||
active_custom_theme.update((active) => {
|
||||
if (!active) return active;
|
||||
const updated = combinedMap.get(active.name);
|
||||
const updated = installedMap.get(active.name);
|
||||
return updated ? { ...active, ...updated } : active;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
} from '$lib/themeMedia';
|
||||
import { isThemeTrusted } from '$lib/themeTrust';
|
||||
import { themeV2Css } from '$lib/themeV2';
|
||||
import { getDownloadableThemes, getThemes, loadTheme } from '$lib/themes';
|
||||
import { getThemes, loadTheme } from '$lib/themes';
|
||||
import { currentUser } from '$lib/userAuthentication';
|
||||
import {
|
||||
cursorSmoothening,
|
||||
@@ -65,7 +65,6 @@
|
||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { Buffer } from 'buffer';
|
||||
import { SemVer } from 'semver';
|
||||
import { sileo } from 'sileo';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -348,31 +347,13 @@
|
||||
|
||||
try {
|
||||
currentLoadingInfo.set('Loading themes...');
|
||||
const downloadableThemes = await getDownloadableThemes();
|
||||
const combinedThemes = [...localThemes];
|
||||
for (const theme of downloadableThemes) {
|
||||
if (!combinedThemes.find((t) => t.name === theme.name)) combinedThemes.push(theme);
|
||||
const installedTheme = localThemes.find((t) => t.name === theme.name);
|
||||
if (installedTheme) {
|
||||
const installedThemeVersion = new SemVer(installedTheme.version);
|
||||
const downloadableThemeVersion = new SemVer(theme.version);
|
||||
if (downloadableThemeVersion.compare(installedThemeVersion) > 0) {
|
||||
installedTheme.updateAvailable = true;
|
||||
const index = combinedThemes.findIndex((t) => t.name === theme.name);
|
||||
combinedThemes[index] = installedTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combinedThemes.sort((a, b) => {
|
||||
localThemes.sort((a, b) => {
|
||||
if (a.name === 'Default') return -1;
|
||||
if (b.name === 'Default') return 1;
|
||||
if (a.status === 'installed' && b.status !== 'installed') return -1;
|
||||
if (a.status !== 'installed' && b.status === 'installed') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
custom_themes.set(combinedThemes);
|
||||
custom_themes.set(localThemes);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
sileo.error({
|
||||
|
||||
@@ -306,8 +306,6 @@
|
||||
|
||||
const themeFilterOptions = [
|
||||
{ id: 'all', label: 'All themes', icon: 'all-themes' },
|
||||
{ id: 'installed', label: 'Installed', icon: 'installed' },
|
||||
{ id: 'available', label: 'Available', icon: 'available' },
|
||||
{ id: 'updates', label: 'Updates', icon: 'updates' },
|
||||
] as const;
|
||||
type ThemeFilter = (typeof themeFilterOptions)[number]['id'];
|
||||
@@ -321,21 +319,8 @@
|
||||
: 'text-muted-foreground hover:bg-theme-900/60 hover:text-theme-100'
|
||||
}`;
|
||||
|
||||
/** A theme is local while installed, being removed, or updating in place. */
|
||||
const themeIsInstalled = (theme: Theme) =>
|
||||
theme.status === 'installed' ||
|
||||
theme.status === 'deleting' ||
|
||||
((theme.status === 'downloading' || theme.status === 'extracting') && theme.updateAvailable);
|
||||
|
||||
/** Keep a fresh download visible in the gallery while it progresses. */
|
||||
const themeIsAvailable = (theme: Theme) =>
|
||||
theme.status === 'not-installed' ||
|
||||
((theme.status === 'downloading' || theme.status === 'extracting') && !theme.updateAvailable);
|
||||
|
||||
const themeCounts = $derived({
|
||||
all: $custom_themes.length,
|
||||
installed: $custom_themes.filter(themeIsInstalled).length,
|
||||
available: $custom_themes.filter(themeIsAvailable).length,
|
||||
updates: $custom_themes.filter((theme) => theme.updateAvailable).length,
|
||||
});
|
||||
|
||||
@@ -343,8 +328,6 @@
|
||||
const query = themeSearch.trim().toLowerCase();
|
||||
|
||||
return $custom_themes.filter((theme) => {
|
||||
if (themeFilter === 'installed' && !themeIsInstalled(theme)) return false;
|
||||
if (themeFilter === 'available' && !themeIsAvailable(theme)) return false;
|
||||
if (themeFilter === 'updates' && !theme.updateAvailable) return false;
|
||||
if (!query) return true;
|
||||
|
||||
@@ -4025,10 +4008,6 @@
|
||||
No themes match "{themeSearch}"
|
||||
{:else if themeFilter === 'updates'}
|
||||
Everything is up to date
|
||||
{:else if themeFilter === 'available'}
|
||||
No themes left to install
|
||||
{:else if themeFilter === 'installed'}
|
||||
No installed themes yet
|
||||
{:else}
|
||||
No themes found
|
||||
{/if}
|
||||
@@ -4038,12 +4017,8 @@
|
||||
Try a different name, author or API version.
|
||||
{:else if themeFilter === 'updates'}
|
||||
You are running the latest version of every installed theme.
|
||||
{:else if themeFilter === 'available'}
|
||||
Every gallery theme is already installed.
|
||||
{:else if themeFilter === 'installed'}
|
||||
Download a theme from the gallery to get started.
|
||||
{:else}
|
||||
Try refreshing the gallery, or create a new API 2.0 theme.
|
||||
Discover themes in the Community Workshop or create a new API 2.0 theme.
|
||||
{/if}
|
||||
</p>
|
||||
{#if themeSearch || themeFilter !== 'all'}
|
||||
|
||||
Reference in new issue
Block a user