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
+48
View File
@@ -0,0 +1,48 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { SettingsState } from './types/AppProps';
import useSettingsState from './hooks/settingsState';
// Create a context with an initial state
const SettingsContext = createContext<{
settingsState: SettingsState;
setSettingsState: React.Dispatch<React.SetStateAction<SettingsState>>;
showPicker: boolean;
setShowPicker: React.Dispatch<React.SetStateAction<boolean>>;
standalone: boolean;
setStandalone: React.Dispatch<React.SetStateAction<boolean>>;
} | undefined>(undefined);
export const SettingsContextProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [settingsState, setSettingsState] = useState<SettingsState>({
notificationCollector: false,
lessonAlerts: false,
telemetry: false,
animatedBackground: false,
animatedBackgroundSpeed: "0",
customThemeColor: "rgba(219, 105, 105, 1)",
betterSEQTAPlus: true,
shortcuts: [],
customshortcuts: [],
transparencyEffects: false,
});
const [showPicker, setShowPicker] = useState<boolean>(false);
const [standalone, setStandalone] = useState<boolean>(false);
useSettingsState({ settingsState, setSettingsState });
return (
<SettingsContext.Provider value={{ settingsState, setSettingsState, showPicker, setShowPicker, standalone, setStandalone }}>
{children}
</SettingsContext.Provider>
);
};
// eslint-disable-next-line
export const useSettingsContext = () => {
const context = useContext(SettingsContext);
if (!context) {
throw new Error('useSettingsContext must be used within a SettingsContextProvider');
}
return context;
};
+41
View File
@@ -0,0 +1,41 @@
import TabbedContainer from './components/TabbedContainer';
import Settings from './pages/Settings';
import logo from './assets/betterseqta-dark-full.png';
import logoDark from './assets/betterseqta-light-full.png';
import Shortcuts from './pages/Shortcuts';
import Picker from './components/Picker';
import Themes from './pages/Themes';
interface SettingsPage {
standalone: boolean;
}
const SettingsPage = ({ standalone }: SettingsPage) => {
const tabs = [
{
title: 'Settings',
content: <Settings />
},
{
title: 'Shortcuts',
content: <Shortcuts />
},
{
title: 'Themes',
content: <Themes />
}
];
return (
<div className={`flex flex-col w-[384px] shadow-2xl gap-2 bg-white ${ standalone ? '' : 'rounded-xl' } h-[600px] overflow-clip dark:bg-zinc-800 dark:text-white`}>
<div className="grid border-b border-b-zinc-200/40 place-items-center">
<img src={logo} className="w-4/5 dark:hidden" />
<img src={logoDark} className="hidden w-4/5 dark:block" />
</div>
<Picker />
<TabbedContainer tabs={tabs} />
</div>
);
};
export default SettingsPage;
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,70 @@
const presetBackgrounds = [
// Images
{
id: 'image-preset-1',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-1.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-1-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-2',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-2.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-2-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-3',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-3.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-3-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-4',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-4.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-4-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-5',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-5.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-5-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-6',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-6.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-6-thumb.jpg',
isPreset: true
},
{
id: 'image-preset-7',
type: 'image',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-7.jpg',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/images/background-7-thumb.jpg',
isPreset: true
},
// Videos
{
id: 'video-preset-1',
type: 'video',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/videos/animated-1.mp4',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/videos/animation-1-thumb.mp4',
isPreset: true
},
{
id: 'video-preset-2',
type: 'video',
url: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/videos/animation-2.mp4',
previewUrl: 'https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/backgrounds/videos/animation-2-thumb.mp4',
isPreset: true
}
];
export default presetBackgrounds;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 KiB

