237 lines
12 KiB
TypeScript
237 lines
12 KiB
TypeScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2024 Vendicated and contributors
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { CodeBlock } from "@components/CodeBlock";
|
|
import { Heading } from "@components/Heading";
|
|
import { Heart } from "@components/Heart";
|
|
import { Paragraph } from "@components/Paragraph";
|
|
import { Theme, ThemeInfoModalProps } from "@equicordplugins/themeLibrary/types";
|
|
import { ClockIcon, WarningIcon } from "@equicordplugins/themeLibrary/utils/Icons";
|
|
import { copyToClipboard } from "@utils/clipboard";
|
|
import { openInviteModal } from "@utils/discord";
|
|
import { Margins } from "@utils/margins";
|
|
import type { PluginNative } from "@utils/types";
|
|
import { findComponentByCodeLazy } from "@webpack";
|
|
import { Button, Modal, openModal, Parser, React, showToast, Toasts } from "@webpack/common";
|
|
|
|
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) {
|
|
try {
|
|
await Native.downloadTheme(themesDir, theme);
|
|
showToast(`Downloaded ${theme.name}!`, Toasts.Type.SUCCESS);
|
|
} catch (err: unknown) {
|
|
logger.error(err);
|
|
showToast(`Failed to download ${theme.name}! (check console)`, Toasts.Type.FAILURE);
|
|
}
|
|
}
|
|
|
|
export const ThemeInfoModal: React.FC<ThemeInfoModalProps> = ({ author, theme, ...props }) => {
|
|
const { type, content, likes, guild, tags, last_updated, requiresThemeAttributes } = theme;
|
|
|
|
const themeContent = window.atob(content);
|
|
const metadata = themeContent.match(/\/\*\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\//g)?.[0] || "";
|
|
const donate = metadata.match(/@donate\s+(.+)/)?.[1] || "";
|
|
const version = metadata.match(/@version\s+(.+)/)?.[1] || "";
|
|
const invite = metadata.match(/@invite\s+(.+)/)?.[1] || "";
|
|
|
|
const authors = Array.isArray(author) ? author : [author];
|
|
|
|
const lastUpdated = Math.floor(new Date(last_updated ?? 0).getTime() / 1000);
|
|
|
|
return (
|
|
<Modal
|
|
{...props}
|
|
size="md"
|
|
title={`${type} Details`}
|
|
actions={[
|
|
{
|
|
text: "Close",
|
|
variant: "dangerPrimary",
|
|
onClick: () => props.onClose()
|
|
},
|
|
{
|
|
text: "Download",
|
|
variant: "positive",
|
|
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);
|
|
if (exists) {
|
|
openModal(modalProps => (
|
|
<Modal
|
|
{...modalProps}
|
|
size="sm"
|
|
title="Conflict!"
|
|
actions={[
|
|
{
|
|
text: "Overwrite",
|
|
variant: "dangerPrimary",
|
|
onClick: async () => {
|
|
await downloadTheme(themesDir, theme);
|
|
modalProps.onClose();
|
|
}
|
|
},
|
|
{
|
|
text: "Keep my file",
|
|
variant: "positive",
|
|
onClick: () => modalProps.onClose()
|
|
}
|
|
]}
|
|
>
|
|
<Paragraph style={{ padding: "8px" }}>
|
|
<div style={{ display: "flex", flexDirection: "column" }}>
|
|
<p>A theme with the same name <b>already exists</b> in your themes directory! Do you want to overwrite it?</p>
|
|
<div className="vce-overwrite-modal">
|
|
<code style={{ wordWrap: "break-word" }}>
|
|
{validThemesDir}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
</Paragraph>
|
|
</Modal>
|
|
));
|
|
} else {
|
|
await downloadTheme(themesDir, theme);
|
|
}
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
<Heading style={{ marginTop: "10px" }}>{authors.length > 1 ? "Authors" : "Author"}</Heading>
|
|
<div style={{ display: "flex", alignItems: "center", marginBottom: "10px" }}>
|
|
<div>
|
|
<div style={{ display: "flex", alignItems: "center" }}>
|
|
<UserSummaryItem
|
|
users={authors}
|
|
count={authors.length}
|
|
guildId={undefined}
|
|
renderIcon={false}
|
|
max={4}
|
|
size={32}
|
|
showDefaultAvatarsForNullUsers
|
|
showUserPopout
|
|
className={Margins.right8}
|
|
/>
|
|
<Paragraph style={{ maxWidth: "200px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
{authors.map(author => author.username).join(", ")}
|
|
</Paragraph>
|
|
</div>
|
|
{version && (
|
|
<>
|
|
<Heading style={{ marginTop: "10px" }}>Version</Heading>
|
|
<Paragraph>
|
|
{version}
|
|
</Paragraph>
|
|
</>
|
|
)}
|
|
<Heading style={{ marginTop: "10px" }}>Likes</Heading>
|
|
<Paragraph>
|
|
{likes === 0 ? `Nobody liked this ${type} yet.` : `${likes} users liked this ${type}!`}
|
|
</Paragraph>
|
|
{donate && (
|
|
<>
|
|
<Heading style={{ marginTop: "10px" }}>Donate</Heading>
|
|
<Paragraph>
|
|
You can support the author by donating below!
|
|
</Paragraph>
|
|
<Paragraph style={{ marginTop: "10px" }}>
|
|
<Button onClick={() => VencordNative.native.openExternal(donate)}>
|
|
<Heart />
|
|
Donate
|
|
</Button>
|
|
</Paragraph>
|
|
</>
|
|
)}
|
|
{(guild || invite) && (
|
|
<>
|
|
<Heading style={{ marginTop: "10px" }}>Support Server</Heading>
|
|
{guild && (
|
|
<Paragraph>
|
|
{guild.name}
|
|
</Paragraph>
|
|
)}
|
|
<Paragraph>
|
|
<Button
|
|
color={Button.Colors.BRAND}
|
|
look={Button.Looks.FILLED}
|
|
className={Margins.top8}
|
|
onClick={async e => {
|
|
e.preventDefault();
|
|
const useInvite = guild ? guild.invite_link?.split("discord.gg/")[1] : invite;
|
|
useInvite != null && openInviteModal(useInvite).catch(() => showToast("Invalid or expired invite!", Toasts.Type.FAILURE));
|
|
}}
|
|
>
|
|
Join Discord Server
|
|
</Button>
|
|
</Paragraph>
|
|
</>
|
|
)}
|
|
<Heading style={{ marginTop: "10px" }}>Source</Heading>
|
|
<Paragraph>
|
|
<Button
|
|
disabled={!theme.content || theme.id === "preview"}
|
|
onClick={() => openModal(modalProps => (
|
|
<Modal
|
|
{...modalProps}
|
|
size="lg"
|
|
title="Theme Source"
|
|
actions={[
|
|
{
|
|
text: "Close",
|
|
variant: "dangerPrimary",
|
|
onClick: () => modalProps.onClose()
|
|
},
|
|
{
|
|
text: "Copy to Clipboard",
|
|
variant: "primary",
|
|
onClick: () => {
|
|
copyToClipboard(themeContent);
|
|
showToast("Copied to Clipboard", Toasts.Type.SUCCESS);
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
<Paragraph style={{ padding: "8px" }}>
|
|
<CodeBlock lang="css" content={themeContent} />
|
|
</Paragraph>
|
|
</Modal>
|
|
))}
|
|
>
|
|
View Theme Source
|
|
</Button>
|
|
</Paragraph>
|
|
{tags && (
|
|
<>
|
|
<Heading style={{ marginTop: "10px" }}>Tags</Heading>
|
|
<Paragraph>
|
|
{tags.map(tag => (
|
|
<span className="vce-theme-info-tag" key={"vce-theme-info-tag"}>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</Paragraph>
|
|
</>
|
|
)}
|
|
{requiresThemeAttributes && (
|
|
<Paragraph style={{ marginTop: "10px" }}>
|
|
<WarningIcon /> This theme requires the <b>ThemeAttributes</b> plugin!
|
|
</Paragraph>
|
|
)}
|
|
{last_updated && (
|
|
<Paragraph style={{ marginTop: "10px" }}>
|
|
<ClockIcon /> This theme was last updated {Parser.parse("<t:" + lastUpdated + ":F>")} ({Parser.parse("<t:" + lastUpdated + ":R>")})
|
|
</Paragraph>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|