Compare commits
17 Commits
feat/preac
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 85b239f540 | |||
| 7efdd17534 | |||
| b1957f2c19 | |||
| efc9e5c3dd | |||
| 65c6946e7f | |||
| 3129fa6531 | |||
| 82da27cf8d | |||
| c4dce04d42 | |||
| c2b3356022 | |||
| d067e0b95c | |||
| aec7a331c6 | |||
| 0d636ab680 | |||
| 69c6c6fe09 | |||
| fd552c48cd | |||
| 95b7be5219 | |||
| b8e1468a46 | |||
| 199d92f733 |
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
.claude/
|
||||||
@@ -1,14 +1,22 @@
|
|||||||
name: Build and Release
|
name: Build and Release to Staging
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_HOST: git.ivanch.me
|
||||||
|
REGISTRY_USERNAME: ivanch
|
||||||
|
IMAGE_NAME: ${{ env.REGISTRY_HOST }}/ivanch/vision-start
|
||||||
|
IMAGE_TAG: staging
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
name: Build Vision Start
|
||||||
if: gitea.event_name == 'push'
|
if: gitea.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-amd64
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -16,3 +24,47 @@ jobs:
|
|||||||
run: npm install
|
run: npm install
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
build_vision_start:
|
||||||
|
name: Build Vision Start Image
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
|
| docker login "${{ env.REGISTRY_HOST }}" \
|
||||||
|
-u "${{ env.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and Push Multi-Arch Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||||
|
|
||||||
|
deploy_vision_start:
|
||||||
|
name: Deploy Vision Start (staging)
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: build_vision_start
|
||||||
|
steps:
|
||||||
|
- name: Recreate Container
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USERNAME }}
|
||||||
|
key: ${{ secrets.KEY }}
|
||||||
|
port: ${{ secrets.PORT }}
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.STAGING_DIR }}
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d --force-recreate
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- v*
|
- v*
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_HOST: git.ivanch.me
|
||||||
|
REGISTRY_USERNAME: ivanch
|
||||||
|
IMAGE_NAME: ${{ env.REGISTRY_HOST }}/ivanch/vision-start
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -53,8 +59,9 @@ jobs:
|
|||||||
virustotal_apikey: ${{ secrets.VIRUSTOTAL_APIKEY }}
|
virustotal_apikey: ${{ secrets.VIRUSTOTAL_APIKEY }}
|
||||||
VIRUS_TOTAL_FILE: vision-start-${{ gitea.ref_name }}.zip
|
VIRUS_TOTAL_FILE: vision-start-${{ gitea.ref_name }}.zip
|
||||||
run: |
|
run: |
|
||||||
# Run the VirusTotal check script and capture output
|
# Run the VirusTotal check script and capture output in real-time
|
||||||
bash scripts/check_virustotal.sh > vt_output.txt 2>&1
|
set -o pipefail
|
||||||
|
bash scripts/check_virustotal.sh 2>&1 | tee vt_output.txt
|
||||||
|
|
||||||
# Extract analysis URL and detection ratio from output
|
# Extract analysis URL and detection ratio from output
|
||||||
ANALYSIS_URL=$(grep "Analysis URL:" vt_output.txt | cut -d' ' -f3- || echo "Not available")
|
ANALYSIS_URL=$(grep "Analysis URL:" vt_output.txt | cut -d' ' -f3- || echo "Not available")
|
||||||
@@ -64,9 +71,6 @@ jobs:
|
|||||||
echo "analysis-url=$ANALYSIS_URL" >> $GITEA_OUTPUT
|
echo "analysis-url=$ANALYSIS_URL" >> $GITEA_OUTPUT
|
||||||
echo "detection-ratio=$DETECTION_RATIO" >> $GITEA_OUTPUT
|
echo "detection-ratio=$DETECTION_RATIO" >> $GITEA_OUTPUT
|
||||||
|
|
||||||
# Display the full output
|
|
||||||
cat vt_output.txt
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build, virus-total-check]
|
needs: [build, virus-total-check]
|
||||||
@@ -88,3 +92,47 @@ jobs:
|
|||||||
name: ${{ gitea.ref_name }}
|
name: ${{ gitea.ref_name }}
|
||||||
tag_name: ${{ gitea.ref_name }}
|
tag_name: ${{ gitea.ref_name }}
|
||||||
files: vision-start-${{ gitea.ref_name }}.zip
|
files: vision-start-${{ gitea.ref_name }}.zip
|
||||||
|
|
||||||
|
build_vision_start:
|
||||||
|
name: Build Vision Start Image
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: [build, virus-total-check]
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Log in to Container Registry
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_PASSWORD }}" \
|
||||||
|
| docker login "${{ env.REGISTRY_HOST }}" \
|
||||||
|
-u "${{ env.REGISTRY_USERNAME }}" \
|
||||||
|
--password-stdin
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and Push Multi-Arch Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
|
||||||
|
|
||||||
|
deploy_vision_start:
|
||||||
|
name: Deploy Vision Start (production)
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: build_vision_start
|
||||||
|
steps:
|
||||||
|
- name: Recreate Container
|
||||||
|
uses: appleboy/ssh-action@v0.1.7
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USERNAME }}
|
||||||
|
key: ${{ secrets.KEY }}
|
||||||
|
port: ${{ secrets.PORT }}
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.PROD_DIR }}
|
||||||
|
docker compose pull
|
||||||
|
docker compose up -d --force-recreate
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
*.local
|
*.local
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
43
App.tsx
43
App.tsx
@@ -10,29 +10,7 @@ import EditButton from './components/layout/EditButton';
|
|||||||
import ConfigurationButton from './components/layout/ConfigurationButton';
|
import ConfigurationButton from './components/layout/ConfigurationButton';
|
||||||
import CategoryGroup from './components/layout/CategoryGroup';
|
import CategoryGroup from './components/layout/CategoryGroup';
|
||||||
import Wallpaper from './components/Wallpaper';
|
import Wallpaper from './components/Wallpaper';
|
||||||
|
import { ConfigurationService } from './components/services/ConfigurationService';
|
||||||
const defaultConfig: Config = {
|
|
||||||
title: 'Vision Start',
|
|
||||||
currentWallpapers: ['Abstract'],
|
|
||||||
wallpaperFrequency: '1d',
|
|
||||||
wallpaperBlur: 0,
|
|
||||||
wallpaperBrightness: 100,
|
|
||||||
wallpaperOpacity: 100,
|
|
||||||
titleSize: 'medium',
|
|
||||||
alignment: 'middle',
|
|
||||||
horizontalAlignment: 'middle',
|
|
||||||
clock: {
|
|
||||||
enabled: true,
|
|
||||||
size: 'medium',
|
|
||||||
font: 'Helvetica',
|
|
||||||
format: 'h:mm A',
|
|
||||||
},
|
|
||||||
serverWidget: {
|
|
||||||
enabled: false,
|
|
||||||
pingFrequency: 15,
|
|
||||||
servers: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [categories, setCategories] = useState<Category[]>(() => {
|
const [categories, setCategories] = useState<Category[]>(() => {
|
||||||
@@ -52,21 +30,10 @@ const App: React.FC = () => {
|
|||||||
const [addingWebsite, setAddingWebsite] = useState<Category | null>(null);
|
const [addingWebsite, setAddingWebsite] = useState<Category | null>(null);
|
||||||
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
const [editingCategory, setEditingCategory] = useState<Category | null>(null);
|
||||||
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
|
const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false);
|
||||||
const [config, setConfig] = useState<Config>(() => {
|
const [config, setConfig] = useState<Config>(() => ConfigurationService.loadConfig());
|
||||||
try {
|
|
||||||
const storedConfig = localStorage.getItem('config');
|
|
||||||
if (storedConfig) {
|
|
||||||
const parsedConfig = JSON.parse(storedConfig);
|
|
||||||
return { ...defaultConfig, ...parsedConfig };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing config from localStorage', error);
|
|
||||||
}
|
|
||||||
return { ...defaultConfig };
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem('config', JSON.stringify(config));
|
ConfigurationService.saveConfig(config);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -240,13 +207,13 @@ const App: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className={`flex justify-center transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}>
|
<div className={`flex justify-center transition-all duration-200 ease-ios transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingCategory(null);
|
setEditingCategory(null);
|
||||||
setIsCategoryModalOpen(true);
|
setIsCategoryModalOpen(true);
|
||||||
}}
|
}}
|
||||||
className="text-white/50 hover:text-white transition-colors"
|
className="text-white/50 hover:text-white active:scale-90 transition-all duration-150 ease-ios"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
||||||
|
|||||||
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN sh scripts/prepare_release.sh
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
COPY manifest.json /usr/share/nginx/html/
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
14
README.md
14
README.md
@@ -1,5 +1,12 @@
|
|||||||
# Vision Start
|
<div style="display: flex; justify-content: center; font-size: 2rem; font-weight: bold;">
|
||||||
#### A glassmorphism-looking like, modern and customizable startpage built with React.
|
Vision Start
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: center; font-size: 1.5rem;">
|
||||||
|
A glassmorphism-looking like, modern and customizable startpage built with React.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span style="display: block; text-align: center; font-size: 1.2rem;">Try it here: <a href="http://vision-start.ivanch.me">http://vision-start.ivanch.me</a></span>
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@@ -25,7 +32,7 @@ Vision Start is not yet available on Chrome Web Store, but it can be installed m
|
|||||||
* **Server Status Widgets:** Monitor the status of services directly from the startpage.
|
* **Server Status Widgets:** Monitor the status of services directly from the startpage.
|
||||||
* **Glassmorphism UI:** A modern and stylish interface with a frosted glass effect.
|
* **Glassmorphism UI:** A modern and stylish interface with a frosted glass effect.
|
||||||
* **Icon Library:** It uses the [Dashboard Icon library](https://dashboardicons.com/) for a better look and feel. It also supports auto-fetch for some websites.
|
* **Icon Library:** It uses the [Dashboard Icon library](https://dashboardicons.com/) for a better look and feel. It also supports auto-fetch for some websites.
|
||||||
* **Future**: a long to do list :(
|
* **Settings:** A settings page to configure the startpage, with export/import functionality.
|
||||||
|
|
||||||
## Backgrounds
|
## Backgrounds
|
||||||
|
|
||||||
@@ -87,4 +94,3 @@ npm run dev
|
|||||||
|
|
||||||
From a technical side:
|
From a technical side:
|
||||||
* Refactor everything :(
|
* Refactor everything :(
|
||||||
* Add small nginx demo (with docker)
|
|
||||||
|
|||||||
@@ -1,71 +1,42 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import ToggleSwitch from './ToggleSwitch';
|
import { Config, Wallpaper } from '../types';
|
||||||
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
|
||||||
import { Server, Wallpaper } from '../types';
|
|
||||||
|
|
||||||
import Dropdown from './Dropdown';
|
|
||||||
import { baseWallpapers } from './utils/baseWallpapers';
|
import { baseWallpapers } from './utils/baseWallpapers';
|
||||||
import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager';
|
import { checkChromeStorageLocalAvailable } from './utils/StorageLocalManager';
|
||||||
|
import { ConfigurationService } from './services/ConfigurationService';
|
||||||
|
import GeneralTab from './configuration/GeneralTab';
|
||||||
|
import ThemeTab from './configuration/ThemeTab';
|
||||||
|
import ClockTab from './configuration/ClockTab';
|
||||||
|
import ServerWidgetTab from './configuration/ServerWidgetTab';
|
||||||
|
|
||||||
interface ConfigurationModalProps {
|
interface ConfigurationModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (config: any) => void;
|
onSave: (config: Config) => void;
|
||||||
currentConfig: any;
|
currentConfig: Config;
|
||||||
onWallpaperChange: (newConfig: Partial<any>) => void;
|
onWallpaperChange: (newConfig: Partial<Config>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave, currentConfig, onWallpaperChange }) => {
|
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({
|
||||||
const [config, setConfig] = useState({
|
onClose,
|
||||||
...currentConfig,
|
onSave,
|
||||||
titleSize: currentConfig.titleSize || 'medium',
|
currentConfig,
|
||||||
alignment: currentConfig.alignment || 'middle',
|
onWallpaperChange,
|
||||||
tileSize: currentConfig.tileSize || 'medium',
|
}) => {
|
||||||
horizontalAlignment: currentConfig.horizontalAlignment || 'middle',
|
const [config, setConfig] = useState<Config>(currentConfig);
|
||||||
wallpaperBlur: currentConfig.wallpaperBlur || 0,
|
|
||||||
wallpaperBrightness: currentConfig.wallpaperBrightness || 100,
|
|
||||||
wallpaperOpacity: currentConfig.wallpaperOpacity || 100,
|
|
||||||
serverWidget: {
|
|
||||||
enabled: false,
|
|
||||||
pingFrequency: 15,
|
|
||||||
servers: [],
|
|
||||||
...currentConfig.serverWidget,
|
|
||||||
},
|
|
||||||
clock: {
|
|
||||||
enabled: true,
|
|
||||||
size: 'medium',
|
|
||||||
font: 'Helvetica',
|
|
||||||
format: 'h:mm A',
|
|
||||||
...currentConfig.clock,
|
|
||||||
},
|
|
||||||
currentWallpapers: Array.isArray(currentConfig.currentWallpapers)
|
|
||||||
? currentConfig.currentWallpapers.filter((name: string) => typeof name === 'string')
|
|
||||||
: [],
|
|
||||||
wallpaperFrequency: currentConfig.wallpaperFrequency || '1d',
|
|
||||||
});
|
|
||||||
const [activeTab, setActiveTab] = useState('general');
|
const [activeTab, setActiveTab] = useState('general');
|
||||||
const [newServerName, setNewServerName] = useState('');
|
|
||||||
const [newServerAddress, setNewServerAddress] = useState('');
|
|
||||||
const [newWallpaperName, setNewWallpaperName] = useState('');
|
|
||||||
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
|
|
||||||
const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
|
const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
|
||||||
const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false);
|
const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const isSaving = useRef(false);
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const importInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const isSaving = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChromeStorageAvailable(checkChromeStorageLocalAvailable());
|
setChromeStorageAvailable(checkChromeStorageLocalAvailable());
|
||||||
const storedUserWallpapers = localStorage.getItem('userWallpapers');
|
setUserWallpapers(ConfigurationService.loadUserWallpapers());
|
||||||
if (storedUserWallpapers) {
|
|
||||||
setUserWallpapers(JSON.parse(storedUserWallpapers));
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => setIsVisible(true), 10);
|
||||||
setIsVisible(true);
|
|
||||||
}, 10);
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -77,179 +48,98 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setIsVisible(false);
|
|
||||||
setTimeout(() => {
|
|
||||||
onClose();
|
|
||||||
}, 300);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | { target: { name: string; value: string | string[] } }) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
if (name === 'currentWallpapers') {
|
|
||||||
const wallpaperNames = Array.isArray(value) ? value : [value];
|
|
||||||
setConfig({ ...config, currentWallpapers: wallpaperNames });
|
|
||||||
} else if (name.startsWith('serverWidget.')) {
|
|
||||||
const field = name.split('.')[1];
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
serverWidget: { ...config.serverWidget, [field]: value },
|
|
||||||
});
|
|
||||||
} else if (name.startsWith('clock.')) {
|
|
||||||
const field = name.split('.')[1];
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
clock: { ...config.clock, [field]: value },
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setConfig({ ...config, [name]: value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onWallpaperChange({ currentWallpapers: config.currentWallpapers });
|
onWallpaperChange({ currentWallpapers: config.currentWallpapers });
|
||||||
// Set wallpaperState in localStorage with lastWallpaperChange datetime
|
ConfigurationService.resetWallpaperState();
|
||||||
localStorage.setItem('wallpaperState', JSON.stringify({
|
|
||||||
lastWallpaperChange: new Date().toISOString(),
|
|
||||||
currentIndex: 0,
|
|
||||||
}));
|
|
||||||
}, [config.currentWallpapers]);
|
}, [config.currentWallpapers]);
|
||||||
|
|
||||||
const handleClockToggleChange = (checked: boolean) => {
|
const handleClose = () => {
|
||||||
setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 250);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleServerWidgetToggleChange = (checked: boolean) => {
|
const handleConfigChange = (updates: Partial<Config>) => {
|
||||||
setConfig({
|
setConfig((prev) => ({ ...prev, ...updates }));
|
||||||
...config,
|
|
||||||
serverWidget: { ...config.serverWidget, enabled: checked },
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddServer = () => {
|
const handleAddWallpaper = async (name: string, url: string) => {
|
||||||
if (newServerName.trim() === '' || newServerAddress.trim() === '') return;
|
const newWallpaper = await ConfigurationService.addWallpaper(name, url);
|
||||||
|
const updated = [...userWallpapers, newWallpaper];
|
||||||
const newServer: Server = {
|
setUserWallpapers(updated);
|
||||||
id: Date.now().toString(),
|
ConfigurationService.saveUserWallpapers(updated);
|
||||||
name: newServerName,
|
setConfig((prev) => ({
|
||||||
address: newServerAddress,
|
...prev,
|
||||||
};
|
currentWallpapers: [...prev.currentWallpapers, newWallpaper.name],
|
||||||
|
}));
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
serverWidget: {
|
|
||||||
...config.serverWidget,
|
|
||||||
servers: [...config.serverWidget.servers, newServer],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
setNewServerName('');
|
|
||||||
setNewServerAddress('');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveServer = (id: string) => {
|
const handleAddWallpaperFile = async (file: File) => {
|
||||||
setConfig({
|
const newWallpaper = await ConfigurationService.addWallpaperFile(file);
|
||||||
...config,
|
const updated = [...userWallpapers, newWallpaper];
|
||||||
serverWidget: {
|
setUserWallpapers(updated);
|
||||||
...config.serverWidget,
|
ConfigurationService.saveUserWallpapers(updated);
|
||||||
servers: config.serverWidget.servers.filter((server: Server) => server.id !== id),
|
setConfig((prev) => ({
|
||||||
},
|
...prev,
|
||||||
});
|
currentWallpapers: [...prev.currentWallpapers, newWallpaper.name],
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (result: any) => {
|
const handleDeleteWallpaper = async (wallpaper: Wallpaper) => {
|
||||||
if (!result.destination) return;
|
|
||||||
|
|
||||||
const items = Array.from(config.serverWidget.servers);
|
|
||||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
|
||||||
items.splice(result.destination.index, 0, reorderedItem);
|
|
||||||
|
|
||||||
setConfig({
|
|
||||||
...config,
|
|
||||||
serverWidget: {
|
|
||||||
...config.serverWidget,
|
|
||||||
servers: items,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddWallpaper = async () => {
|
|
||||||
if (newWallpaperUrl.trim() === '') return;
|
|
||||||
try {
|
try {
|
||||||
const finalName = await addWallpaperToChromeStorageLocal(newWallpaperName, newWallpaperUrl);
|
await ConfigurationService.deleteWallpaper(wallpaper);
|
||||||
const newWallpaper: Wallpaper = { name: finalName };
|
const updated = userWallpapers.filter((w) => w.name !== wallpaper.name);
|
||||||
const updatedUserWallpapers = [...userWallpapers, newWallpaper];
|
setUserWallpapers(updated);
|
||||||
setUserWallpapers(updatedUserWallpapers);
|
ConfigurationService.saveUserWallpapers(updated);
|
||||||
localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
|
const newCurrentWallpapers = config.currentWallpapers.filter((n) => n !== wallpaper.name);
|
||||||
setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
|
setConfig((prev) => ({ ...prev, currentWallpapers: newCurrentWallpapers }));
|
||||||
setNewWallpaperName('');
|
onWallpaperChange({ currentWallpapers: newCurrentWallpapers });
|
||||||
setNewWallpaperUrl('');
|
|
||||||
} catch (error) {
|
|
||||||
alert('Error adding wallpaper. Please check the URL and try again.');
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
if (file.size > 4 * 1024 * 1024) {
|
|
||||||
alert('File size exceeds 4MB. Please choose a smaller file.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async () => {
|
|
||||||
const base64 = reader.result as string;
|
|
||||||
if (base64.length > 4.5 * 1024 * 1024) {
|
|
||||||
alert('The uploaded image is too large. Please choose a smaller file.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const finalName = await addWallpaperToChromeStorageLocal(file.name, base64);
|
|
||||||
const newWallpaper: Wallpaper = { name: finalName };
|
|
||||||
const updatedUserWallpapers = [...userWallpapers, newWallpaper];
|
|
||||||
setUserWallpapers(updatedUserWallpapers);
|
|
||||||
localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
|
|
||||||
setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
|
|
||||||
} catch (error) {
|
|
||||||
alert('Error adding wallpaper. Please try again.');
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteUserWallpaper = async (wallpaper: Wallpaper) => {
|
|
||||||
try {
|
|
||||||
await removeWallpaperFromChromeStorageLocal(wallpaper.name);
|
|
||||||
const updatedUserWallpapers = userWallpapers.filter(w => w.name !== wallpaper.name);
|
|
||||||
setUserWallpapers(updatedUserWallpapers);
|
|
||||||
localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
|
|
||||||
const newcurrentWallpapers = config.currentWallpapers.filter((name: string) => name !== wallpaper.name);
|
|
||||||
const newConfig = { ...config, currentWallpapers: newcurrentWallpapers };
|
|
||||||
setConfig(newConfig);
|
|
||||||
onWallpaperChange({ currentWallpapers: newcurrentWallpapers });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error deleting wallpaper. Please try again.');
|
alert('Error deleting wallpaper. Please try again.');
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportConfig = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const { config: importedConfig, userWallpapers: importedWallpapers } =
|
||||||
|
await ConfigurationService.importConfig(file);
|
||||||
|
setConfig(importedConfig);
|
||||||
|
setUserWallpapers(importedWallpapers);
|
||||||
|
onWallpaperChange({ currentWallpapers: importedConfig.currentWallpapers || [] });
|
||||||
|
onSave(importedConfig);
|
||||||
|
alert('Configuration imported successfully. The page will reload to apply all data.');
|
||||||
|
window.location.reload();
|
||||||
|
} catch (error) {
|
||||||
|
alert('Could not import configuration. Please use a valid export JSON file.');
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'general', label: 'General' },
|
||||||
|
{ id: 'theme', label: 'Theme' },
|
||||||
|
{ id: 'clock', label: 'Clock' },
|
||||||
|
{ id: 'serverWidget', label: 'Server Widget' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-50" role="dialog" aria-modal="true">
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-300 ease-in-out ${
|
className={`fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-250 ease-ios ${
|
||||||
isVisible ? 'opacity-100' : 'opacity-0'
|
isVisible ? 'opacity-100' : 'opacity-0'
|
||||||
}`}
|
}`}
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
></div>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={`fixed top-0 right-0 h-full w-full max-w-lg bg-black/50 backdrop-blur-xl border-l border-white/10 text-white flex flex-col transition-transform duration-300 ease-in-out transform ${
|
className={`fixed top-0 right-0 h-full w-full max-w-lg bg-black/50 backdrop-blur-xl border-l border-white/10 text-white flex flex-col transition-transform duration-300 ease-spring transform ${
|
||||||
isVisible ? 'translate-x-0' : 'translate-x-full'
|
isVisible ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -257,398 +147,85 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
<h2 className="text-3xl font-bold mb-6">Configuration</h2>
|
<h2 className="text-3xl font-bold mb-6">Configuration</h2>
|
||||||
|
|
||||||
<div className="flex border-b border-white/10 mb-6">
|
<div className="flex border-b border-white/10 mb-6">
|
||||||
<button
|
{tabs.map((tab) => (
|
||||||
className={`px-4 py-2 text-lg font-semibold ${activeTab === 'general' ? 'text-cyan-400 border-b-2 border-cyan-400' : 'text-slate-400'}`}
|
<button
|
||||||
onClick={() => setActiveTab('general')}
|
key={tab.id}
|
||||||
>
|
className={`px-4 py-2 text-lg font-semibold ${
|
||||||
General
|
activeTab === tab.id
|
||||||
</button>
|
? 'text-cyan-400 border-b-2 border-cyan-400'
|
||||||
<button
|
: 'text-slate-400'
|
||||||
className={`px-4 py-2 text-lg font-semibold ${activeTab === 'theme' ? 'text-cyan-400 border-b-2 border-cyan-400' : 'text-slate-400'}`}
|
}`}
|
||||||
onClick={() => setActiveTab('theme')}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
>
|
>
|
||||||
Theme
|
{tab.label}
|
||||||
</button>
|
</button>
|
||||||
<button
|
))}
|
||||||
className={`px-4 py-2 text-lg font-semibold ${activeTab === 'clock' ? 'text-cyan-400 border-b-2 border-cyan-400' : 'text-slate-400'}`}
|
|
||||||
onClick={() => setActiveTab('clock')}
|
|
||||||
>
|
|
||||||
Clock
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`px-4 py-2 text-lg font-semibold ${activeTab === 'serverWidget' ? 'text-cyan-400 border-b-2 border-cyan-400' : 'text-slate-400'}`}
|
|
||||||
onClick={() => setActiveTab('serverWidget')}
|
|
||||||
>
|
|
||||||
Server Widget
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeTab === 'general' && (
|
{activeTab === 'general' && (
|
||||||
<div className="flex flex-col gap-6">
|
<GeneralTab config={config} onChange={handleConfigChange} />
|
||||||
<div>
|
|
||||||
<label className="text-slate-300 text-sm font-semibold mb-2 block">Title</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="title"
|
|
||||||
value={config.title}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="bg-white/10 p-3 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Title Size</label>
|
|
||||||
<Dropdown
|
|
||||||
name="titleSize"
|
|
||||||
value={config.titleSize}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'tiny', label: 'Tiny' },
|
|
||||||
{ value: 'small', label: 'Small' },
|
|
||||||
{ value: 'medium', label: 'Medium' },
|
|
||||||
{ value: 'large', label: 'Large' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Vertical Alignment</label>
|
|
||||||
<Dropdown
|
|
||||||
name="alignment"
|
|
||||||
value={config.alignment}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'top', label: 'Top' },
|
|
||||||
{ value: 'middle', label: 'Middle' },
|
|
||||||
{ value: 'bottom', label: 'Bottom' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Tile Size</label>
|
|
||||||
<Dropdown
|
|
||||||
name="tileSize"
|
|
||||||
value={config.tileSize}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'small', label: 'Small' },
|
|
||||||
{ value: 'medium', label: 'Medium' },
|
|
||||||
{ value: 'large', label: 'Large' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Horizontal Alignment</label>
|
|
||||||
<Dropdown
|
|
||||||
name="horizontalAlignment"
|
|
||||||
value={config.horizontalAlignment}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'left', label: 'Left' },
|
|
||||||
{ value: 'middle', label: 'Middle' },
|
|
||||||
{ value: 'right', label: 'Right' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'theme' && (
|
{activeTab === 'theme' && (
|
||||||
<div className="flex flex-col gap-6">
|
<ThemeTab
|
||||||
<div className="flex items-center justify-between">
|
config={config}
|
||||||
<label className="text-slate-300 text-sm font-semibold">Background</label>
|
onChange={handleConfigChange}
|
||||||
<Dropdown
|
userWallpapers={userWallpapers}
|
||||||
name="currentWallpapers"
|
allWallpapers={allWallpapers}
|
||||||
value={config.currentWallpapers}
|
chromeStorageAvailable={chromeStorageAvailable}
|
||||||
onChange={handleChange}
|
onAddWallpaper={handleAddWallpaper}
|
||||||
multiple
|
onAddWallpaperFile={handleAddWallpaperFile}
|
||||||
options={allWallpapers.map(w => ({
|
onDeleteWallpaper={handleDeleteWallpaper}
|
||||||
value: w.name,
|
/>
|
||||||
label: w.name
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
|
|
||||||
<Dropdown
|
|
||||||
name="wallpaperFrequency"
|
|
||||||
value={config.wallpaperFrequency}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: '1h', label: '1 hour' },
|
|
||||||
{ value: '3h', label: '3 hours' },
|
|
||||||
{ value: '6h', label: '6 hours' },
|
|
||||||
{ value: '12h', label: '12 hours' },
|
|
||||||
{ value: '1d', label: '1 day' },
|
|
||||||
{ value: '2d', label: '2 days' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Wallpaper Blur</label>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
name="wallpaperBlur"
|
|
||||||
min="0"
|
|
||||||
max="50"
|
|
||||||
value={config.wallpaperBlur}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-48"
|
|
||||||
/>
|
|
||||||
<span>{config.wallpaperBlur}px</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Wallpaper Brightness</label>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
name="wallpaperBrightness"
|
|
||||||
min="0"
|
|
||||||
max="200"
|
|
||||||
value={config.wallpaperBrightness}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-48"
|
|
||||||
/>
|
|
||||||
<span>{config.wallpaperBrightness}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Wallpaper Opacity</label>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
name="wallpaperOpacity"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
value={config.wallpaperOpacity}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-48"
|
|
||||||
/>
|
|
||||||
<span>{config.wallpaperOpacity}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{chromeStorageAvailable && (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-slate-300 text-sm font-semibold mb-2">User Wallpapers</h3>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{userWallpapers.map((wallpaper) => (
|
|
||||||
<div key={wallpaper.name} className="flex items-center justify-between bg-white/10 p-2 rounded-lg">
|
|
||||||
<span className="truncate">{wallpaper.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteUserWallpaper(wallpaper)}
|
|
||||||
className="text-red-500 hover:text-red-400"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-trash" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
||||||
<path fillRule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Wallpaper Name (optional for URLs)"
|
|
||||||
value={newWallpaperName}
|
|
||||||
onChange={(e) => setNewWallpaperName(e.target.value)}
|
|
||||||
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
|
||||||
/>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Image URL"
|
|
||||||
value={newWallpaperUrl}
|
|
||||||
onChange={(e) => setNewWallpaperUrl(e.target.value)}
|
|
||||||
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleAddWallpaper}
|
|
||||||
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center w-full">
|
|
||||||
<label
|
|
||||||
htmlFor="file-upload"
|
|
||||||
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
|
||||||
<svg className="w-8 h-8 mb-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
|
|
||||||
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
|
|
||||||
</svg>
|
|
||||||
<p className="mb-2 text-sm text-gray-400"><span className="font-semibold">Click to upload</span> or drag and drop</p>
|
|
||||||
<p className="text-xs text-gray-400">PNG, JPG, WEBP, etc.</p>
|
|
||||||
</div>
|
|
||||||
<input id="file-upload" type="file" className="hidden" onChange={handleFileUpload} ref={fileInputRef} />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'clock' && (
|
{activeTab === 'clock' && (
|
||||||
<div className="flex flex-col gap-6">
|
<ClockTab config={config} onChange={handleConfigChange} />
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Enable Clock</label>
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={config.clock.enabled}
|
|
||||||
onChange={handleClockToggleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Clock Size</label>
|
|
||||||
<Dropdown
|
|
||||||
name="clock.size"
|
|
||||||
value={config.clock.size}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'tiny', label: 'Tiny' },
|
|
||||||
{ value: 'small', label: 'Small' },
|
|
||||||
{ value: 'medium', label: 'Medium' },
|
|
||||||
{ value: 'large', label: 'Large' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Clock Font</label>
|
|
||||||
<Dropdown
|
|
||||||
name="clock.font"
|
|
||||||
value={config.clock.font}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'Helvetica', label: 'Helvetica' },
|
|
||||||
{ value: `'Orbitron', sans-serif`, label: 'Orbitron' },
|
|
||||||
{ value: 'monospace', label: 'Monospace' },
|
|
||||||
{ value: 'cursive', label: 'Cursive' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Time Format</label>
|
|
||||||
<Dropdown
|
|
||||||
name="clock.format"
|
|
||||||
value={config.clock.format}
|
|
||||||
onChange={handleChange}
|
|
||||||
options={[
|
|
||||||
{ value: 'h:mm A', label: 'AM/PM' },
|
|
||||||
{ value: 'HH:mm', label: '24:00' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'serverWidget' && (
|
{activeTab === 'serverWidget' && (
|
||||||
<div className="flex flex-col gap-6">
|
<ServerWidgetTab config={config} onChange={handleConfigChange} />
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Enable Server Widget</label>
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={config.serverWidget.enabled}
|
|
||||||
onChange={handleServerWidgetToggleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{config.serverWidget.enabled && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<label className="text-slate-300 text-sm font-semibold">Ping Frequency</label>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
name="serverWidget.pingFrequency"
|
|
||||||
min="5"
|
|
||||||
max="60"
|
|
||||||
value={config.serverWidget.pingFrequency}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="w-48"
|
|
||||||
/>
|
|
||||||
<span>{config.serverWidget.pingFrequency}s</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-slate-300 text-sm font-semibold mb-2">Servers</h3>
|
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
|
||||||
<Droppable droppableId="servers">
|
|
||||||
{(provided) => (
|
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef} className="flex flex-col gap-2">
|
|
||||||
{config.serverWidget.servers.map((server: Server, index: number) => (
|
|
||||||
<Draggable key={server.id} draggableId={server.id} index={index}>
|
|
||||||
{(provided) => (
|
|
||||||
<div
|
|
||||||
ref={provided.innerRef}
|
|
||||||
{...provided.draggableProps}
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p className="font-semibold">{server.name}</p>
|
|
||||||
<p className="text-sm text-slate-400">{server.address}</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => handleRemoveServer(server.id)}
|
|
||||||
className="text-red-500 hover:text-red-400"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-trash" viewBox="0 0 16 16">
|
|
||||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
|
||||||
<path fillRule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Draggable>
|
|
||||||
))}
|
|
||||||
{provided.placeholder}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
</DragDropContext>
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Server Name"
|
|
||||||
value={newServerName}
|
|
||||||
onChange={(e) => setNewServerName(e.target.value)}
|
|
||||||
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="HTTP Address"
|
|
||||||
value={newServerAddress}
|
|
||||||
onChange={(e) => setNewServerAddress(e.target.value)}
|
|
||||||
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleAddServer}
|
|
||||||
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-8 border-t border-white/10">
|
<div className="p-8 border-t border-white/10">
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<button onClick={() => { isSaving.current = true; onSave(config); }} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
|
<div className="flex gap-2">
|
||||||
Save & Close
|
<button
|
||||||
</button>
|
onClick={() => ConfigurationService.exportConfig()}
|
||||||
<button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
|
className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios"
|
||||||
Cancel
|
>
|
||||||
</button>
|
Export
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => importInputRef.current?.click()}
|
||||||
|
className="bg-slate-700 hover:bg-slate-600 active:scale-95 text-white text-sm font-semibold py-1.5 px-3 rounded-lg transition-all duration-150 ease-ios"
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={importInputRef}
|
||||||
|
type="file"
|
||||||
|
accept="application/json"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImportConfig}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
isSaving.current = true;
|
||||||
|
onSave(config);
|
||||||
|
}}
|
||||||
|
className="bg-green-500 hover:bg-green-400 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"
|
||||||
|
>
|
||||||
|
Save & Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleClose}
|
||||||
|
className="bg-gray-600 hover:bg-gray-500 active:scale-95 text-white font-bold py-2 px-6 rounded-lg transition-all duration-150 ease-ios"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
|
|||||||
>
|
>
|
||||||
<span className="truncate">{selectedOptionLabel}</span>
|
<span className="truncate">{selectedOptionLabel}</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-5 h-5 transition-transform duration-300 ease-in-out ${isOpen ? 'rotate-180' : 'rotate-0'}`}
|
className={`w-5 h-5 transition-transform duration-200 ease-ios ${isOpen ? 'rotate-180' : 'rotate-0'}`}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -95,14 +95,14 @@ const Dropdown: React.FC<DropdownProps> = ({ options, value, onChange, name, mul
|
|||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<ul
|
<ul
|
||||||
className="absolute z-10 mt-1 w-full bg-black/70 backdrop-blur-xl border border-white/20 rounded-lg shadow-2xl overflow-hidden animate-in slide-in-from-top-2 fade-in duration-200"
|
className="absolute z-10 mt-1 w-full bg-black/70 backdrop-blur-xl border border-white/20 rounded-lg shadow-2xl overflow-hidden animate-in slide-in-from-top-2 fade-in duration-150"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
>
|
>
|
||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<li
|
<li
|
||||||
key={option.value}
|
key={option.value}
|
||||||
onClick={() => handleOptionClick(option.value)}
|
onClick={() => handleOptionClick(option.value)}
|
||||||
className={`h-10 px-3 text-white cursor-pointer transition-all duration-150 ease-in-out flex items-center
|
className={`h-10 px-3 text-white cursor-pointer transition-all duration-150 ease-ios flex items-center
|
||||||
${
|
${
|
||||||
isSelected(option.value)
|
isSelected(option.value)
|
||||||
? 'bg-cyan-500/20 text-cyan-300'
|
? 'bg-cyan-500/20 text-cyan-300'
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ const ToggleSwitch: React.FC<ToggleSwitchProps> = ({ checked, onChange }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-14 h-8 flex items-center rounded-full p-1 cursor-pointer transition-colors duration-300 ${checked ? 'bg-cyan-500' : 'bg-gray-600'}`}
|
className={`w-14 h-8 flex items-center rounded-full p-1 cursor-pointer transition-colors duration-200 ease-ios ${checked ? 'bg-cyan-500' : 'bg-gray-600'}`}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`bg-white w-6 h-6 rounded-full shadow-md transform transition-transform duration-300 ${checked ? 'translate-x-6' : 'translate-x-0'}`}
|
className={`bg-white w-6 h-6 rounded-full shadow-md transform transition-transform duration-200 ease-spring ${checked ? 'translate-x-6' : 'translate-x-0'}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,13 +73,13 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
|
|||||||
const iconSizeLoadingClass = `w-[${getIconLoadingPixelSize(tileSize)}px] h-[${getIconLoadingPixelSize(tileSize)}px]`;
|
const iconSizeLoadingClass = `w-[${getIconLoadingPixelSize(tileSize)}px] h-[${getIconLoadingPixelSize(tileSize)}px]`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-300 ease-in-out`}>
|
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-200 ease-ios`}>
|
||||||
<a
|
<a
|
||||||
href={isEditing ? undefined : website.url}
|
href={isEditing ? undefined : website.url}
|
||||||
target="_self"
|
target="_self"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="group flex flex-col items-center justify-center p-4 bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl w-full h-full transform transition-all duration-300 ease-in-out hover:scale-105 hover:bg-white/25 shadow-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:ring-opacity-75"
|
className="group flex flex-col items-center justify-center p-4 bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl w-full h-full transform transition-all duration-200 ease-ios hover:scale-[1.04] active:scale-[0.96] hover:bg-white/25 shadow-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 focus:ring-opacity-75"
|
||||||
>
|
>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center mb-6">
|
<div className="absolute inset-0 flex items-center justify-center mb-6">
|
||||||
@@ -89,11 +89,11 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={`flex items-center transition-all duration-300 ease-in ${isLoading ? 'mt-18' : 'flex-col'} ${isLoading ? 'gap-2' : ''}`}>
|
<div className={`flex items-center transition-all duration-200 ease-ios ${isLoading ? 'mt-18' : 'flex-col'} ${isLoading ? 'gap-2' : ''}`}>
|
||||||
<div className={`transition-all duration-300 ease-in ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
|
<div className={`transition-all duration-200 ease-ios ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
|
||||||
<img src={website.icon} alt={`${website.name} icon`} className={`object-contain w-full h-full`} />
|
<img src={website.icon} alt={`${website.name} icon`} className={`object-contain w-full h-full`} />
|
||||||
</div>
|
</div>
|
||||||
<span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-300 ease-in ${isLoading ? 'text-sm' : ''}`}>
|
<span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-200 ease-ios ${isLoading ? 'text-sm' : ''}`}>
|
||||||
{website.name}
|
{website.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
69
components/configuration/ClockTab.tsx
Normal file
69
components/configuration/ClockTab.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Dropdown from '../Dropdown';
|
||||||
|
import ToggleSwitch from '../ToggleSwitch';
|
||||||
|
import { Config } from '../../types';
|
||||||
|
|
||||||
|
interface ClockTabProps {
|
||||||
|
config: Config;
|
||||||
|
onChange: (updates: Partial<Config>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ClockTab: React.FC<ClockTabProps> = ({ config, onChange }) => {
|
||||||
|
const updateClock = (updates: Partial<Config['clock']>) => {
|
||||||
|
onChange({ clock: { ...config.clock, ...updates } });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Enable Clock</label>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={config.clock.enabled}
|
||||||
|
onChange={(checked) => updateClock({ enabled: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Clock Size</label>
|
||||||
|
<Dropdown
|
||||||
|
name="clock.size"
|
||||||
|
value={config.clock.size}
|
||||||
|
onChange={(e) => updateClock({ size: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'tiny', label: 'Tiny' },
|
||||||
|
{ value: 'small', label: 'Small' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'large', label: 'Large' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Clock Font</label>
|
||||||
|
<Dropdown
|
||||||
|
name="clock.font"
|
||||||
|
value={config.clock.font}
|
||||||
|
onChange={(e) => updateClock({ font: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'Helvetica', label: 'Helvetica' },
|
||||||
|
{ value: `'Orbitron', sans-serif`, label: 'Orbitron' },
|
||||||
|
{ value: 'monospace', label: 'Monospace' },
|
||||||
|
{ value: 'cursive', label: 'Cursive' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Time Format</label>
|
||||||
|
<Dropdown
|
||||||
|
name="clock.format"
|
||||||
|
value={config.clock.format}
|
||||||
|
onChange={(e) => updateClock({ format: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'h:mm A', label: 'AM/PM' },
|
||||||
|
{ value: 'HH:mm', label: '24:00' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ClockTab;
|
||||||
79
components/configuration/GeneralTab.tsx
Normal file
79
components/configuration/GeneralTab.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Dropdown from '../Dropdown';
|
||||||
|
import { Config } from '../../types';
|
||||||
|
|
||||||
|
interface GeneralTabProps {
|
||||||
|
config: Config;
|
||||||
|
onChange: (updates: Partial<Config>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GeneralTab: React.FC<GeneralTabProps> = ({ config, onChange }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="text-slate-300 text-sm font-semibold mb-2 block">Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={config.title}
|
||||||
|
onChange={(e) => onChange({ title: e.target.value })}
|
||||||
|
className="bg-white/10 p-3 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Title Size</label>
|
||||||
|
<Dropdown
|
||||||
|
name="titleSize"
|
||||||
|
value={config.titleSize}
|
||||||
|
onChange={(e) => onChange({ titleSize: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'tiny', label: 'Tiny' },
|
||||||
|
{ value: 'small', label: 'Small' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'large', label: 'Large' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Vertical Alignment</label>
|
||||||
|
<Dropdown
|
||||||
|
name="alignment"
|
||||||
|
value={config.alignment}
|
||||||
|
onChange={(e) => onChange({ alignment: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'top', label: 'Top' },
|
||||||
|
{ value: 'middle', label: 'Middle' },
|
||||||
|
{ value: 'bottom', label: 'Bottom' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Tile Size</label>
|
||||||
|
<Dropdown
|
||||||
|
name="tileSize"
|
||||||
|
value={config.tileSize || 'medium'}
|
||||||
|
onChange={(e) => onChange({ tileSize: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'small', label: 'Small' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'large', label: 'Large' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Horizontal Alignment</label>
|
||||||
|
<Dropdown
|
||||||
|
name="horizontalAlignment"
|
||||||
|
value={config.horizontalAlignment}
|
||||||
|
onChange={(e) => onChange({ horizontalAlignment: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: 'left', label: 'Left' },
|
||||||
|
{ value: 'middle', label: 'Middle' },
|
||||||
|
{ value: 'right', label: 'Right' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GeneralTab;
|
||||||
150
components/configuration/ServerWidgetTab.tsx
Normal file
150
components/configuration/ServerWidgetTab.tsx
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
|
||||||
|
import ToggleSwitch from '../ToggleSwitch';
|
||||||
|
import { Config, Server } from '../../types';
|
||||||
|
|
||||||
|
interface ServerWidgetTabProps {
|
||||||
|
config: Config;
|
||||||
|
onChange: (updates: Partial<Config>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServerWidgetTab: React.FC<ServerWidgetTabProps> = ({ config, onChange }) => {
|
||||||
|
const [newServerName, setNewServerName] = useState('');
|
||||||
|
const [newServerAddress, setNewServerAddress] = useState('');
|
||||||
|
|
||||||
|
const updateServerWidget = (updates: Partial<Config['serverWidget']>) => {
|
||||||
|
onChange({ serverWidget: { ...config.serverWidget, ...updates } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddServer = () => {
|
||||||
|
if (newServerName.trim() === '' || newServerAddress.trim() === '') return;
|
||||||
|
const newServer: Server = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
name: newServerName,
|
||||||
|
address: newServerAddress,
|
||||||
|
};
|
||||||
|
updateServerWidget({ servers: [...config.serverWidget.servers, newServer] });
|
||||||
|
setNewServerName('');
|
||||||
|
setNewServerAddress('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveServer = (id: string) => {
|
||||||
|
updateServerWidget({
|
||||||
|
servers: config.serverWidget.servers.filter((s) => s.id !== id),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (result: any) => {
|
||||||
|
if (!result.destination) return;
|
||||||
|
const items = Array.from(config.serverWidget.servers);
|
||||||
|
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||||
|
items.splice(result.destination.index, 0, reorderedItem);
|
||||||
|
updateServerWidget({ servers: items });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Enable Server Widget</label>
|
||||||
|
<ToggleSwitch
|
||||||
|
checked={config.serverWidget.enabled}
|
||||||
|
onChange={(checked) => updateServerWidget({ enabled: checked })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{config.serverWidget.enabled && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Ping Frequency</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="5"
|
||||||
|
max="60"
|
||||||
|
value={config.serverWidget.pingFrequency}
|
||||||
|
onChange={(e) => updateServerWidget({ pingFrequency: Number(e.target.value) })}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
<span>{config.serverWidget.pingFrequency}s</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-slate-300 text-sm font-semibold mb-2">Servers</h3>
|
||||||
|
<DragDropContext onDragEnd={onDragEnd}>
|
||||||
|
<Droppable droppableId="servers">
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
{...provided.droppableProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
className="flex flex-col gap-2"
|
||||||
|
>
|
||||||
|
{config.serverWidget.servers.map((server: Server, index: number) => (
|
||||||
|
<Draggable key={server.id} draggableId={server.id} index={index}>
|
||||||
|
{(provided) => (
|
||||||
|
<div
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{server.name}</p>
|
||||||
|
<p className="text-sm text-slate-400">{server.address}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveServer(server.id)}
|
||||||
|
className="text-red-500 hover:text-red-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
className="bi bi-trash"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Server Name"
|
||||||
|
value={newServerName}
|
||||||
|
onChange={(e) => setNewServerName(e.target.value)}
|
||||||
|
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="HTTP Address"
|
||||||
|
value={newServerAddress}
|
||||||
|
onChange={(e) => setNewServerAddress(e.target.value)}
|
||||||
|
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAddServer}
|
||||||
|
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerWidgetTab;
|
||||||
228
components/configuration/ThemeTab.tsx
Normal file
228
components/configuration/ThemeTab.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import Dropdown from '../Dropdown';
|
||||||
|
import { Config, Wallpaper } from '../../types';
|
||||||
|
|
||||||
|
interface ThemeTabProps {
|
||||||
|
config: Config;
|
||||||
|
onChange: (updates: Partial<Config>) => void;
|
||||||
|
userWallpapers: Wallpaper[];
|
||||||
|
allWallpapers: Wallpaper[];
|
||||||
|
chromeStorageAvailable: boolean;
|
||||||
|
onAddWallpaper: (name: string, url: string) => Promise<void>;
|
||||||
|
onAddWallpaperFile: (file: File) => Promise<void>;
|
||||||
|
onDeleteWallpaper: (wallpaper: Wallpaper) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
userWallpapers,
|
||||||
|
allWallpapers,
|
||||||
|
chromeStorageAvailable,
|
||||||
|
onAddWallpaper,
|
||||||
|
onAddWallpaperFile,
|
||||||
|
onDeleteWallpaper,
|
||||||
|
}) => {
|
||||||
|
const [newWallpaperName, setNewWallpaperName] = useState('');
|
||||||
|
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleAddWallpaper = async () => {
|
||||||
|
if (newWallpaperUrl.trim() === '') return;
|
||||||
|
try {
|
||||||
|
await onAddWallpaper(newWallpaperName, newWallpaperUrl);
|
||||||
|
setNewWallpaperName('');
|
||||||
|
setNewWallpaperUrl('');
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error adding wallpaper. Please check the URL and try again.');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
await onAddWallpaperFile(file);
|
||||||
|
} catch (error: any) {
|
||||||
|
alert(error?.message || 'Error adding wallpaper. Please try again.');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
e.target.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Background</label>
|
||||||
|
<Dropdown
|
||||||
|
name="currentWallpapers"
|
||||||
|
value={config.currentWallpapers}
|
||||||
|
onChange={(e) => onChange({ currentWallpapers: e.target.value as string[] })}
|
||||||
|
multiple
|
||||||
|
options={allWallpapers.map((w) => ({ value: w.name, label: w.name }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
|
||||||
|
<Dropdown
|
||||||
|
name="wallpaperFrequency"
|
||||||
|
value={config.wallpaperFrequency}
|
||||||
|
onChange={(e) => onChange({ wallpaperFrequency: e.target.value as string })}
|
||||||
|
options={[
|
||||||
|
{ value: '1h', label: '1 hour' },
|
||||||
|
{ value: '3h', label: '3 hours' },
|
||||||
|
{ value: '6h', label: '6 hours' },
|
||||||
|
{ value: '12h', label: '12 hours' },
|
||||||
|
{ value: '1d', label: '1 day' },
|
||||||
|
{ value: '2d', label: '2 days' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Wallpaper Blur</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="50"
|
||||||
|
value={config.wallpaperBlur}
|
||||||
|
onChange={(e) => onChange({ wallpaperBlur: Number(e.target.value) })}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
<span>{config.wallpaperBlur}px</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Wallpaper Brightness</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="200"
|
||||||
|
value={config.wallpaperBrightness}
|
||||||
|
onChange={(e) => onChange({ wallpaperBrightness: Number(e.target.value) })}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
<span>{config.wallpaperBrightness}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-slate-300 text-sm font-semibold">Wallpaper Opacity</label>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
value={config.wallpaperOpacity}
|
||||||
|
onChange={(e) => onChange({ wallpaperOpacity: Number(e.target.value) })}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
<span>{config.wallpaperOpacity}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{chromeStorageAvailable && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-slate-300 text-sm font-semibold mb-2">User Wallpapers</h3>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{userWallpapers.map((wallpaper) => (
|
||||||
|
<div
|
||||||
|
key={wallpaper.name}
|
||||||
|
className="flex items-center justify-between bg-white/10 p-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<span className="truncate">{wallpaper.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteWallpaper(wallpaper)}
|
||||||
|
className="text-red-500 hover:text-red-400"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
className="bi bi-trash"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z" />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Wallpaper Name (optional for URLs)"
|
||||||
|
value={newWallpaperName}
|
||||||
|
onChange={(e) => setNewWallpaperName(e.target.value)}
|
||||||
|
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Image URL"
|
||||||
|
value={newWallpaperUrl}
|
||||||
|
onChange={(e) => setNewWallpaperUrl(e.target.value)}
|
||||||
|
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAddWallpaper}
|
||||||
|
className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<label
|
||||||
|
htmlFor="file-upload"
|
||||||
|
className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center pt-5 pb-6">
|
||||||
|
<svg
|
||||||
|
className="w-8 h-8 mb-4 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 20 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="mb-2 text-sm text-gray-400">
|
||||||
|
<span className="font-semibold">Click to upload</span> or drag and drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">PNG, JPG, WEBP, etc.</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="file-upload"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
ref={fileInputRef}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeTab;
|
||||||
@@ -9,7 +9,7 @@ const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) =>
|
|||||||
<div className="absolute top-4 right-4">
|
<div className="absolute top-4 right-4">
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 active:scale-90 transition-all duration-200 ease-ios"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
|
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const EditButton: React.FC<EditButtonProps> = ({ isEditing, onClick }) => {
|
|||||||
<div className="absolute top-4 left-4">
|
<div className="absolute top-4 left-4">
|
||||||
<button
|
<button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 active:scale-90 transition-all duration-200 ease-ios"
|
||||||
style={{ fontSize: '12px' }}
|
style={{ fontSize: '12px' }}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
|
|||||||
181
components/services/ConfigurationService.ts
Normal file
181
components/services/ConfigurationService.ts
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { Config, Wallpaper } from '../../types';
|
||||||
|
import {
|
||||||
|
addWallpaperToChromeStorageLocal,
|
||||||
|
removeWallpaperFromChromeStorageLocal,
|
||||||
|
} from '../utils/StorageLocalManager';
|
||||||
|
|
||||||
|
const REQUIRED_LOCAL_STORAGE_KEYS = ['config', 'categories', 'userWallpapers', 'wallpaperState'] as const;
|
||||||
|
type RequiredLocalStorageKey = typeof REQUIRED_LOCAL_STORAGE_KEYS[number];
|
||||||
|
|
||||||
|
export const DEFAULT_CONFIG: Config = {
|
||||||
|
title: 'Vision Start',
|
||||||
|
currentWallpapers: ['Abstract'],
|
||||||
|
wallpaperFrequency: '1d',
|
||||||
|
wallpaperBlur: 0,
|
||||||
|
wallpaperBrightness: 100,
|
||||||
|
wallpaperOpacity: 100,
|
||||||
|
titleSize: 'medium',
|
||||||
|
alignment: 'middle',
|
||||||
|
horizontalAlignment: 'middle',
|
||||||
|
tileSize: 'medium',
|
||||||
|
clock: {
|
||||||
|
enabled: true,
|
||||||
|
size: 'medium',
|
||||||
|
font: 'Helvetica',
|
||||||
|
format: 'h:mm A',
|
||||||
|
},
|
||||||
|
serverWidget: {
|
||||||
|
enabled: false,
|
||||||
|
pingFrequency: 15,
|
||||||
|
servers: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeParse = (value: string | null): unknown => {
|
||||||
|
if (value === null) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toStorageString = (value: unknown): string =>
|
||||||
|
typeof value === 'string' ? value : JSON.stringify(value);
|
||||||
|
|
||||||
|
export const ConfigurationService = {
|
||||||
|
loadConfig(): Config {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('config');
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored);
|
||||||
|
return { ...DEFAULT_CONFIG, ...parsed };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing config from localStorage', error);
|
||||||
|
}
|
||||||
|
return { ...DEFAULT_CONFIG };
|
||||||
|
},
|
||||||
|
|
||||||
|
saveConfig(config: Config): void {
|
||||||
|
localStorage.setItem('config', JSON.stringify(config));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadUserWallpapers(): Wallpaper[] {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('userWallpapers');
|
||||||
|
if (stored) return JSON.parse(stored);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
|
||||||
|
saveUserWallpapers(wallpapers: Wallpaper[]): void {
|
||||||
|
localStorage.setItem('userWallpapers', JSON.stringify(wallpapers));
|
||||||
|
},
|
||||||
|
|
||||||
|
async addWallpaper(name: string, url: string): Promise<Wallpaper> {
|
||||||
|
const finalName = await addWallpaperToChromeStorageLocal(name, url);
|
||||||
|
return { name: finalName };
|
||||||
|
},
|
||||||
|
|
||||||
|
async addWallpaperFile(file: File): Promise<Wallpaper> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (file.size > 4 * 1024 * 1024) {
|
||||||
|
reject(new Error('File size exceeds 4MB. Please choose a smaller file.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async () => {
|
||||||
|
const base64 = reader.result as string;
|
||||||
|
if (base64.length > 4.5 * 1024 * 1024) {
|
||||||
|
reject(new Error('The uploaded image is too large. Please choose a smaller file.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const finalName = await addWallpaperToChromeStorageLocal(file.name, base64);
|
||||||
|
resolve({ name: finalName });
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteWallpaper(wallpaper: Wallpaper): Promise<void> {
|
||||||
|
await removeWallpaperFromChromeStorageLocal(wallpaper.name);
|
||||||
|
},
|
||||||
|
|
||||||
|
exportConfig(): void {
|
||||||
|
const exportPayload = {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
requiredLocalStorageKeys: [...REQUIRED_LOCAL_STORAGE_KEYS],
|
||||||
|
localStorage: REQUIRED_LOCAL_STORAGE_KEYS.reduce(
|
||||||
|
(acc, key) => {
|
||||||
|
acc[key] = safeParse(localStorage.getItem(key));
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<RequiredLocalStorageKey, unknown>,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(exportPayload, null, 2)], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `vision-start-config-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async importConfig(file: File): Promise<{ config: Config; userWallpapers: Wallpaper[] }> {
|
||||||
|
const fileContent = await file.text();
|
||||||
|
const parsed = JSON.parse(fileContent);
|
||||||
|
const localStorageData =
|
||||||
|
parsed?.localStorage && typeof parsed.localStorage === 'object'
|
||||||
|
? parsed.localStorage
|
||||||
|
: parsed;
|
||||||
|
|
||||||
|
if (!localStorageData || typeof localStorageData !== 'object') {
|
||||||
|
throw new Error('Invalid import file format.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let importedAny = false;
|
||||||
|
REQUIRED_LOCAL_STORAGE_KEYS.forEach((key) => {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(localStorageData, key)) {
|
||||||
|
const rawValue = (localStorageData as Record<string, unknown>)[key];
|
||||||
|
localStorage.setItem(key, toStorageString(rawValue));
|
||||||
|
importedAny = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!importedAny) {
|
||||||
|
throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const importedConfig = (localStorageData as Record<string, unknown>).config as Config;
|
||||||
|
const importedUserWallpapers = (localStorageData as Record<string, unknown>)
|
||||||
|
.userWallpapers as Wallpaper[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: importedConfig || { ...DEFAULT_CONFIG },
|
||||||
|
userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
resetWallpaperState(): void {
|
||||||
|
localStorage.setItem(
|
||||||
|
'wallpaperState',
|
||||||
|
JSON.stringify({
|
||||||
|
lastWallpaperChange: new Date().toISOString(),
|
||||||
|
currentIndex: 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--ease-ios: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
|
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
616
package-lock.json
generated
616
package-lock.json
generated
@@ -10,14 +10,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"react": "^19.1.0",
|
"preact": "^10.26.4"
|
||||||
"react-dom": "^19.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.10.1",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@types/react-dom": "^19.1.5",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
@@ -52,13 +52,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.27.1",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.27.1",
|
"@babel/helper-validator-identifier": "^7.28.5",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
"picocolors": "^1.1.1"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
@@ -108,14 +108,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "7.28.0",
|
"version": "7.29.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||||
"integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==",
|
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.28.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@babel/types": "^7.28.0",
|
"@babel/types": "^7.29.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"jsesc": "^3.0.2"
|
"jsesc": "^3.0.2"
|
||||||
@@ -124,6 +124,19 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-annotate-as-pure": {
|
||||||
|
"version": "7.27.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
|
||||||
|
"integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.27.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-compilation-targets": {
|
"node_modules/@babel/helper-compilation-targets": {
|
||||||
"version": "7.27.2",
|
"version": "7.27.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
|
||||||
@@ -152,14 +165,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-module-imports": {
|
"node_modules/@babel/helper-module-imports": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
||||||
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
|
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.27.1",
|
"@babel/traverse": "^7.28.6",
|
||||||
"@babel/types": "^7.27.1"
|
"@babel/types": "^7.28.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -184,9 +197,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-plugin-utils": {
|
"node_modules/@babel/helper-plugin-utils": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
|
||||||
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
|
"integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -204,9 +217,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -238,13 +251,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.28.0",
|
"version": "7.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
||||||
"integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==",
|
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.28.0"
|
"@babel/types": "^7.29.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -253,14 +266,14 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-react-jsx-self": {
|
"node_modules/@babel/plugin-syntax-jsx": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
|
||||||
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
|
"integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -269,14 +282,34 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/plugin-transform-react-jsx-source": {
|
"node_modules/@babel/plugin-transform-react-jsx": {
|
||||||
"version": "7.27.1",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz",
|
||||||
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
|
"integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-annotate-as-pure": "^7.27.3",
|
||||||
|
"@babel/helper-module-imports": "^7.28.6",
|
||||||
|
"@babel/helper-plugin-utils": "^7.28.6",
|
||||||
|
"@babel/plugin-syntax-jsx": "^7.28.6",
|
||||||
|
"@babel/types": "^7.28.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/plugin-transform-react-jsx-development": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.27.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -295,33 +328,33 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.27.2",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||||
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
|
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/parser": "^7.27.2",
|
"@babel/parser": "^7.28.6",
|
||||||
"@babel/types": "^7.27.1"
|
"@babel/types": "^7.28.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/traverse": {
|
"node_modules/@babel/traverse": {
|
||||||
"version": "7.28.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
||||||
"integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==",
|
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.28.0",
|
"@babel/generator": "^7.29.0",
|
||||||
"@babel/helper-globals": "^7.28.0",
|
"@babel/helper-globals": "^7.28.0",
|
||||||
"@babel/parser": "^7.28.0",
|
"@babel/parser": "^7.29.0",
|
||||||
"@babel/template": "^7.27.2",
|
"@babel/template": "^7.28.6",
|
||||||
"@babel/types": "^7.28.0",
|
"@babel/types": "^7.29.0",
|
||||||
"debug": "^4.3.1"
|
"debug": "^4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -329,14 +362,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.28.1",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||||
"integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
|
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
"@babel/helper-validator-identifier": "^7.27.1"
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -807,9 +840,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
@@ -822,13 +855,121 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@preact/preset-vite": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "2.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@preact/preset-vite/-/preset-vite-2.10.5.tgz",
|
||||||
"integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
|
"integrity": "sha512-p0vJpxiVO7KWWazWny3LUZ+saXyZKWv6Ju0bYMWNJRp2YveufRPgSUB1C4MTqGJfz07EehMgfN+AJNwQy+w6Iw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/plugin-transform-react-jsx": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-react-jsx-development": "^7.27.1",
|
||||||
|
"@prefresh/vite": "^2.4.11",
|
||||||
|
"@rollup/pluginutils": "^5.0.0",
|
||||||
|
"babel-plugin-transform-hook-names": "^1.0.2",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"magic-string": "^0.30.21",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"vite-prerender-plugin": "^0.5.8",
|
||||||
|
"zimmerframe": "^1.1.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "7.x",
|
||||||
|
"vite": "2.x || 3.x || 4.x || 5.x || 6.x || 7.x || 8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prefresh/babel-plugin": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prefresh/babel-plugin/-/babel-plugin-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-57LX2SHs4BX2s1IwCjNzTE2OJeEepRCNf1VTEpbNcUyHfMO68eeOWGDIt4ob9aYlW6PEWZ1SuwNikuoIXANDtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@prefresh/core": {
|
||||||
|
"version": "1.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prefresh/core/-/core-1.5.9.tgz",
|
||||||
|
"integrity": "sha512-IKBKCPaz34OFVC+adiQ2qaTF5qdztO2/4ZPf4KsRTgjKosWqxVXmEbxCiUydYZRY8GVie+DQlKzQr9gt6HQ+EQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": "^10.0.0 || ^11.0.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prefresh/utils": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prefresh/utils/-/utils-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@prefresh/vite": {
|
||||||
|
"version": "2.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prefresh/vite/-/vite-2.4.12.tgz",
|
||||||
|
"integrity": "sha512-FY1fzXpUjiuosznMV0YM7XAOPZjB5FIdWS0W24+XnlxYkt9hNAwwsiKYn+cuTEoMtD/ZVazS5QVssBr9YhpCQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.22.1",
|
||||||
|
"@prefresh/babel-plugin": "^0.5.2",
|
||||||
|
"@prefresh/core": "^1.5.0",
|
||||||
|
"@prefresh/utils": "^1.2.0",
|
||||||
|
"@rollup/pluginutils": "^4.2.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"preact": "^10.4.0 || ^11.0.0-0",
|
||||||
|
"vite": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prefresh/vite/node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"estree-walker": "^2.0.1",
|
||||||
|
"picomatch": "^2.2.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@prefresh/vite/node_modules/picomatch": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/pluginutils": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/estree": "^1.0.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"picomatch": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.45.0",
|
"version": "4.45.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.0.tgz",
|
||||||
@@ -1365,51 +1506,6 @@
|
|||||||
"vite": "^5.2.0 || ^6 || ^7"
|
"vite": "^5.2.0 || ^6 || ^7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/babel__core": {
|
|
||||||
"version": "7.20.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
|
||||||
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.20.7",
|
|
||||||
"@babel/types": "^7.20.7",
|
|
||||||
"@types/babel__generator": "*",
|
|
||||||
"@types/babel__template": "*",
|
|
||||||
"@types/babel__traverse": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__generator": {
|
|
||||||
"version": "7.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
|
|
||||||
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__template": {
|
|
||||||
"version": "7.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
|
|
||||||
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/parser": "^7.1.0",
|
|
||||||
"@babel/types": "^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__traverse": {
|
|
||||||
"version": "7.20.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
|
|
||||||
"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/types": "^7.20.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -1427,13 +1523,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.8",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "19.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/use-sync-external-store": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
@@ -1442,27 +1548,6 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/core": "^7.28.0",
|
|
||||||
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
|
|
||||||
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
|
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.27",
|
|
||||||
"@types/babel__core": "^7.20.5",
|
|
||||||
"react-refresh": "^0.17.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.18.0 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||||
@@ -1501,6 +1586,23 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-transform-hook-names": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": "^7.12.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.25.1",
|
"version": "4.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz",
|
||||||
@@ -1580,17 +1682,47 @@
|
|||||||
"tiny-invariant": "^1.0.6"
|
"tiny-invariant": "^1.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1614,6 +1746,65 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.187",
|
"version": "1.5.187",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.187.tgz",
|
||||||
@@ -1634,6 +1825,19 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.6",
|
"version": "0.25.6",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz",
|
||||||
@@ -1685,6 +1889,13 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.4.6",
|
"version": "6.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
|
||||||
@@ -1743,6 +1954,16 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/he": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"he": "bin/he"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jiti": {
|
"node_modules/jiti": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
@@ -1785,6 +2006,13 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/kolorist": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.1",
|
"version": "1.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||||
@@ -2031,12 +2259,12 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
@@ -2100,6 +2328,17 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-html-parser": {
|
||||||
|
"version": "6.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
|
||||||
|
"integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"he": "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-releases": {
|
"node_modules/node-releases": {
|
||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
@@ -2117,6 +2356,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -2170,6 +2422,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz",
|
||||||
|
"integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/raf-schd": {
|
"node_modules/raf-schd": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||||
@@ -2181,6 +2443,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -2190,6 +2453,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -2220,16 +2484,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-refresh": {
|
|
||||||
"version": "0.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
|
||||||
"integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
@@ -2279,7 +2533,8 @@
|
|||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
@@ -2291,6 +2546,26 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-code-frame": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-code-frame/-/simple-code-frame-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kolorist": "^1.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/source-map": {
|
||||||
|
"version": "0.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||||
|
"integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
@@ -2300,6 +2575,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stack-trace": {
|
||||||
|
"version": "1.0.0-pre2",
|
||||||
|
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-1.0.0-pre2.tgz",
|
||||||
|
"integrity": "sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||||
@@ -2489,6 +2774,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-prerender-plugin": {
|
||||||
|
"version": "0.5.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz",
|
||||||
|
"integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"magic-string": "0.x >= 0.26.0",
|
||||||
|
"node-html-parser": "^6.1.12",
|
||||||
|
"simple-code-frame": "^1.3.0",
|
||||||
|
"source-map": "^0.7.4",
|
||||||
|
"stack-trace": "^1.0.0-pre2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vite": "5.x || 6.x || 7.x || 8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
@@ -2497,6 +2800,13 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zimmerframe": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
"react": "^19.1.0",
|
"preact": "^10.26.4"
|
||||||
"react-dom": "^19.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.10.1",
|
||||||
"@tailwindcss/postcss": "^4.1.11",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
"@types/react-dom": "^19.1.5",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ set -e
|
|||||||
# Configuration
|
# Configuration
|
||||||
FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}"
|
FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}"
|
||||||
API_KEY="${virustotal_apikey}"
|
API_KEY="${virustotal_apikey}"
|
||||||
BASE_URL="https://www.virustotal.com/vtapi/v2"
|
BASE_URL="https://www.virustotal.com/api/v3"
|
||||||
|
|
||||||
# Check if API key is set
|
# Check if API key is set
|
||||||
if [ -z "$API_KEY" ]; then
|
if [ -z "$API_KEY" ]; then
|
||||||
@@ -38,12 +38,12 @@ echo "Uploading $FILE_PATH to VirusTotal for analysis..."
|
|||||||
|
|
||||||
# Upload file to VirusTotal
|
# Upload file to VirusTotal
|
||||||
UPLOAD_RESPONSE=$(curl -s -X POST \
|
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||||
-F "apikey=$API_KEY" \
|
-H "x-apikey: $API_KEY" \
|
||||||
-F "file=@$FILE_PATH" \
|
-F "file=@$FILE_PATH" \
|
||||||
"$BASE_URL/file/scan")
|
"$BASE_URL/files")
|
||||||
|
|
||||||
# Extract scan_id from response
|
# Extract scan_id from response
|
||||||
SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.scan_id')
|
SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.data.id')
|
||||||
|
|
||||||
if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then
|
if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then
|
||||||
echo "Error: Failed to upload file or get scan ID"
|
echo "Error: Failed to upload file or get scan ID"
|
||||||
@@ -55,7 +55,7 @@ echo "File uploaded successfully. Scan ID: $SCAN_ID"
|
|||||||
echo "Waiting for analysis to complete..."
|
echo "Waiting for analysis to complete..."
|
||||||
|
|
||||||
# Wait for analysis to complete and get results
|
# Wait for analysis to complete and get results
|
||||||
MAX_ATTEMPTS=30
|
MAX_ATTEMPTS=60
|
||||||
ATTEMPT=0
|
ATTEMPT=0
|
||||||
SLEEP_INTERVAL=10
|
SLEEP_INTERVAL=10
|
||||||
|
|
||||||
@@ -63,40 +63,40 @@ while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
|||||||
echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
|
echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
|
||||||
|
|
||||||
# Get scan report
|
# Get scan report
|
||||||
REPORT_RESPONSE=$(curl -s -X POST \
|
REPORT_RESPONSE=$(curl -s -X GET \
|
||||||
-d "apikey=$API_KEY" \
|
-H "x-apikey: $API_KEY" \
|
||||||
-d "resource=$SCAN_ID" \
|
"$BASE_URL/analyses/$SCAN_ID")
|
||||||
"$BASE_URL/file/report")
|
|
||||||
|
|
||||||
# Check if analysis is complete
|
# Check if analysis is complete
|
||||||
RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.response_code')
|
RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.status')
|
||||||
|
|
||||||
if [ "$RESPONSE_CODE" == "1" ]; then
|
if [ "$RESPONSE_CODE" == "completed" ]; then
|
||||||
# Analysis complete
|
# Analysis complete
|
||||||
echo "Analysis completed!"
|
echo "Analysis completed!"
|
||||||
|
|
||||||
# Extract results
|
# Extract results
|
||||||
POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.positives')
|
POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.stats.malicious')
|
||||||
TOTAL=$(echo "$REPORT_RESPONSE" | jq -r '.total')
|
SUSPICIOUS=$(echo "$REPORT_RESPONSE" | jq -r '.data.attributes.stats.suspicious')
|
||||||
PERMALINK=$(echo "$REPORT_RESPONSE" | jq -r '.permalink')
|
# The v3 analyses object has no 'total' field — compute it by summing all stat categories
|
||||||
|
TOTAL=$(echo "$REPORT_RESPONSE" | jq '[.data.attributes.stats | to_entries[].value] | add')
|
||||||
|
ANALYSIS_ID=$(echo "$REPORT_RESPONSE" | jq -r '.data.id')
|
||||||
|
PERMALINK="https://www.virustotal.com/gui/file-analysis/${ANALYSIS_ID}"
|
||||||
|
|
||||||
echo "Analysis URL: $PERMALINK"
|
echo "Analysis URL: $PERMALINK"
|
||||||
echo "Detection ratio: $POSITIVES/$TOTAL"
|
echo "Detection ratio: $POSITIVES/$TOTAL"
|
||||||
|
|
||||||
# Check if file is safe
|
# Check if file is safe
|
||||||
if [ "$POSITIVES" -eq 0 ]; then
|
if [ "$POSITIVES" -eq 0 ] && [ "$SUSPICIOUS" -eq 0 ]; then
|
||||||
echo "✅ File is clean (no threats detected)"
|
echo "✅ File is clean (no threats detected)"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echo "❌ File contains threats ($POSITIVES detections out of $TOTAL scanners)"
|
echo "❌ File flagged: $POSITIVES malicious, $SUSPICIOUS suspicious (out of $TOTAL scanners)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
elif [ "$RESPONSE_CODE" == "0" ]; then
|
elif [ "$RESPONSE_CODE" == "queued" ]; then
|
||||||
# File not found or analysis not complete yet
|
|
||||||
echo "Analysis still in progress..."
|
|
||||||
elif [ "$RESPONSE_CODE" == "-2" ]; then
|
|
||||||
# Still queued for analysis
|
|
||||||
echo "File still queued for analysis..."
|
echo "File still queued for analysis..."
|
||||||
|
elif [ "$RESPONSE_CODE" == "in-progress" ]; then
|
||||||
|
echo "Analysis still in progress..."
|
||||||
else
|
else
|
||||||
echo "Unexpected response code: $RESPONSE_CODE"
|
echo "Unexpected response code: $RESPONSE_CODE"
|
||||||
echo "Response: $REPORT_RESPONSE"
|
echo "Response: $REPORT_RESPONSE"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import preact from '@preact/preset-vite'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [preact(), tailwindcss()],
|
||||||
build: {
|
build: {
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
Reference in New Issue
Block a user