+11
View File
@@ -0,0 +1,11 @@
import hacker from './themeCovers/hacker.jpeg'
const themes = [
{
name: "Hacker",
url: "https://raw.githubusercontent.com/SethBurkart123/BetterSEQTA-Themes/main/themes/hacker.json",
coverImage: <img className="object-cover object-center w-full h-full" src={hacker} />,
},
];
export default themes;
@@ -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;
@@ -0,0 +1,73 @@
import { Background } from "../components/BackgroundSelector";
export const downloadPresetBackground = async (background: Background, onProgress: (progress: number) => void): Promise<Background> => {
const response = await fetch(background.url as string);
const totalLength = +response.headers.get('Content-Length')!;
let receivedLength = 0;
const reader = response.body?.getReader();
const chunks = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader!.read();
if (done) break;
chunks.push(value!);
receivedLength += value!.length;
onProgress(Math.ceil(receivedLength / totalLength * 100));
}
const blob = new Blob(chunks);
await writeData(background.id, background.type, blob);
return {
id: background.id,
type: background.type,
blob,
url: URL.createObjectURL(blob),
};
};
// IndexedDB utility functions
export const openDB = () => {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open('MyDatabase', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore('backgrounds', { keyPath: 'id' });
};
});
};
export const writeData = async (fileId: string, type: string, blob: Blob) => {
return new Promise((resolve, reject) => {
openDB().then(async (db) => {
const tx = db.transaction('backgrounds', 'readwrite');
const store = tx.objectStore('backgrounds');
const request = store.put({ id: fileId, type, blob });
await new Promise((res, rej) => {
tx.oncomplete = () => res(request.result);
tx.onerror = () => rej(tx.error);
}).then(resolve, reject);
}).catch(reject);
});
};
export const readAllData = async (): Promise<Background[]> => {
const db = await openDB();
const tx = db.transaction('backgrounds', 'readonly');
const store = tx.objectStore('backgrounds');
const request = store.getAll();
return await new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
};
+63
View File
@@ -0,0 +1,63 @@
import browser from 'webextension-polyfill'
interface ThemeList {
themes: string[];
selectedTheme: string;
}
export const downloadTheme = async (themeName: string, themeURL: string) => {
// send message to the background script
const response = await browser.runtime.sendMessage({
type: 'currentTab',
info: 'DownloadTheme',
body: {
themeName: themeName,
themeURL: themeURL
}
});
console.log("Response: ", response);
}
export const setTheme = async (themeName: string, themeURL: string) => {
// send message to the background script
const response = await browser.runtime.sendMessage({
type: 'currentTab',
info: 'SetTheme',
body: {
themeName: themeName,
themeURL: themeURL
}
});
console.log("Response: ", response);
}
export const listThemes = async () => {
// send message to the background script
const response: ThemeList = await browser.runtime.sendMessage({
type: 'currentTab',
info: 'ListThemes'
});
// response.themes is an array of strings that are identical to the theme names that we loop over. Use this list to see which ones are downloaded and which ones need to see the download icon.
console.log("Response: ", response);
return response;
}
export const disableTheme = async () => {
await browser.runtime.sendMessage({
type: 'currentTab',
info: 'DisableTheme',
});
};
export const deleteTheme = async (themeName: string) => {
await browser.runtime.sendMessage({
type: 'currentTab',
info: 'DeleteTheme',
body: {
themeName: themeName
}
});
}
+97
View File
@@ -0,0 +1,97 @@
import browser from 'webextension-polyfill'
import { useEffect, useMemo } from "react";
import { SettingsProps } from "../types/SettingsProps";
import { MainConfig, SettingsState } from "../types/AppProps";
let RanOnce = false;
let previousSettingsState: SettingsState
const useSettingsState = ({ settingsState, setSettingsState }: SettingsProps) => {
useEffect(() => {
if (RanOnce) return;
RanOnce = true;
// get the current settings state
// @ts-expect-error - TODO: Fix this
browser.storage.local.get().then((result: MainConfig) => {
setSettingsState({
notificationCollector: result.notificationcollector,
lessonAlerts: result.lessonalert,
telemetry: result.telemetry,
animatedBackground: result.animatedbk,
animatedBackgroundSpeed: result.bksliderinput,
customThemeColor: result.selectedColor,
betterSEQTAPlus: result.onoff,
shortcuts: result.shortcuts,
customshortcuts: result.customshortcuts,
transparencyEffects: result.transparencyEffects
});
if (result.DarkMode) {
document.body.classList.add('dark');
}
});
});
const keyToStateMap = useMemo(() => ({
"notificationcollector": "notificationCollector",
"lessonalert": "lessonAlerts",
"telemetry": "telemetry",
"animatedbk": "animatedBackground",
"bksliderinput": "animatedBackgroundSpeed",
"selectedColor": "customThemeColor",
"onoff": "betterSEQTAPlus",
"shortcuts": "shortcuts",
"customshortcuts": "customshortcuts",
"transparencyEffects": "transparencyEffects"
}), []);
const storageChangeListener = (changes: browser.Storage.StorageChange) => {
for (const [key, { newValue }] of Object.entries(changes)) {
if (key === "DarkMode") {
if (key === "DarkMode" && newValue) {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
}
// @ts-expect-error - TODO: Fix this
const stateKey = keyToStateMap[key as keyof MainConfig];
if (stateKey) {
setSettingsState((prevState: SettingsState) => ({
...prevState,
[stateKey]: newValue
}));
}
}
};
useEffect(() => {
browser.storage.onChanged.addListener(storageChangeListener);
return () => {
browser.storage.onChanged.removeListener(storageChangeListener);
};
});
const setStorage = (key: keyof MainConfig, value: any) => {
browser.storage.local.set({ [key]: value });
}
useEffect(() => {
if (previousSettingsState) {
for (const [key, value] of Object.entries(settingsState)) {
// @ts-expect-error - TODO: Fix this
const storageKey = Object.keys(keyToStateMap).find(k => keyToStateMap[k] === key);
// @ts-expect-error - TODO: Fix this
if (storageKey && value !== previousSettingsState[key]) {
setStorage(storageKey as keyof MainConfig, value);
}
}
}
previousSettingsState = settingsState;
}, [settingsState, keyToStateMap])
}
export default useSettingsState;
+17
View File
@@ -0,0 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
::-webkit-scrollbar {
display: none;
}
+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body class="">
<div id="ExtensionPopup"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
import './index.css';
declare module "*.png";
declare module "*.svg";
declare module "*.jpeg";
declare module "*.jpg";
declare module 'react-best-gradient-color-picker';
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HashRouter, Routes, Route } from 'react-router-dom';
import './index.css';
import { SettingsContextProvider } from './SettingsContext.js';
import SettingsPage from './SettingsPage.js';
import browser from 'webextension-polyfill';
import * as Sentry from "@sentry/react";
browser.storage.local.get([ "telemetry" ]).then((telemetry) => {
if (telemetry.telemetry === true)
Sentry.init({
dsn: "https://4bc7197431b170218e15daba4095d08b@o4506347383291904.ingest.sentry.io/4506347394105344",
integrations: [
new Sentry.BrowserTracing({
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/yourserver\.io\/api/],
}),
new Sentry.Replay(),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
})
const fontURL = browser.runtime.getURL("fonts/IconFamily.woff");
const style = document.createElement("style");
style.setAttribute("type", "text/css");
style.innerHTML = `
@font-face {
font-family: 'IconFamily';
src: url('${fontURL}') format('woff');
font-weight: normal;
font-style: normal;
}`;
document.head.appendChild(style);
const root = ReactDOM.createRoot(document.getElementById('ExtensionPopup')!);
root.render(
<React.StrictMode>
<SettingsContextProvider>
<HashRouter>
<Routes>
<Route path="/settings" element={<SettingsPage standalone={true} />} />
<Route path="/settings/embedded" element={<SettingsPage standalone={false} />} />
</Routes>
</HashRouter>
</SettingsContextProvider>
</React.StrictMode>,
);
+20
View File
@@ -0,0 +1,20 @@
const About: React.FC = () => {
return (
<div className="flex flex-col overflow-y-scroll divide-y divide-zinc-100/50 dark:divide-zinc-700/50">
<div>
<h2 className="text-lg font-bold">About</h2>
<p className="py-2">BetterSEQTA+ is a branch of BetterSEQTA which was originally developed by Nulkem. It was discontinued. So BetterSEQTA+ has come in to fill in that gap!</p>
<p className="py-2">We are currently working on fixing bugs and adding new features. If you want to request a feature or report a bug, you can do so on
<a className="pl-1 text-blue-500 underline hover:text-blue-600" href="https://github.com/SethBurkart123/EvenBetterSEQTA" target="_blank">Github</a>.
</p>
</div>
<div>
<h2 className="pt-2 text-lg font-bold">Credits</h2>
<p className="py-2">Nulkem for the original extension, OG-RandomTechChannel, Crazypersonalph, and the current maintainer SethBurkart123</p>
</div>
</div>
);
};
export default About;
+92
View File
@@ -0,0 +1,92 @@
import Switch from '../components/Switch';
import Slider from '../components/Slider';
import PickerSwatch from '../components/PickerSwatch';
import { SettingsList } from '../types/SettingsProps';
import { useSettingsContext } from '../SettingsContext';
import browser from 'webextension-polyfill'
const Settings: React.FC = () => {
const { settingsState, setSettingsState } = useSettingsContext();
const switchChange = (key: string, isOn: boolean) => {
setSettingsState({
...settingsState,
[key]: isOn,
});
};
const sliderChange = (key: string, value: number) => {
setSettingsState({
...settingsState,
[key]: value,
});
};
const settings: SettingsList[] = [
{
title: "Transparency Effects",
description: "Enables transparency effects on certain elements such as blur. (May impact battery life)",
modifyElement: <Switch state={settingsState.transparencyEffects} onChange={(isOn: boolean) => switchChange('transparencyEffects', isOn)} />
},
{
title: "Animated Background",
description: "Adds an animated background to BetterSEQTA. (May impact battery life)",
modifyElement: <Switch state={settingsState.animatedBackground} onChange={(isOn: boolean) => switchChange('animatedBackground', isOn)} />
},
{
title: "Animated Background Speed",
description: "Controls the speed of the animated background.",
modifyElement: <Slider state={parseInt(settingsState.animatedBackgroundSpeed)} onChange={(value: number) => sliderChange('animatedBackgroundSpeed', value)} />
},
{
title: "Custom Theme Colour",
description: "Customise the overall theme colour of SEQTA Learn.",
modifyElement: <PickerSwatch />
},
{
title: "Telemetry",
description: "Enables/disables error collecting.",
modifyElement: <Switch state={settingsState.telemetry} onChange={(isOn: boolean) => switchChange('telemetry', isOn)} />
},
{
title: "Edit Sidebar Layout",
description: "Customise the sidebar layout.",
modifyElement: <button onClick={() => browser.runtime.sendMessage({ type: 'currentTab', info: 'EditSidebar' })} className='px-4 py-1 text-[0.75rem] dark:bg-[#38373D] bg-[#DDDDDD] dark:text-white rounded-md'>Edit</button>
},
{
title: "Notification Collector",
description: "Uncaps the 9+ limit for notifications, showing the real number.",
modifyElement: <Switch state={settingsState.notificationCollector} onChange={(isOn: boolean) => switchChange('notificationCollector', isOn)} />
},
{
title: "Lesson Alerts",
description: "Sends a native browser notification ~5 minutes prior to lessons.",
modifyElement: <Switch state={settingsState.lessonAlerts} onChange={(isOn: boolean) => switchChange('lessonAlerts', isOn)} />
},
{
title: "BetterSEQTA+",
description: "Enables BetterSEQTA+ features",
modifyElement: <Switch state={settingsState.betterSEQTAPlus} onChange={(isOn: boolean) => switchChange('betterSEQTAPlus', isOn)} />
}
];
return (
<div className="flex flex-col -mt-4 overflow-y-scroll divide-y divide-zinc-100 dark:divide-zinc-700">
{settings.map((setting, index) => (
<div className="flex items-center justify-between px-4 py-3" key={index}>
<div className="pr-4">
<h2 className="text-sm font-bold">{setting.title}</h2>
<p className="text-xs">{setting.description}</p>
</div>
<div>
{setting.modifyElement}
</div>
</div>
))}
</div>
);
};
export default Settings;
+155
View File
@@ -0,0 +1,155 @@
import { useState } from "react";
import Switch from "../components/Switch";
import { useSettingsContext } from "../SettingsContext";
import { motion, AnimatePresence } from "framer-motion";
import { CustomShortcut } from "../types/AppProps";
function formatUrl (inputUrl: string) {
// Regular expression to check if the URL starts with http://, https://, or ftp://
const protocolRegex = /^(http:\/\/|https:\/\/|ftp:\/\/)/;
// Check if the URL starts with one of the protocols
if (protocolRegex.test(inputUrl)) {
return inputUrl; // The URL is fine as is
} else {
return `https://${inputUrl}`; // Prepend https:// to the URL
}
}
export default function Shortcuts() {
const { settingsState, setSettingsState } = useSettingsContext();
const switchChange = (shortcutName: string, isOn: boolean): void => {
const updatedShortcuts = settingsState.shortcuts.map((shortcut) => {
if (shortcut.name === shortcutName) {
return { ...shortcut, enabled: isOn };
}
return shortcut;
});
setSettingsState({ ...settingsState, shortcuts: updatedShortcuts });
};
const [newTitle, setNewTitle] = useState<string>("");
const [newURL, setNewURL] = useState<string>("");
const isValidTitle = (title: string): boolean => title.trim() !== "";
const isValidURL = (url: string): boolean => {
const pattern = new RegExp("^(https?:\\/\\/)?[\\w.-]+[\\w.-]+$", "i");
return pattern.test(url);
};
const addNewCustomShortcut = (): void => {
if (isValidTitle(newTitle) && isValidURL(newURL)) {
const newShortcut: CustomShortcut = { name: newTitle.trim(), url: formatUrl(newURL).trim(), icon: newTitle[0] };
const updatedCustomShortcuts = [...settingsState.customshortcuts, newShortcut];
setSettingsState({ ...settingsState, customshortcuts: updatedCustomShortcuts });
setNewTitle("");
setNewURL("");
setFormVisible(false);
} else {
// Replace with a more user-friendly way to display errors
console.error("Please enter a valid title and URL.");
}
};
const deleteCustomShortcut = (index: number): void => {
const updatedCustomShortcuts = settingsState.customshortcuts.filter((_, i) => i !== index);
setSettingsState({ ...settingsState, customshortcuts: updatedCustomShortcuts });
};
const [isFormVisible, setFormVisible] = useState(false);
const toggleForm = () => {
setFormVisible(!isFormVisible);
};
return (
<div className="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
<AnimatePresence>
{isFormVisible ? (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ type: "spring", damping: 20 }}
>
<div className="flex flex-col items-center mb-4">
<motion.input
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="w-full p-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
type="text"
placeholder="Shortcut Name"
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
/>
<motion.input
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="w-full p-2 my-2 rounded-md bg-zinc-100 dark:bg-zinc-700 focus:outline-none"
type="text"
placeholder="URL eg. https://google.com"
value={newURL}
onChange={(e) => setNewURL(e.target.value)}
/>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
className="w-full px-4 py-2 text-white bg-blue-500 rounded-md"
onClick={ addNewCustomShortcut }
>
Add
</motion.button>
</div>
</motion.div>
) : (
<motion.button
initial={{ backgroundColor: "rgba(29, 161, 242, 1)", height: "auto" }}
animate={{ backgroundColor: "rgba(29, 161, 242, 1)", height: "auto" }}
exit={{ backgroundColor: "rgba(29, 161, 242, 1)", height: "auto" }}
transition={{ type: 'tween', ease: "easeOut" }}
className="px-4 py-2 mb-4 text-white bg-blue-500 rounded"
onClick={toggleForm}
>
Add Custom Shortcut
</motion.button>
)}
</AnimatePresence>
{/* Shortcuts Section */}
{settingsState.shortcuts ? (
settingsState.shortcuts.map((shortcut, index) => shortcut.name && (
<div className="flex items-center justify-between px-4 py-3" key={index}>
{shortcut.name}
<Switch state={shortcut.enabled} onChange={(isOn) => switchChange(shortcut.name, isOn)} />
</div>
))
) : (
<p>Loading shortcuts...</p>
)}
{/* Custom Shortcuts Section */}
{settingsState.customshortcuts ? (
settingsState.customshortcuts.map((shortcut, index) => (
<div className="flex items-center justify-between px-4 py-3" key={index}>
{shortcut.name}
<button onClick={() => deleteCustomShortcut(index)}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
))
) : (
<p>Loading custom shortcuts...</p>
)}
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { FC, useEffect, useState } from 'react';
import BackgroundSelector from '../components/BackgroundSelector';
import ThemeSelector from '../components/ThemeSelector';
import { listThemes } from '../hooks/ThemeManagment';
const Themes: FC = () => {
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const [selectedType, setSelectedType] = useState<'background' | 'theme'>('background');
useEffect(() => {
listThemes().then(themes => {
if (themes.selectedTheme) {
setSelectedType('theme');
} else {
setSelectedType('background');
}
});
}, [])
return (
<div>
<button className="absolute top-12 z-20 right-0 p-2 text-[0.8rem] text-blue-500" onClick={() => setIsEditMode(!isEditMode)}>
{isEditMode ? 'Done' : 'Edit'}
</button>
<BackgroundSelector setSelectedType={setSelectedType} selectedType={selectedType} isEditMode={isEditMode} />
<ThemeSelector setSelectedType={setSelectedType} selectedType={selectedType} isEditMode={isEditMode} />
</div>
);
};
export default Themes;
+62
View File
@@ -0,0 +1,62 @@
export interface SettingsState {
notificationCollector: boolean;
lessonAlerts: boolean;
telemetry: boolean;
animatedBackground: boolean;
animatedBackgroundSpeed: string;
customThemeColor: string;
betterSEQTAPlus: boolean;
shortcuts: Shortcut[];
customshortcuts: CustomShortcut[];
transparencyEffects: boolean;
}
interface ToggleItem {
toggle: boolean;
}
interface Shortcut {
enabled: boolean;
name: string;
}
export interface CustomShortcut {
name: string;
url: string;
icon: string;
}
export interface MainConfig {
DarkMode: boolean;
animatedbk: boolean;
bksliderinput: string;
customshortcuts: CustomShortcut[];
defaultmenuorder: any[];
lessonalert: boolean;
menuitems: {
assessments: ToggleItem;
courses: ToggleItem;
dashboard: ToggleItem;
documents: ToggleItem;
forums: ToggleItem;
goals: ToggleItem;
home: ToggleItem;
messages: ToggleItem;
myed: ToggleItem;
news: ToggleItem;
notices: ToggleItem;
portals: ToggleItem;
reports: ToggleItem;
settings: ToggleItem;
timetable: ToggleItem;
welcome: ToggleItem;
};
menuorder: any[];
notificationcollector: boolean;
telemetry: boolean;
onoff: boolean;
selectedColor: string;
shortcuts: Shortcut[];
subjectfilters: Record<string, any>;
transparencyEffects: boolean;
}
+5
View File
@@ -0,0 +1,5 @@
export interface ColorPickerProps {
color: string;
onChange: (color: string) => void;
id: string;
}
+11
View File
@@ -0,0 +1,11 @@
import type { SettingsState } from './AppProps';
export interface SettingsList {
title: string;
description: string;
modifyElement: JSX.Element;
}
export interface SettingsProps {
settingsState: SettingsState;
setSettingsState: React.Dispatch<React.SetStateAction<SettingsState>>;
}
+7
View File
@@ -0,0 +1,7 @@
import React from 'react';
import "./Slider.css";
export interface Slider {
onValueChange: (value: number) => void;
}
declare const Slider: React.FC<Slider>;
export default Slider;
+6
View File
@@ -0,0 +1,6 @@
import "./Switch.css";
export interface SwitchProps {
onChange: (isOn: boolean) => void;
state: boolean;
}
@@ -0,0 +1,10 @@
import React, { JSX } from 'react';
export interface Tab {
title: string;
content: JSX.Element;
}
export interface TabbedContainerProps {
tabs: Tab[];
}
declare const TabbedContainer: React.FC<TabbedContainerProps>;
export default TabbedContainer;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />