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:
parent
8366a7d544
commit
065a703a57
4 changed files with 40 additions and 23 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 : (
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue