Harden ThemeLibrary theme download path handling (#1143)

* Refactor theme handling functions for better clarity

Update ThemeLibrary modal native calls

* Update ThemeLibrary modal native calls

* Simplify Native + add error card

* Update native.ts

---------

Co-authored-by: thororen1234 <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
ArviS 2026-06-03 17:13:38 +03:00 • committed by GitHub
parent 8366a7d544
commit 065a703a57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 40 additions and 23 deletions

View file

@ -44,7 +44,7 @@ import { getThemeInfo, stripBOM, type UserThemeHeader } from "@utils/themes/bd";
import { usercssParse } from "@utils/themes/usercss";
import { getStylusWebStoreUrl } from "@utils/web";
import { findComponentByCodeLazy, findLazy } from "@webpack";
import { Alerts, Menu, openModal,React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common";
import { Alerts, Menu, openModal, React, Select, showToast, TextInput, Toasts, Tooltip, useEffect, useMemo, useState } from "@webpack/common";
import { ContextMenuApi } from "@webpack/common/menu";
import type { ComponentType, Ref, SyntheticEvent } from "react";
import type { UserstyleHeader } from "usercss-meta";
@ -1005,7 +1005,7 @@ function ThemesTab() {
showDeleteButton
onPin={() => togglePinTheme(localTheme.fileName)}
isPinned={settings.pinnedThemes.includes(localTheme.fileName)}
onOpenFolder={!IS_WEB ? () => showItemInFolder(themeDir + "/" + localTheme.fileName) : undefined}
onOpenFolder={!IS_WEB ? () => showItemInFolder(`${themeDir}/${localTheme.fileName}`) : undefined}
onRefresh={refreshLocalThemes}
isLocal
theme={localTheme}

View file

@ -22,9 +22,9 @@ import { logger } from "./ThemeTab";
const Native = VencordNative.pluginHelpers.ThemeLibrary as PluginNative<typeof import("../native")>;
const UserSummaryItem = findComponentByCodeLazy("defaultRenderUser", "showDefaultAvatarsForNullUsers");
async function downloadTheme(themesDir: string, theme: Theme) {
async function downloadTheme(theme: Theme) {
try {
await Native.downloadTheme(themesDir, theme);
await Native.downloadTheme(theme);
showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS);
} catch (err: unknown) {
logger.error(err);
@ -33,7 +33,7 @@ async function downloadTheme(themesDir: string, theme: Theme) {
}
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
const { type, content, likes, guild, tags, last_updated, requiresThemeAttributes } = theme;
const { name, type, content, likes, guild, tags, last_updated, requiresThemeAttributes } = theme;
const themeContent = window.atob(content);
const metadata = themeContent.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
@ -49,21 +49,25 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
<Modal
{...props}
size="md"
title={`${type} Details`}
title={`${name} details`}
actions={[
{
text: "Close",
variant: "dangerPrimary",
variant: "secondary",
onClick: () => props.onClose()
},
{
text: "Download",
variant: "positive",
variant: "primary",
disabled: !theme.content || theme.id === "preview",
onClick: async () => {
const themesDir = await VencordNative.themes.getThemesDir();
const exists = await Native.themeExists(themesDir, theme);
const validThemesDir = await Native.getThemesDir(themesDir, theme);
const validThemesDir = await VencordNative.themes.getThemesDir() + `/${theme?.name}.theme.css`;
if (!validThemesDir) {
showToast(`Failed to download ${theme.name}!`, Toasts.Type.FAILURE);
return;
}
const exists = await Native.themeExists(theme);
if (exists) {
openModal(modalProps => (
<Modal
@ -75,7 +79,7 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
text: "Overwrite",
variant: "dangerPrimary",
onClick: async () => {
await downloadTheme(themesDir, theme);
await downloadTheme(theme);
modalProps.onClose();
}
},
@ -99,7 +103,7 @@ export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, .
</Modal>
));
} else {
await downloadTheme(themesDir, theme);
await downloadTheme(theme);
}
}
}

View file

@ -64,6 +64,7 @@ function ThemeTab() {
const [searchValue, setSearchValue] = useState({ value: "", status: SearchStatus.ALL });
const [hideWarningCard, setHideWarningCard] = useState(Settings.plugins.ThemeLibrary.hideWarningCard);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const onSearch = (query: string) => setSearchValue(prev => ({ ...prev, value: query }));
const onStatusChange = (status: SearchStatus) => setSearchValue(prev => ({ ...prev, status }));
@ -115,6 +116,7 @@ function ThemeTab() {
setFilteredThemes(themes);
} catch (err) {
logger.error(err);
setError(true);
} finally {
setLoading(false);
}
@ -161,6 +163,11 @@ function ThemeTab() {
}}> This won't take long! </p>
</div>
) : error ? (
<ErrorCard>
<HeadingTertiary>Failed to fetch themes</HeadingTertiary>
<Paragraph className={Margins.top8}>Could not fetch the theme list. Try again later.</Paragraph>
</ErrorCard>
) : (
<>
{hideWarningCard ? null : (

View file

@ -4,24 +4,30 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { ensureSafePath } from "@main/ipcMain";
import { THEMES_DIR } from "@main/utils/constants";
import { IpcMainInvokeEvent } from "electron";
import { existsSync, type PathLike, writeFileSync } from "fs";
import { join } from "path";
import { existsSync, writeFileSync } from "fs";
import type { Theme } from "./types";
export async function themeExists(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
return existsSync(join(dir.toString(), `${theme.name}.theme.css`));
function getThemePath(theme: Theme): string | null {
if (!theme?.name) return null;
return ensureSafePath(THEMES_DIR, `${theme.name}.theme.css`);
}
export function getThemesDir(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
return join(dir.toString(), `${theme.name}.theme.css`);
export async function themeExists(_: IpcMainInvokeEvent, theme: Theme) {
const path = getThemePath(theme);
return path ? existsSync(path) : false;
}
export async function downloadTheme(_: IpcMainInvokeEvent, dir: PathLike, theme: Theme) {
if (!theme.content || !theme.name) return;
const path = join(dir.toString(), `${theme.name}.theme.css`);
const download = await fetch(`https://themes.equicord.org/api/download/${theme.id}`);
export async function downloadTheme(_: IpcMainInvokeEvent, theme: Theme) {
if (!theme?.content || !theme?.name || !theme?.id) return;
const path = getThemePath(theme);
if (!path) throw new Error("Invalid theme name");
const download = await fetch(`https://themes.equicord.org/api/download/${encodeURIComponent(theme.id)}`);
const content = await download.text();
writeFileSync(path, content);
}