move interface to src/ folder

This commit is contained in:
SethBurkart123
2023-12-20 13:08:31 +11:00
parent 378a983bf5
commit ebb8ddbaf0
40 changed files with 3 additions and 4 deletions
@@ -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>
</>
);
}
+95
View File
@@ -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;
}
+126
View File
@@ -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>
);
}
+19
View File
@@ -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;
+19
View File
@@ -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%;
}
+27
View File
@@ -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;
+4
View File
@@ -0,0 +1,4 @@
.dark .switch[data-ison="true"],
.switch[data-ison="true"] {
background-color: #30D259;
}
+32
View File
@@ -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;
+174
View File
@@ -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;