chore: add drag and drop theme import
This commit is contained in:
1 parent
2788cd6e8b
commit
52c60acf37
2 files changed
+299
-35
No files matched your search
+135
-18
@@ -20,6 +20,16 @@ export type Theme = {
|
|||||||
updateAvailable: boolean;
|
updateAvailable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ThemeInfo = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
apiVersion: string;
|
||||||
|
author: string;
|
||||||
|
entry: string;
|
||||||
|
style: string;
|
||||||
|
preview: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const getDownloadableThemes = async () => {
|
export const getDownloadableThemes = async () => {
|
||||||
const themes: Theme[] = [];
|
const themes: Theme[] = [];
|
||||||
|
|
||||||
@@ -82,15 +92,7 @@ export const getThemes = async (): Promise<Theme[]> => {
|
|||||||
const themeConfigFilePath = await path.join(folderPath, themeFolder.name, 'theme.json');
|
const themeConfigFilePath = await path.join(folderPath, themeFolder.name, 'theme.json');
|
||||||
if (await fs.exists(themeConfigFilePath)) {
|
if (await fs.exists(themeConfigFilePath)) {
|
||||||
const themeConfig = await fs.readTextFile(themeConfigFilePath);
|
const themeConfig = await fs.readTextFile(themeConfigFilePath);
|
||||||
const theme = JSON.parse(themeConfig) as {
|
const theme = JSON.parse(themeConfig) as ThemeInfo;
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
apiVersion: string;
|
|
||||||
author: string;
|
|
||||||
entry: string;
|
|
||||||
style: string;
|
|
||||||
preview: string;
|
|
||||||
};
|
|
||||||
const scriptFile = await path.join(folderPath, themeFolder.name, theme.entry);
|
const scriptFile = await path.join(folderPath, themeFolder.name, theme.entry);
|
||||||
if (await fs.exists(scriptFile)) {
|
if (await fs.exists(scriptFile)) {
|
||||||
themes.push({
|
themes.push({
|
||||||
@@ -272,15 +274,7 @@ export const downloadTheme = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const themeInfo = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
|
const themeInfo = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
|
||||||
const themeInfoObj = JSON.parse(themeInfo) as {
|
const themeInfoObj = JSON.parse(themeInfo) as ThemeInfo;
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
apiVersion: string;
|
|
||||||
author: string;
|
|
||||||
entry: string;
|
|
||||||
style: string;
|
|
||||||
preview: string;
|
|
||||||
};
|
|
||||||
const themeScriptUrl = convertFileSrc(
|
const themeScriptUrl = convertFileSrc(
|
||||||
await path.normalize(`${themeFolder}/${themeInfoObj.entry}`)
|
await path.normalize(`${themeFolder}/${themeInfoObj.entry}`)
|
||||||
);
|
);
|
||||||
@@ -390,6 +384,129 @@ export const deleteTheme = async (themeToUninstall: Theme) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkThemeFromFile = async (filePath: string) => {
|
||||||
|
const normalizedFilePath = await path.normalize(filePath);
|
||||||
|
if (!(await fs.exists(normalizedFilePath))) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'File not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const fileBuffer = await fs.readFile(normalizedFilePath);
|
||||||
|
|
||||||
|
let themeInfo: ThemeInfo | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const zipFile = await zip.loadAsync(fileBuffer);
|
||||||
|
|
||||||
|
const themeConfig = zipFile.file('theme.json');
|
||||||
|
if (!themeConfig) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Theme config file not found',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const themeConfigData = await themeConfig.async('string');
|
||||||
|
themeInfo = JSON.parse(themeConfigData) as {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
apiVersion: string;
|
||||||
|
author: string;
|
||||||
|
entry: string;
|
||||||
|
style: string;
|
||||||
|
preview: string;
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Could not read theme file',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: !!themeInfo,
|
||||||
|
themeInfo,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importThemeFromFile = async (themeName: string, filePath: string) => {
|
||||||
|
const baseThemeFolder = await path.join(await path.homeDir(), '.ezpplauncher', 'themes');
|
||||||
|
const themeFolder = await path.join(baseThemeFolder, toSafeName(themeName));
|
||||||
|
if (!(await fs.exists(baseThemeFolder))) await fs.mkdir(baseThemeFolder, { recursive: true });
|
||||||
|
if (!(await fs.exists(themeFolder))) await fs.mkdir(themeFolder, { recursive: true });
|
||||||
|
|
||||||
|
const fileBuffer = await fs.readFile(filePath);
|
||||||
|
const zipFile = await zip.loadAsync(fileBuffer);
|
||||||
|
for (const zipEntry of Object.keys(zipFile.files)) {
|
||||||
|
const file = zipFile.file(zipEntry);
|
||||||
|
if (file) {
|
||||||
|
try {
|
||||||
|
const fileData = await file.async('uint8array');
|
||||||
|
const filePath = await path.join(themeFolder, zipEntry);
|
||||||
|
const dirPath = await path.dirname(filePath);
|
||||||
|
if (!(await fs.exists(dirPath))) await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
await new Promise((res) => setTimeout(res, 250));
|
||||||
|
await fs.writeFile(filePath, fileData);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to extract theme file.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const themeInfo = await fs.readTextFile(await path.join(themeFolder, 'theme.json'));
|
||||||
|
const themeInfoObj = JSON.parse(themeInfo) as ThemeInfo;
|
||||||
|
const themeScriptUrl = convertFileSrc(
|
||||||
|
await path.normalize(`${themeFolder}/${themeInfoObj.entry}`)
|
||||||
|
);
|
||||||
|
const themeAssets = convertFileSrc(await path.normalize(`${themeFolder}/assets`));
|
||||||
|
const themePreview = convertFileSrc(
|
||||||
|
await path.normalize(`${themeFolder}/assets/${themeInfoObj.preview}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const downloadableThemes = await getDownloadableThemes();
|
||||||
|
custom_themes.update((themes) => {
|
||||||
|
const reloadedThemes = [...themes];
|
||||||
|
for (const theme of downloadableThemes) {
|
||||||
|
if (!reloadedThemes.find((t) => t.name === theme.name)) reloadedThemes.push(theme);
|
||||||
|
|
||||||
|
const installedTheme = reloadedThemes.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 = reloadedThemes.findIndex((t) => t.name === theme.name);
|
||||||
|
reloadedThemes[index] = installedTheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const installedThemes of reloadedThemes) {
|
||||||
|
if (installedThemes.name === themeName) {
|
||||||
|
installedThemes.name = themeInfoObj.name;
|
||||||
|
installedThemes.author = themeInfoObj.author;
|
||||||
|
installedThemes.version = themeInfoObj.version;
|
||||||
|
installedThemes.scriptUrl = themeScriptUrl;
|
||||||
|
installedThemes.assets = themeAssets;
|
||||||
|
installedThemes.preview = themePreview;
|
||||||
|
installedThemes.status = 'installed';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reloadedThemes.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const toSafeName = (name: string) => {
|
export const toSafeName = (name: string) => {
|
||||||
return (
|
return (
|
||||||
name
|
name
|
||||||
|
|||||||
+164
-17
@@ -123,7 +123,14 @@
|
|||||||
import { animate } from 'animejs';
|
import { animate } from 'animejs';
|
||||||
import { sileo } from 'sileo';
|
import { sileo } from 'sileo';
|
||||||
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
|
import ScrollContainer from '@/components/ui/scroll-container/ScrollContainer.svelte';
|
||||||
import { deleteTheme, downloadTheme, loadTheme } from '@/themes';
|
import {
|
||||||
|
checkThemeFromFile,
|
||||||
|
deleteTheme,
|
||||||
|
downloadTheme,
|
||||||
|
importThemeFromFile,
|
||||||
|
loadTheme,
|
||||||
|
type ThemeInfo,
|
||||||
|
} from '@/themes';
|
||||||
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 { useDropZone } from '@/dropZone.svelte';
|
import { useDropZone } from '@/dropZone.svelte';
|
||||||
@@ -140,9 +147,44 @@
|
|||||||
|
|
||||||
let dragAndDrop = useDropZone({
|
let dragAndDrop = useDropZone({
|
||||||
onDrop: async (file) => {
|
onDrop: async (file) => {
|
||||||
console.log('dropped', file);
|
const firstFile = file[0];
|
||||||
|
if (!firstFile.endsWith('.ezpplauncher-theme')) {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Uhh...',
|
||||||
|
description: 'Dropped file is not a valid theme file.',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const importResult = await checkThemeFromFile(firstFile);
|
||||||
|
if (!importResult.success || !importResult.themeInfo) {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Hmmm...',
|
||||||
|
description: importResult.error || 'An unknown error occurred.',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
droppedTheme = {
|
||||||
|
filePath: firstFile,
|
||||||
|
themeInfo: importResult.themeInfo,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let droppedTheme = $state<
|
||||||
|
| {
|
||||||
|
filePath: string;
|
||||||
|
themeInfo: ThemeInfo;
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
>(undefined);
|
||||||
|
let themeInstalling = $state(false);
|
||||||
|
|
||||||
let downloadingEZPPFiles = $state(false);
|
let downloadingEZPPFiles = $state(false);
|
||||||
let cleanup = $state(false);
|
let cleanup = $state(false);
|
||||||
@@ -765,21 +807,6 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if dragAndDrop.isDraggingOverApp}
|
|
||||||
<div
|
|
||||||
class="fixed top-0 left-0 w-full h-full z-50 flex items-center justify-center bg-black/10 backdrop-blur-sm pointer-events-none"
|
|
||||||
transition:fade={{ duration: 300 }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="w-[90vw] h-[90vh] flex flex-col items-center justify-center bg-theme-900 border border-theme-800 rounded-lg"
|
|
||||||
transition:scale={{ start: 0.7, duration: 300 }}
|
|
||||||
>
|
|
||||||
<Import size={64} />
|
|
||||||
<p>Drag and Drop a .ezpplauncher-theme file here to import</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<AlertDialog.Root open={launchError !== undefined}>
|
<AlertDialog.Root open={launchError !== undefined}>
|
||||||
<AlertDialog.Content class="bg-theme-950 border-theme-800 p-0 max-w-[90vw]">
|
<AlertDialog.Content class="bg-theme-950 border-theme-800 p-0 max-w-[90vw]">
|
||||||
<div
|
<div
|
||||||
@@ -940,7 +967,127 @@
|
|||||||
</AlertDialog.Content>
|
</AlertDialog.Content>
|
||||||
</AlertDialog.Root>
|
</AlertDialog.Root>
|
||||||
|
|
||||||
|
<AlertDialog.Root open={$newVersion === undefined && !!droppedTheme}>
|
||||||
|
<AlertDialog.Content
|
||||||
|
class="bg-theme-950 border-theme-800 p-0 max-w-2xl"
|
||||||
|
escapeKeydownBehavior="ignore"
|
||||||
|
interactOutsideBehavior="ignore"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center border-b border-theme-800 bg-black/40 rounded-t-lg p-3"
|
||||||
|
>
|
||||||
|
<Import size={40} />
|
||||||
|
<span class="font-semibold text-xl">Import Theme</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center text-sm text-center bg-theme-900 border border-theme-800 rounded-lg mx-3 p-3"
|
||||||
|
>
|
||||||
|
<p class="mb-4">
|
||||||
|
You are about to import the theme <strong>"{droppedTheme?.themeInfo.name}"</strong> by
|
||||||
|
<strong>{droppedTheme?.themeInfo.author}</strong>.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mb-4 text-red-400 bg-red-800/20 border border-red-700/20 rounded-lg p-2 flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<div class="text-base font-semibold">Warning</div>
|
||||||
|
<div>Themes from external sources may contain malicious code or assets.</div>
|
||||||
|
<div>Only import themes from creators you trust.</div>
|
||||||
|
</div>
|
||||||
|
<p>Are you sure you want to proceed with the installation?</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-center mb-3 gap-4 mt-4">
|
||||||
|
<Button
|
||||||
|
class="min-w-28"
|
||||||
|
onclick={async () => {
|
||||||
|
if (!droppedTheme) {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Hmmm...',
|
||||||
|
description: 'Failed to install theme.',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
themeInstalling = true;
|
||||||
|
try {
|
||||||
|
const importResult = await importThemeFromFile(
|
||||||
|
droppedTheme.themeInfo.name,
|
||||||
|
droppedTheme.filePath
|
||||||
|
);
|
||||||
|
if (!importResult.success) {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Hmmm...',
|
||||||
|
description:
|
||||||
|
importResult.error || 'An unknown error occurred while importing your theme.',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sileo.success({
|
||||||
|
title: 'Yay!',
|
||||||
|
description: 'Theme imported successfully',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
sileo.error({
|
||||||
|
title: 'Hmmm...',
|
||||||
|
description: 'An unknown error occurred while importing your theme.',
|
||||||
|
fill: '#181825',
|
||||||
|
styles: {
|
||||||
|
description: 'text-center!',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
droppedTheme = undefined;
|
||||||
|
themeInstalling = false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={themeInstalling}
|
||||||
|
>
|
||||||
|
{#if themeInstalling}
|
||||||
|
<LoaderCircle class="animate-spin" size={18} />
|
||||||
|
{:else}
|
||||||
|
Yes, import
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onclick={async () => {
|
||||||
|
themeInstalling = false;
|
||||||
|
droppedTheme = undefined;
|
||||||
|
}}
|
||||||
|
disabled={themeInstalling}
|
||||||
|
>
|
||||||
|
No, don't import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</AlertDialog.Content>
|
||||||
|
</AlertDialog.Root>
|
||||||
|
|
||||||
<div class="grid grid-cols-[0.085fr_1fr] h-[100vh] relative" bind:this={dragAndDrop.ref}>
|
<div class="grid grid-cols-[0.085fr_1fr] h-[100vh] relative" bind:this={dragAndDrop.ref}>
|
||||||
|
{#if dragAndDrop.isDraggingOverApp}
|
||||||
|
<div
|
||||||
|
class="fixed top-0 left-0 w-full h-full z-50 flex items-center justify-center bg-black/10 backdrop-blur-sm"
|
||||||
|
transition:fade={{ duration: 300 }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-[90vw] h-[90vh] flex flex-col items-center justify-center bg-theme-900 border border-theme-800 rounded-lg"
|
||||||
|
transition:scale={{ start: 0.7, duration: 300 }}
|
||||||
|
>
|
||||||
|
<Import size={64} />
|
||||||
|
<p>Drag and Drop a .ezpplauncher-theme file here to import</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div
|
<div
|
||||||
class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
|
class="p-3 border-r border-r-theme-900 flex flex-col items-center gap-2 z-10 bg-black/40 backdrop-blur-sm"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in new issue
Block a user