mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-06 03:34:40 +00:00
move interface to src/ folder
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(1deg);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shake {
|
||||
animation: shake 0.5s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { downloadPresetBackground, openDB, readAllData, writeData } from "../hooks/BackgroundDataLoader";
|
||||
import presetBackgrounds from "../assets/presetBackgrounds";
|
||||
import "./BackgroundSelector.css";
|
||||
import { disableTheme } from "../hooks/ThemeManagment";
|
||||
|
||||
// Custom Types and Interfaces
|
||||
export interface Background {
|
||||
id: string;
|
||||
type: string;
|
||||
blob: Blob;
|
||||
url?: string;
|
||||
previewUrl?: string;
|
||||
isPreset?: boolean;
|
||||
isDownloaded?: boolean;
|
||||
}
|
||||
|
||||
interface BackgroundSelectorProps {
|
||||
selectedType: "background" | "theme";
|
||||
setSelectedType: (type: "background" | "theme") => void;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
export default function BackgroundSelector({ selectedType, setSelectedType, isEditMode }: BackgroundSelectorProps) {
|
||||
const [backgrounds, setBackgrounds] = useState<Background[]>([]);
|
||||
const [selectedBackground, setSelectedBackground] = useState<string | null>(localStorage.getItem('selectedBackground'));
|
||||
const [downloadedPresetIds, setDownloadedPresetIds] = useState<string[]>([]);
|
||||
const [downloadProgress, setDownloadProgress] = useState<Record<string, number>>({});
|
||||
|
||||
const handleFileChange = async (e: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const fileId = `${Date.now()}-${file.name}`;
|
||||
const fileType = file.type.split('/')[0];
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
|
||||
await writeData(fileId, fileType, blob);
|
||||
setBackgrounds(prev => [...prev, { id: fileId, type: fileType, blob, url: URL.createObjectURL(blob) }]);
|
||||
};
|
||||
|
||||
const loadBackgrounds = async (): Promise<void> => {
|
||||
const data = await readAllData();
|
||||
const dataWithUrls = data.map(bg => ({ ...bg, url: URL.createObjectURL(bg.blob) }));
|
||||
|
||||
// Update downloaded preset IDs
|
||||
setDownloadedPresetIds(data.map(bg => bg.id));
|
||||
|
||||
setBackgrounds(dataWithUrls);
|
||||
};
|
||||
|
||||
const handlePresetClick = async (bg: Background): Promise<void> => {
|
||||
if (bg.isPreset) {
|
||||
// Check if already exists in IndexedDB or is currently being downloaded
|
||||
const existingBackgrounds = await readAllData();
|
||||
const alreadyExists = existingBackgrounds.some(ebg => ebg.id === bg.id) || downloadProgress[bg.id] !== undefined;
|
||||
|
||||
if (!alreadyExists) {
|
||||
setDownloadProgress(prev => ({ ...prev, [bg.id]: 0 }));
|
||||
const downloadedBg = await downloadPresetBackground(bg, progress => {
|
||||
setDownloadProgress(prev => ({ ...prev, [bg.id]: progress }));
|
||||
});
|
||||
setDownloadProgress(prev => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [bg.id]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
await writeData(downloadedBg.id, downloadedBg.type, downloadedBg.blob);
|
||||
setBackgrounds(prev => [...prev, downloadedBg]);
|
||||
setDownloadedPresetIds(prev => [...prev, downloadedBg.id]);
|
||||
}
|
||||
selectBackground(bg.id);
|
||||
}
|
||||
};
|
||||
|
||||
const selectBackground = (fileId: string): void => {
|
||||
disableTheme();
|
||||
setSelectedType('background');
|
||||
setSelectedBackground(fileId);
|
||||
localStorage.setItem('selectedBackground', fileId);
|
||||
};
|
||||
|
||||
const deleteBackground = async (fileId: string): Promise<void> => {
|
||||
const db = await openDB();
|
||||
const tx = db.transaction('backgrounds', 'readwrite');
|
||||
const store = tx.objectStore('backgrounds');
|
||||
store.delete(fileId);
|
||||
setBackgrounds(prev => prev.filter(bg => bg.id !== fileId));
|
||||
|
||||
// Check if the background being deleted is currently selected
|
||||
if (fileId === selectedBackground) {
|
||||
selectNoBackground(); // Disable the current background
|
||||
}
|
||||
};
|
||||
|
||||
const selectNoBackground = (): void => {
|
||||
setSelectedType('background');
|
||||
disableTheme();
|
||||
setSelectedBackground(null);
|
||||
localStorage.removeItem('selectedBackground');
|
||||
};
|
||||
|
||||
const calcCircumference = (radius: number) => 2 * Math.PI * radius;
|
||||
|
||||
useEffect(() => {
|
||||
loadBackgrounds();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button disabled={selectedBackground == null ? true : false} className={`w-full px-4 py-2 mb-4 dark:text-white transition ${selectedBackground == null ? 'dark:bg-zinc-900 bg-zinc-100' : 'bg-blue-500 text-white'} rounded`} onClick={() => selectNoBackground()}>
|
||||
{selectedBackground == null ? 'No Background' : 'Remove Background'}
|
||||
</button>
|
||||
<div className="relative">
|
||||
<h2 className="pb-2 text-lg font-bold">Images</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{/* Image uploader swatch */}
|
||||
<div className="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
|
||||
<div className="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
|
||||
{/* Plus icon */}
|
||||
|
||||
</div>
|
||||
<input type="file" accept='image/*, video/*' onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
{backgrounds.filter(bg => bg.type === 'image').map(bg => (
|
||||
<div key={bg.id}
|
||||
onClick={() => selectBackground(bg.id)}
|
||||
className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id && selectedType === "background" ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
|
||||
{isEditMode && (
|
||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onClick={() => deleteBackground(bg.id)}>
|
||||
<div className="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
)}
|
||||
<img className="object-cover w-full h-full rounded-xl" src={bg.url} alt="swatch" />
|
||||
</div>
|
||||
))}
|
||||
{backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'image' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
|
||||
<button key={bg.id}
|
||||
onClick={() => handlePresetClick(bg)}
|
||||
className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
|
||||
{bg.isPreset && downloadProgress[bg.id] !== undefined && (
|
||||
<div className="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full">
|
||||
<svg className="w-full h-full text-zinc-100 dark:text-zinc-700" viewBox="0 0 36 36">
|
||||
<circle stroke="currentColor" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset="0" transform="rotate(-90 18 18)"></circle>
|
||||
<circle stroke="#3B82F6" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset={`${calcCircumference(14) * (1 - (downloadProgress[bg.id] / 100))}`} transform="rotate(-90 18 18)"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className={`relative transition top-0 z-10 flex justify-center w-full h-full text-white rounded-xl group place-items-center ${downloadProgress[bg.id] === undefined ? 'hover:bg-black/20' : ''}`}>
|
||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
||||
{downloadProgress[bg.id] === undefined ? '' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<img
|
||||
className="absolute top-0 object-cover w-full h-full rounded-xl"
|
||||
src={bg.isPreset ? bg.previewUrl : bg.url} // Use preview for preset backgrounds
|
||||
alt="swatch" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h2 className="py-2 text-lg font-bold">Videos</h2>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{/* Video uploader swatch */}
|
||||
<div className="relative w-16 h-16 overflow-hidden transition rounded-xl bg-zinc-100 dark:bg-zinc-900">
|
||||
<div className="flex items-center justify-center w-full h-full text-3xl font-bold text-gray-400 transition font-IconFamily hover:text-gray-500">
|
||||
{/* Plus icon */}
|
||||
|
||||
</div>
|
||||
<input type="file" accept='image/*, video/*' onChange={handleFileChange} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
|
||||
</div>
|
||||
{backgrounds.filter(bg => bg.type === 'video').map(bg => (
|
||||
<div key={bg.id} onClick={() => selectBackground(bg.id)} className={`relative w-16 h-16 cursor-pointer rounded-xl transition ring dark:ring-white ring-zinc-300 ${isEditMode ? 'animate-shake' : ''} ${selectedBackground === bg.id && selectedType === "background" ? 'dark:ring-2 ring-4' : 'ring-0'}`}>
|
||||
{isEditMode && (
|
||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onClick={() => deleteBackground(bg.id)}>
|
||||
<div className="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
)}
|
||||
<video muted loop autoPlay src={bg.url} className="object-cover w-full h-full rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
{backgrounds.concat(presetBackgrounds as Background[]).filter(bg => bg.type === 'video' && bg.isPreset && !bg.isDownloaded && !downloadedPresetIds.includes(bg.id)).map(bg => (
|
||||
<div key={bg.id}
|
||||
onClick={() => handlePresetClick(bg)}
|
||||
className={`relative w-16 h-16 transition cursor-pointer rounded-xl duration-300 ${ isEditMode ? 'opacity-0 pointer-events-none hidden' : 'opacity-100'}`}>
|
||||
{bg.isPreset && downloadProgress[bg.id] !== undefined && (
|
||||
<div className="absolute top-0 left-0 z-20 flex items-center justify-center w-full h-full">
|
||||
<svg className="w-full h-full text-zinc-100 dark:text-zinc-700" viewBox="0 0 36 36">
|
||||
<circle stroke="currentColor" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset="0" transform="rotate(-90 18 18)"></circle>
|
||||
<circle stroke="#3B82F6" fill="none" strokeWidth="4" strokeLinecap="round" cx="18" cy="18" r="10" strokeDasharray={`${calcCircumference(14)} ${calcCircumference(14)}`} strokeDashoffset={`${calcCircumference(14) * (1 - (downloadProgress[bg.id] / 100))}`} transform="rotate(-90 18 18)"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
<div className={`relative transition top-0 z-10 flex justify-center w-full h-full text-white rounded-xl group place-items-center ${downloadProgress[bg.id] === undefined ? 'hover:bg-black/20' : ''}`}>
|
||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
||||
{downloadProgress[bg.id] === undefined ? '' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<video muted loop autoPlay src={bg.isPreset ? bg.previewUrl : bg.url} className="absolute top-0 object-cover w-full h-full rounded-xl" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.dark #rbgcp-wrapper [style="height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ box-sizing\:\ border-box\;"] {
|
||||
background-color: #37373b !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ box-sizing\:\ border-box\;"] [style="padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ display\:\ flex\;\ align-items\:\ center\;\ justify-content\:\ center\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;"] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ box-sizing\:\ border-box\;"] [style="padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ display\:\ flex\;\ align-items\:\ center\;\ justify-content\:\ center\;\ background\:\ white\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;"] {
|
||||
background-color: #4b4b53 !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ justify-content\:\ space-between\;\ margin-top\:\ 12px\;\ margin-bottom\:\ -4px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ box-sizing\:\ border-box\;\ padding-left\:\ 0px\;"]:has(svg) {
|
||||
background-color: #37373b !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ height\:\ 28px\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ width\:\ 30px\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;\ margin-right\:\ 1px\;"] svg {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ justify-content\:\ space-between\;\ margin-top\:\ 12px\;\ margin-bottom\:\ -4px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ box-sizing\:\ border-box\;\ padding-left\:\ 0px\;"] [style="position\:\ relative\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ align-items\:\ center\;"]:has(svg) {
|
||||
background: transparent !important;
|
||||
filter: invert();
|
||||
color: #39393b !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ justify-content\:\ space-between\;\ margin-top\:\ 12px\;\ margin-bottom\:\ -4px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ box-sizing\:\ border-box\;\ padding-left\:\ 0px\;"] [style^="display\: "]:has(svg) {
|
||||
background-color: #3f3f44 !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ box-sizing\:\ border-box\;"] [style="padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ display\:\ flex\;\ align-items\:\ center\;\ justify-content\:\ center\;\ background\:\ white\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;"]:has(svg) {
|
||||
background-color: #4b4b53 !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ box-sizing\:\ border-box\;"] [style="padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ display\:\ flex\;\ align-items\:\ center\;\ justify-content\:\ center\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;"] svg {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\ 2px\ 2px\ 8px\;"] svg {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\ 2px\ 2px\ 8px\;"] input {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"]:has(svg) {
|
||||
background-color: #37373b !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style^="width\: "] [style^="width\: "] {
|
||||
color: white !important;
|
||||
}
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"] [style="width\:\ 30px\;\ height\:\ 24px\;\ border-radius\:\ 4px\;\ display\:\ flex\;\ align-items\:\ center\;\ justify-content\:\ center\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;"]:has(svg) svg,
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"] [style="width\:\ 30px\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;\ height\:\ 24px\;\ border-radius\:\ 4px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ position\:\ relative\;"]:has(svg) svg,
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"] [style="width\:\ 30px\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;\ height\:\ 24px\;\ border-radius\:\ 4px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;"]:has(svg) svg {
|
||||
filter: invert();
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ justify-content\:\ center\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;"] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ position\:\ relative\;\ padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 86\,\ 86\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ justify-content\:\ center\;\ background\:\ rgba\(255\,\ 255\,\ 255\,\ 0\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\)\ 1px\ 1px\ 3px\;"] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper :last-child > [style$=" relative\;"] [style$=" \31 px\;"] {
|
||||
filter: none !important;
|
||||
background-color: #37373b !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ position\:\ relative\;\ padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ justify-content\:\ center\;\ background\:\ white\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;"],
|
||||
.dark #rbgcp-wrapper [style="display\:\ flex\;\ align-items\:\ center\;\ padding-left\:\ 8px\;\ padding-right\:\ 8px\;\ line-height\:\ 1\;\ border-radius\:\ 4px\;\ font-weight\:\ 700\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ font-size\:\ 12px\;\ height\:\ 24px\;\ transition\:\ all\ 160ms\ ease\ 0s\;\ justify-content\:\ center\;\ background\:\ white\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;"] {
|
||||
background-color: #3f3f44 !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"] [style="width\:\ 30px\;\ background\:\ white\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;\ height\:\ 24px\;\ border-radius\:\ 4px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;\ position\:\ relative\;"]:has(svg) {
|
||||
background-color: #3f3f44 !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="height\:\ 216px\;\ width\:\ 100\%\;\ transition\:\ all\ 120ms\ linear\ 0s\;"] [style="text-align\:\ center\;\ color\:\ rgb\(50\,\ 49\,\ 54\)\;\ font-size\:\ 12px\;\ font-weight\:\ 500\;\ margin-top\:\ 3px\;"],
|
||||
.dark #rbgcp-wrapper [style="height\:\ 216px\;\ width\:\ 100\%\;\ transition\:\ all\ 120ms\ linear\ 0s\;"] [style="text-align\:\ center\;\ color\:\ rgb\(50\,\ 49\,\ 54\)\;\ font-size\:\ 13px\;\ font-weight\:\ 600\;\ position\:\ absolute\;\ top\:\ 6\.5px\;\ left\:\ 2px\;"] {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="height\:\ 98px\;\ width\:\ 100\%\;\ transition\:\ all\ 120ms\ linear\ 0s\;"] [style="cursor\:\ ew-resize\;\ position\:\ relative\;"] [style="text-align\:\ center\;color\:\ rgb\(255\,\ 255\,\ 255\)\;font-size\:\ 12px\;font-weight\:\ 500\;line-height\:\ 1\;position\:\ absolute\;left\:\ 50\%\;transform\:\ translate\(-50\%\,\ 0\%\)\;top\:\ 0px\;z-index\:\ 10\;text-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.6\)\ 1px\ 1px\ 1px\;"] {
|
||||
text-shadow: none !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.dark #rbgcp-wrapper [style="align-items\:\ center\;\ justify-content\:\ flex-end\;\ height\:\ 28px\;\ background\:\ rgb\(233\,\ 233\,\ 245\)\;\ border-radius\:\ 6px\;\ padding\:\ 2px\;\ display\:\ flex\;\ box-sizing\:\ border-box\;"] [style="width\:\ 30px\;\ background\:\ white\;\ color\:\ rgb\(86\,\ 140\,\ 245\)\;\ box-shadow\:\ rgba\(0\,\ 0\,\ 0\,\ 0\.2\)\ 1px\ 1px\ 3px\;\ height\:\ 24px\;\ border-radius\:\ 4px\;\ display\:\ flex\;\ justify-content\:\ center\;\ align-items\:\ center\;"]:has(svg) {
|
||||
background-color: #3f3f44 !important;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import ColorPicker from 'react-best-gradient-color-picker';
|
||||
import { useSettingsContext } from '../SettingsContext';
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import "./Picker.css";
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Picker() {
|
||||
const { settingsState, setSettingsState, showPicker, setShowPicker } = useSettingsContext();
|
||||
|
||||
const defaultPresets = [
|
||||
'linear-gradient(30deg, rgba(229,209,218,1) 0%, RGBA(235,169,202,1) 46%, rgba(214,155,162,1) 100%)',
|
||||
'linear-gradient(40deg, rgba(201,61,0,1) 0%, RGBA(170, 5, 58, 1) 100%)',
|
||||
'linear-gradient(40deg, rgba(0, 141, 201, 0.76) 0%, rgba(8, 5, 170, 0.66) 100%)',
|
||||
'linear-gradient(40deg, rgba(0, 201, 20, 0.76) 0%, rgba(4, 160, 105, 0.66) 100%)',
|
||||
'linear-gradient(40deg, rgba(199, 20, 55, 0.76) 0%, rgba(95, 11, 160, 0.66) 100%)',
|
||||
'linear-gradient(40deg, rgba(24, 20, 199, 0.76) 0%, rgba(23, 173, 65, 0.66) 100%)',
|
||||
'radial-gradient(circle, rgba(20, 199, 178, 0.76) 32%, rgba(3, 120, 57, 0.66) 100%)',
|
||||
'radial-gradient(circle, rgba(13, 15, 145, 0.76) 12%, rgba(103, 3, 120, 0.66) 100%)',
|
||||
'linear-gradient(20deg, rgb(230, 21, 21) 0%, rgb(230, 109, 21) 12%, rgb(230, 34, 21) 26%, rgb(230, 21, 21) 39%, rgb(230, 84, 21) 48%, rgb(230, 34, 21) 58%, rgb(230, 96, 21) 69%, rgb(230, 34, 21) 80%, rgb(230, 71, 21) 89%, rgb(230, 21, 21) 100%)',
|
||||
'rgba(114, 1, 170, 0.89)',
|
||||
'rgba(93, 135, 63, 0.89)',
|
||||
'rgba(4, 4, 138, 0.77)',
|
||||
'rgba(21, 20, 20, 0.89)',
|
||||
'linear-gradient(340deg, rgb(205, 74, 82) 18%, rgba(132, 8, 8, 0.89) 46%, rgb(204, 78, 85) 72%)',
|
||||
'radial-gradient(circle, rgb(74, 205, 158) 0%, rgba(8, 72, 132, 0.89) 99%)',
|
||||
'rgba(17, 94, 89, 1)',
|
||||
'rgba(30, 64, 175, 0.89)',
|
||||
'rgba(134, 25, 143, 1)',
|
||||
'rgba(14, 165, 233, 0.9)'
|
||||
];
|
||||
const [presets, setPresets] = useState(() => {
|
||||
const savedPresets = localStorage.getItem('colorPickerPresets');
|
||||
return savedPresets ? JSON.parse(savedPresets) : defaultPresets;
|
||||
});
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data === "popupClosed") {
|
||||
setShowPicker(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Add event listener for 'message' event
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Watch for changes in showPicker and update the presets
|
||||
if (!showPicker) {
|
||||
// Check if the selected color is already in the presets
|
||||
const existingIndex = presets.indexOf(settingsState.customThemeColor);
|
||||
|
||||
let updatedPresets;
|
||||
if (existingIndex > -1) {
|
||||
// If the color exists, move it to the front
|
||||
updatedPresets = [
|
||||
settingsState.customThemeColor,
|
||||
...presets.slice(0, existingIndex),
|
||||
...presets.slice(existingIndex + 1)
|
||||
];
|
||||
} else {
|
||||
// If the color is new, add it to the front and slice the array
|
||||
updatedPresets = [settingsState.customThemeColor, ...presets].slice(0, 18);
|
||||
}
|
||||
|
||||
setPresets(updatedPresets);
|
||||
localStorage.setItem('colorPickerPresets', JSON.stringify(updatedPresets));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showPicker]);
|
||||
|
||||
const colorChange = (color: string) => {
|
||||
setSettingsState({
|
||||
...settingsState,
|
||||
customThemeColor: color,
|
||||
});
|
||||
};
|
||||
|
||||
// Define animation variants
|
||||
const backgroundVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 }
|
||||
};
|
||||
|
||||
const scaleVariants = {
|
||||
hidden: { scale: 0.3 },
|
||||
visible: { scale: 1 },
|
||||
exit: { scale: 0.4 } // Adding exit animation
|
||||
};
|
||||
|
||||
return (
|
||||
// Apply fade-in animation to background
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={showPicker ? "visible" : "exit"}
|
||||
exit="exit"
|
||||
variants={backgroundVariants}
|
||||
transition={{ duration: 0.2 }}
|
||||
onClick={() => setShowPicker(false)}
|
||||
className={`absolute top-0 left-0 z-50 flex justify-center w-full h-full pt-4 bg-black/20 ${!showPicker ? 'pointer-events-none' : ''}`}
|
||||
>
|
||||
<div>
|
||||
{/* Apply springy scale animation */}
|
||||
<motion.div
|
||||
initial="hidden"
|
||||
animate={showPicker ? "visible" : "exit"}
|
||||
exit="exit"
|
||||
variants={scaleVariants}
|
||||
transition={{ type: "spring", stiffness: 500, damping: 40 }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="h-auto p-4 bg-white border rounded-lg shadow-lg dark:bg-zinc-800 border-zinc-100 dark:border-zinc-700"
|
||||
>
|
||||
<ColorPicker presets={presets} hideInputs={true} value={settingsState.customThemeColor} onChange={colorChange} />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSettingsContext } from '../SettingsContext';
|
||||
|
||||
const PickerSwatch = () => {
|
||||
const { setShowPicker, settingsState } = useSettingsContext();
|
||||
|
||||
const enablePicker = () => {
|
||||
setShowPicker(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={enablePicker}
|
||||
style={{ background: settingsState.customThemeColor }}
|
||||
className="w-16 h-8 rounded-md"
|
||||
></button>
|
||||
);
|
||||
};
|
||||
|
||||
export default PickerSwatch;
|
||||
@@ -0,0 +1,19 @@
|
||||
/* Slider Thumb */
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.3);
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useSettingsContext } from "../SettingsContext";
|
||||
import "./Slider.css";
|
||||
|
||||
interface SliderProps {
|
||||
state: number;
|
||||
onChange: (value: number) => void;
|
||||
}
|
||||
|
||||
const Slider: React.FC<SliderProps> = ({ state, onChange }) => {
|
||||
const { settingsState } = useSettingsContext();
|
||||
|
||||
return (
|
||||
<div className="relative w-full max-w-lg py-8 mx-auto">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={state}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="w-full h-1 rounded-full appearance-none cursor-pointer slider"
|
||||
style={{ background: `${settingsState.customThemeColor}` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Slider;
|
||||
@@ -0,0 +1,4 @@
|
||||
.dark .switch[data-ison="true"],
|
||||
.switch[data-ison="true"] {
|
||||
background-color: #30D259;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { motion } from "framer-motion";
|
||||
import "./Switch.css";
|
||||
import type { SwitchProps } from "../types/SwitchProps";
|
||||
|
||||
export default function Switch(props: SwitchProps) {
|
||||
const toggleSwitch = () => {
|
||||
const newIsOn = !props.state;
|
||||
props.onChange(newIsOn);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex w-14 p-1 cursor-pointer rounded-full dark:bg-[#38373D] bg-[#DDDDDD] switch"
|
||||
data-isOn={props.state}
|
||||
onClick={toggleSwitch}
|
||||
>
|
||||
<motion.div
|
||||
|
||||
className="w-6 h-6 bg-white dark:bg-[#FEFEFE] rounded-full drop-shadow-md"
|
||||
initial={{ x: props.state ? 0 : 0 }}
|
||||
animate={{ x: props.state ? 24 : 0 }}
|
||||
transition={spring}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const spring = {
|
||||
type: "spring",
|
||||
stiffness: 700,
|
||||
damping: 30
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { TabbedContainerProps } from '../types/TabbedContainerProps';
|
||||
import { useSettingsContext } from '../SettingsContext';
|
||||
|
||||
const TabbedContainer: React.FC<TabbedContainerProps> = ({ tabs }) => {
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [hoveredTab, setHoveredTab] = useState<number | null>(null);
|
||||
const [tabWidth, setTabWidth] = useState(0);
|
||||
const [position, setPosition] = useState(0);
|
||||
const positionRef = useRef(position);
|
||||
const themeColor = useSettingsContext().settingsState.customThemeColor;
|
||||
|
||||
|
||||
// Function to handle message
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data === "popupClosed") {
|
||||
setActiveTab(0);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Add event listener for 'message' event
|
||||
window.addEventListener("message", handleMessage);
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener("message", handleMessage);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const newPosition = -activeTab * 100;
|
||||
setPosition(newPosition);
|
||||
positionRef.current = newPosition;
|
||||
}, [activeTab]);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const springTransition = { type: 'spring', stiffness: 250, damping: 25 };
|
||||
|
||||
const contentVariants = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
};
|
||||
|
||||
const fastOpacityTransition = { duration: 0.2 };
|
||||
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
// @ts-expect-error for some reason its giving an error in TS but it works...
|
||||
const width = containerRef.current.getBoundingClientRect().width;
|
||||
setTabWidth(width / tabs.length);
|
||||
}
|
||||
}, [tabs.length]);
|
||||
|
||||
const calcXPos = (index: number | null) => {
|
||||
if (index !== null) {
|
||||
return tabWidth * index;
|
||||
}
|
||||
return tabWidth * activeTab;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={containerRef} className="top-0 z-10 text-[0.875rem] pb-0.5 mx-4">
|
||||
<div className="relative flex">
|
||||
<motion.div
|
||||
className="absolute top-0 left-0 z-0 h-full rounded-full opacity-40"
|
||||
style={{ width: `${tabWidth}px`, background: themeColor }}
|
||||
initial={false}
|
||||
animate={{ x: calcXPos(hoveredTab) }}
|
||||
transition={springTransition}
|
||||
/>
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className="relative z-10 flex-1 px-4 py-2"
|
||||
onClick={() => setActiveTab(index)}
|
||||
onMouseEnter={() => setHoveredTab(index)}
|
||||
onMouseLeave={() => setHoveredTab(null)}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full px-4 overflow-y-scroll overflow-x-clip">
|
||||
<motion.div
|
||||
initial={false}
|
||||
animate={{ x: `${position}%` }}
|
||||
transition={springTransition}
|
||||
>
|
||||
<div className="absolute flex w-full" style={{ left: `${-position}%` }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="absolute w-full pb-4"
|
||||
initial="hidden"
|
||||
animate={activeTab === index ? "visible" : "hidden"}
|
||||
transition={fastOpacityTransition}
|
||||
variants={contentVariants}
|
||||
style={{ display: activeTab === index ? 'block' : 'none' }} // Hide inactive tabs using CSS
|
||||
>
|
||||
{tab.content}
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TabbedContainer;
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import themesList from '../assets/themes';
|
||||
import { listThemes, disableTheme, downloadTheme, setTheme, deleteTheme } from "../hooks/ThemeManagment";
|
||||
|
||||
interface Theme {
|
||||
name: string;
|
||||
url: string;
|
||||
isDownloaded: boolean;
|
||||
isLoading: boolean;
|
||||
coverImage: JSX.Element;
|
||||
}
|
||||
|
||||
interface ThemeSelectorProps {
|
||||
selectedType: "background" | "theme";
|
||||
setSelectedType: (type: "background" | "theme") => void;
|
||||
isEditMode: boolean;
|
||||
}
|
||||
|
||||
const ThemeSelector = ({ selectedType, setSelectedType, isEditMode }: ThemeSelectorProps) => {
|
||||
const [themes, setThemes] = useState<Theme[]>([]);
|
||||
const [enabledThemeName, setEnabledThemeName] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const initializeThemes = async () => {
|
||||
const downloaded = (await listThemes());
|
||||
const initializedThemes = themesList.map(theme => ({
|
||||
...theme,
|
||||
isDownloaded: downloaded.themes.includes(theme.name),
|
||||
isLoading: false
|
||||
}));
|
||||
|
||||
if (downloaded.selectedTheme !== '') {
|
||||
setEnabledThemeName(downloaded.selectedTheme);
|
||||
}
|
||||
|
||||
initializedThemes.sort((a, b) => Number(b.isDownloaded) - Number(a.isDownloaded));
|
||||
|
||||
setThemes(initializedThemes);
|
||||
};
|
||||
|
||||
initializeThemes();
|
||||
}, []);
|
||||
|
||||
const handleDeleteTheme = async (themeName: string) => {
|
||||
await deleteTheme(themeName);
|
||||
setThemes(prevThemes => {
|
||||
// Update the theme's isDownloaded property to false
|
||||
const updatedThemes = prevThemes.map(theme => {
|
||||
if (theme.name === themeName) {
|
||||
return { ...theme, isDownloaded: false };
|
||||
}
|
||||
return theme;
|
||||
});
|
||||
|
||||
// Sort themes so non-downloaded ones appear last
|
||||
updatedThemes.sort((a, b) => Number(b.isDownloaded) - Number(a.isDownloaded));
|
||||
|
||||
return updatedThemes;
|
||||
});
|
||||
|
||||
// Reset the enabled theme name if the deleted theme was the currently enabled one
|
||||
if (enabledThemeName === themeName) {
|
||||
setEnabledThemeName('');
|
||||
setSelectedType('background');
|
||||
}
|
||||
};
|
||||
|
||||
const handleThemeAction = async (themeName: string, themeURL: string) => {
|
||||
const startLoading = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isLoading: true } : theme
|
||||
))
|
||||
);
|
||||
|
||||
// Stop loading for the selected theme.
|
||||
const stopLoading = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isLoading: false } : theme
|
||||
))
|
||||
);
|
||||
|
||||
// Update the theme as downloaded.
|
||||
const markAsDownloaded = (name: string) => (
|
||||
setThemes(prevThemes => prevThemes.map(theme =>
|
||||
theme.name === name ? { ...theme, isDownloaded: true } : theme
|
||||
))
|
||||
);
|
||||
|
||||
startLoading(themeName);
|
||||
|
||||
// Early return if theme is not found.
|
||||
const theme = themes.find(t => t.name === themeName);
|
||||
if (!theme) {
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is downloaded and is the currently enabled theme, disable it.
|
||||
if (theme.isDownloaded && themeName === enabledThemeName) {
|
||||
await disableTheme();
|
||||
setEnabledThemeName('');
|
||||
setSelectedType('background');
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is downloaded but not enabled, enable it.
|
||||
if (theme.isDownloaded && themeName !== enabledThemeName) {
|
||||
await setTheme(themeName, themeURL);
|
||||
setEnabledThemeName(themeName);
|
||||
setSelectedType('theme');
|
||||
stopLoading(themeName);
|
||||
return;
|
||||
}
|
||||
|
||||
// If theme is not downloaded, download and enable it.
|
||||
if (!theme.isDownloaded) {
|
||||
await downloadTheme(themeName, themeURL);
|
||||
markAsDownloaded(themeName);
|
||||
setSelectedType('theme');
|
||||
setEnabledThemeName(themeName);
|
||||
}
|
||||
|
||||
stopLoading(themeName);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedType === 'background') {
|
||||
setEnabledThemeName('');
|
||||
}
|
||||
}, [selectedType]);
|
||||
|
||||
return (
|
||||
<div className="my-2">
|
||||
{(isEditMode ? themes.some(theme => theme.isDownloaded) : themes.length > 0) && (
|
||||
<h2 className="pb-2 text-lg font-bold">Themes</h2>)}
|
||||
<div className="flex flex-col gap-4">
|
||||
{themes
|
||||
.filter(theme => !isEditMode || theme.isDownloaded) // Only show downloaded themes in edit mode
|
||||
.map((theme) => (
|
||||
<button
|
||||
key={theme.name}
|
||||
className={`relative w-full h-16 flex justify-center items-center rounded-lg bg-zinc-700 transition ring dark:ring-white ring-zinc-300 ${enabledThemeName == theme.name && selectedType == "theme" ? 'dark:ring-2 ring-4' : 'ring-0'}`}
|
||||
onClick={() => handleThemeAction(theme.name, theme.url)}
|
||||
disabled={theme.isLoading}
|
||||
>
|
||||
{isEditMode && (
|
||||
<div className="absolute top-0 right-0 z-10 flex w-6 h-6 p-2 text-white translate-x-1/2 -translate-y-1/2 bg-red-600 rounded-full place-items-center"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteTheme(theme.name); }}>
|
||||
<div className="w-4 h-0.5 bg-white"></div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`relative transition rounded-lg overflow-hidden top-0 z-10 flex justify-center w-full h-full text-white group place-items-center ${ theme.isDownloaded ? '' : 'hover:bg-black/20'}`}>
|
||||
<span className="absolute z-10 text-3xl transition opacity-0 font-IconFamily group-hover:opacity-100">
|
||||
{ theme.isDownloaded || theme.isLoading ? '' : ''}
|
||||
</span>
|
||||
|
||||
{ theme.isLoading &&
|
||||
<div className="z-10 inline-block w-6 h-6 border-4 border-current rounded-full animate-spin border-t-transparent" role="status">
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div> }
|
||||
|
||||
</div>
|
||||
<div className="absolute inset-0 z-0 overflow-hidden rounded-lg">
|
||||
{theme.coverImage}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeSelector;
|
||||
Reference in New Issue
Block a user