Compare commits
	
		
			10 Commits
		
	
	
		
			latest
			...
			c60cb24dd4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c60cb24dd4 | |||
| 2f3949c2e3 | |||
| 9b818b05f9 | |||
| 008d2321e5 | |||
| 3aff7ffed6 | |||
| 9e80818fc5 | |||
| 140119cb99 | |||
| af7b778561 | |||
| 2849ed3bb2 | |||
| f1c1b0c6c6 | 
@@ -1,29 +1,18 @@
 | 
				
			|||||||
name: Check scripts syntax
 | 
					name: Build and Release
 | 
				
			||||||
on: [push]
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches:
 | 
				
			||||||
 | 
					      - main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
jobs:
 | 
					jobs:
 | 
				
			||||||
  build-release:
 | 
					  build:
 | 
				
			||||||
 | 
					    if: gitea.event_name == 'push'
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Check out repository code
 | 
					      - name: Check out repository code
 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
      - name: Setup zip
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          sudo apt-get install zip -y
 | 
					 | 
				
			||||||
      - name: Install JS dependencies
 | 
					      - name: Install JS dependencies
 | 
				
			||||||
        run: |
 | 
					        run: npm install
 | 
				
			||||||
          npm install
 | 
					 | 
				
			||||||
      - name: Run scripts
 | 
					 | 
				
			||||||
        run: |
 | 
					 | 
				
			||||||
          bash download-icons.sh
 | 
					 | 
				
			||||||
      - name: Run build
 | 
					      - name: Run build
 | 
				
			||||||
        run: |
 | 
					        run: npm run build
 | 
				
			||||||
          npm run build
 | 
					 | 
				
			||||||
          zip -r vision-start.zip dist
 | 
					 | 
				
			||||||
      - name: Release zip
 | 
					 | 
				
			||||||
        uses: akkuman/gitea-release-action@v1
 | 
					 | 
				
			||||||
        with:
 | 
					 | 
				
			||||||
          name: latest
 | 
					 | 
				
			||||||
          tag_name: latest
 | 
					 | 
				
			||||||
          files: |-
 | 
					 | 
				
			||||||
            vision-start.zip
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										90
									
								
								.gitea/workflows/release.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								.gitea/workflows/release.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					name: Build and Release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    tags:
 | 
				
			||||||
 | 
					      - v*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  build:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    outputs:
 | 
				
			||||||
 | 
					      zip-file: vision-start-${{ gitea.ref_name }}.zip
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Check out repository code
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					      - name: Setup required tools
 | 
				
			||||||
 | 
					        run: sudo apt-get install zip jq curl -y
 | 
				
			||||||
 | 
					      - name: Install JS dependencies
 | 
				
			||||||
 | 
					        run: npm install
 | 
				
			||||||
 | 
					      - name: Run build
 | 
				
			||||||
 | 
					        run: npm run build
 | 
				
			||||||
 | 
					      - name: Prepare release
 | 
				
			||||||
 | 
					        run: | 
 | 
				
			||||||
 | 
					          bash scripts/prepare_release.sh
 | 
				
			||||||
 | 
					          mv dist vision-start/
 | 
				
			||||||
 | 
					          mv manifest.json vision-start/
 | 
				
			||||||
 | 
					      - name: Create zip archive
 | 
				
			||||||
 | 
					        run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
 | 
				
			||||||
 | 
					      - name: Upload artifact
 | 
				
			||||||
 | 
					        uses: actions/upload-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: release-zip
 | 
				
			||||||
 | 
					          path: vision-start-${{ gitea.ref_name }}.zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  virus-total-check:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    needs: build
 | 
				
			||||||
 | 
					    outputs:
 | 
				
			||||||
 | 
					      analysis-url: ${{ steps.vt-check.outputs.analysis-url }}
 | 
				
			||||||
 | 
					      detection-ratio: ${{ steps.vt-check.outputs.detection-ratio }}
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Check out repository code
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					      - name: Setup required tools
 | 
				
			||||||
 | 
					        run: sudo apt-get install jq curl -y
 | 
				
			||||||
 | 
					      - name: Download artifact
 | 
				
			||||||
 | 
					        uses: actions/download-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: release-zip
 | 
				
			||||||
 | 
					      - name: Run VirusTotal check
 | 
				
			||||||
 | 
					        id: vt-check
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          virustotal_apikey: ${{ secrets.VIRUSTOTAL_APIKEY }}
 | 
				
			||||||
 | 
					          VIRUS_TOTAL_FILE: vision-start-${{ gitea.ref_name }}.zip
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          # Run the VirusTotal check script and capture output
 | 
				
			||||||
 | 
					          bash scripts/check_virustotal.sh > vt_output.txt 2>&1
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Extract analysis URL and detection ratio from output
 | 
				
			||||||
 | 
					          ANALYSIS_URL=$(grep "Analysis URL:" vt_output.txt | cut -d' ' -f3- || echo "Not available")
 | 
				
			||||||
 | 
					          DETECTION_RATIO=$(grep "Detection ratio:" vt_output.txt | cut -d' ' -f3- || echo "Not available")
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Set outputs for next job
 | 
				
			||||||
 | 
					          echo "analysis-url=$ANALYSIS_URL" >> $GITEA_OUTPUT
 | 
				
			||||||
 | 
					          echo "detection-ratio=$DETECTION_RATIO" >> $GITEA_OUTPUT
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          # Display the full output
 | 
				
			||||||
 | 
					          cat vt_output.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  release:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    needs: [build, virus-total-check]
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Check out repository code
 | 
				
			||||||
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					      - name: Download artifact
 | 
				
			||||||
 | 
					        uses: actions/download-artifact@v3
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          name: release-zip
 | 
				
			||||||
 | 
					      - name: Release zip
 | 
				
			||||||
 | 
					        uses: akkuman/gitea-release-action@v1
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          body: |
 | 
				
			||||||
 | 
					            This is the release for version ${{ gitea.ref_name }}.
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            **Virus Total Analysis URL:** ${{ needs.virus-total-check.outputs.analysis-url }}
 | 
				
			||||||
 | 
					            **Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }}
 | 
				
			||||||
 | 
					          name: ${{ gitea.ref_name }}
 | 
				
			||||||
 | 
					          tag_name: ${{ gitea.ref_name }}
 | 
				
			||||||
 | 
					          files: vision-start-${{ gitea.ref_name }}.zip
 | 
				
			||||||
							
								
								
									
										96
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								App.tsx
									
									
									
									
									
								
							@@ -1,21 +1,20 @@
 | 
				
			|||||||
import React, { useState, useEffect } from 'react';
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
import ConfigurationModal from './components/ConfigurationModal';
 | 
					import ConfigurationModal from './components/ConfigurationModal';
 | 
				
			||||||
import ServerWidget from './components/ServerWidget';
 | 
					import ServerWidget from './components/ServerWidget';
 | 
				
			||||||
import { DEFAULT_CATEGORIES } from './constants';
 | 
					import { DEFAULT_CATEGORIES } from './constants';
 | 
				
			||||||
import { Category, Website, Wallpaper, Config } from './types';
 | 
					import { Category, Website, Config } from './types';
 | 
				
			||||||
import WebsiteEditModal from './components/WebsiteEditModal';
 | 
					import WebsiteEditModal from './components/WebsiteEditModal';
 | 
				
			||||||
import CategoryEditModal from './components/CategoryEditModal';
 | 
					import CategoryEditModal from './components/CategoryEditModal';
 | 
				
			||||||
import Header from './components/layout/Header';
 | 
					import Header from './components/layout/Header';
 | 
				
			||||||
import EditButton from './components/layout/EditButton';
 | 
					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 { baseWallpapers } from './components/utils/baseWallpapers';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const defaultConfig: Config = {
 | 
					const defaultConfig: Config = {
 | 
				
			||||||
  title: 'Vision Start',
 | 
					  title: 'Vision Start',
 | 
				
			||||||
  subtitle: 'Your personal portal to the web.',
 | 
					  subtitle: 'Your personal portal to the web.',
 | 
				
			||||||
  backgroundUrls: ['https://i.imgur.com/C6ynAtX.jpeg'],
 | 
					  currentWallpapers: ['Abstract'],
 | 
				
			||||||
  wallpaperFrequency: '1d',
 | 
					  wallpaperFrequency: '1d',
 | 
				
			||||||
  wallpaperBlur: 0,
 | 
					  wallpaperBlur: 0,
 | 
				
			||||||
  wallpaperBrightness: 100,
 | 
					  wallpaperBrightness: 100,
 | 
				
			||||||
@@ -60,9 +59,6 @@ const App: React.FC = () => {
 | 
				
			|||||||
      const storedConfig = localStorage.getItem('config');
 | 
					      const storedConfig = localStorage.getItem('config');
 | 
				
			||||||
      if (storedConfig) {
 | 
					      if (storedConfig) {
 | 
				
			||||||
        const parsedConfig = JSON.parse(storedConfig);
 | 
					        const parsedConfig = JSON.parse(storedConfig);
 | 
				
			||||||
        if (!parsedConfig.backgroundUrls) {
 | 
					 | 
				
			||||||
          parsedConfig.backgroundUrls = [parsedConfig.backgroundUrl].filter(Boolean);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return { ...defaultConfig, ...parsedConfig };
 | 
					        return { ...defaultConfig, ...parsedConfig };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch (error) {
 | 
				
			||||||
@@ -70,69 +66,20 @@ const App: React.FC = () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    return { ...defaultConfig };
 | 
					    return { ...defaultConfig };
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>(() => {
 | 
					 | 
				
			||||||
    const storedUserWallpapers = localStorage.getItem('userWallpapers');
 | 
					 | 
				
			||||||
    return storedUserWallpapers ? JSON.parse(storedUserWallpapers) : [];
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  const [currentWallpaper, setCurrentWallpaper] = useState<string>('');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const allWallpapers = [...baseWallpapers, ...userWallpapers];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const getFrequencyInMs = (frequency: string) => {
 | 
					 | 
				
			||||||
      const value = parseInt(frequency.slice(0, -1));
 | 
					 | 
				
			||||||
      const unit = frequency.slice(-1);
 | 
					 | 
				
			||||||
      if (unit === 'h') return value * 60 * 60 * 1000;
 | 
					 | 
				
			||||||
      if (unit === 'd') return value * 24 * 60 * 60 * 1000;
 | 
					 | 
				
			||||||
      return 24 * 60 * 60 * 1000; // Default to 1 day
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const wallpaperState = JSON.parse(localStorage.getItem('wallpaperState') || '{}');
 | 
					 | 
				
			||||||
    const lastChanged = wallpaperState.lastChanged ? new Date(wallpaperState.lastChanged).getTime() : 0;
 | 
					 | 
				
			||||||
    const frequency = getFrequencyInMs(config.wallpaperFrequency);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateWallpaper = () => {
 | 
					 | 
				
			||||||
      const availableWallpapers = allWallpapers.filter(w => config.backgroundUrls.includes(w.url || w.base64));
 | 
					 | 
				
			||||||
      if (availableWallpapers.length > 0) {
 | 
					 | 
				
			||||||
        const currentIndex = availableWallpapers.findIndex(w => (w.url || w.base64) === wallpaperState.current);
 | 
					 | 
				
			||||||
        const nextIndex = (currentIndex + 1) % availableWallpapers.length;
 | 
					 | 
				
			||||||
        const newWallpaper = availableWallpapers[nextIndex];
 | 
					 | 
				
			||||||
        const newWallpaperUrl = newWallpaper.url || newWallpaper.base64;
 | 
					 | 
				
			||||||
        setCurrentWallpaper(newWallpaperUrl || '');
 | 
					 | 
				
			||||||
        localStorage.setItem('wallpaperState', JSON.stringify({ current: newWallpaper.name, lastChanged: new Date().toISOString() }));
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        setCurrentWallpaper('');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (Date.now() - lastChanged > frequency) {
 | 
					 | 
				
			||||||
      updateWallpaper();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      const currentWallpaperName = wallpaperState.current;
 | 
					 | 
				
			||||||
      const wallpaper = allWallpapers.find(w => w.name === currentWallpaperName);
 | 
					 | 
				
			||||||
      if (wallpaper) {
 | 
					 | 
				
			||||||
        setCurrentWallpaper(wallpaper.url || wallpaper.base64 || '');
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        const firstWallpaperUrl = config.backgroundUrls[0] || '';
 | 
					 | 
				
			||||||
        const firstWallpaper = allWallpapers.find(w => (w.url || w.base64) === firstWallpaperUrl);
 | 
					 | 
				
			||||||
        setCurrentWallpaper(firstWallpaperUrl);
 | 
					 | 
				
			||||||
        if (firstWallpaper) {
 | 
					 | 
				
			||||||
          localStorage.setItem('wallpaperState', JSON.stringify({ current: firstWallpaper.name, lastChanged: new Date().toISOString() }));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }, [config.backgroundUrls, config.wallpaperFrequency, allWallpapers]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  useEffect(() => {
 | 
					 | 
				
			||||||
    localStorage.setItem('categories', JSON.stringify(categories));
 | 
					 | 
				
			||||||
    localStorage.setItem('config', JSON.stringify(config));
 | 
					    localStorage.setItem('config', JSON.stringify(config));
 | 
				
			||||||
  }, [categories, config]);
 | 
					  }, [config]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSaveConfig = (newConfig: any) => {
 | 
					  const handleSaveConfig = (newConfig: Config) => {
 | 
				
			||||||
    setConfig(newConfig);
 | 
					    setConfig(newConfig);
 | 
				
			||||||
    setIsConfigModalOpen(false);
 | 
					    setIsConfigModalOpen(false);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleWallpaperChange = (newConfig: Partial<Config>) => {
 | 
				
			||||||
 | 
					    setConfig(prev => ({ ...prev, ...newConfig }));
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSaveWebsite = (website: Partial<Website>) => {
 | 
					  const handleSaveWebsite = (website: Partial<Website>) => {
 | 
				
			||||||
    if (editingWebsite) {
 | 
					    if (editingWebsite) {
 | 
				
			||||||
      const newCategories = categories.map(category => ({
 | 
					      const newCategories = categories.map(category => ({
 | 
				
			||||||
@@ -200,7 +147,6 @@ const App: React.FC = () => {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleMoveWebsite = (website: Website, direction: 'left' | 'right') => {
 | 
					  const handleMoveWebsite = (website: Website, direction: 'left' | 'right') => {
 | 
				
			||||||
    const categoryIndex = categories.findIndex(c => c.id === website.categoryId);
 | 
					 | 
				
			||||||
    if (categoryIndex === -1) return;
 | 
					    if (categoryIndex === -1) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const category = categories[categoryIndex];
 | 
					    const category = categories[categoryIndex];
 | 
				
			||||||
@@ -258,14 +204,13 @@ const App: React.FC = () => {
 | 
				
			|||||||
    <main
 | 
					    <main
 | 
				
			||||||
      className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
 | 
					      className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <div
 | 
					      <Wallpaper
 | 
				
			||||||
        className="fixed inset-0 w-full h-full bg-cover bg-center bg-fixed -z-10"
 | 
					        wallpaperNames={config.currentWallpapers}
 | 
				
			||||||
        style={{
 | 
					        blur={config.wallpaperBlur}
 | 
				
			||||||
          backgroundImage: `url('${currentWallpaper}')`,
 | 
					        brightness={config.wallpaperBrightness}
 | 
				
			||||||
          filter: `blur(${config.wallpaperBlur}px) brightness(${config.wallpaperBrightness}%)`,
 | 
					        opacity={config.wallpaperOpacity}
 | 
				
			||||||
          opacity: `${config.wallpaperOpacity}%`,
 | 
					        wallpaperFrequency={config.wallpaperFrequency}
 | 
				
			||||||
        }}
 | 
					      />
 | 
				
			||||||
      ></div>
 | 
					 | 
				
			||||||
      <EditButton isEditing={isEditing} onClick={() => setIsEditing(!isEditing)} />
 | 
					      <EditButton isEditing={isEditing} onClick={() => setIsEditing(!isEditing)} />
 | 
				
			||||||
      <ConfigurationButton onClick={() => setIsConfigModalOpen(true)} />
 | 
					      <ConfigurationButton onClick={() => setIsConfigModalOpen(true)} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -304,11 +249,7 @@ const App: React.FC = () => {
 | 
				
			|||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {config.serverWidget.enabled && (
 | 
					      {config.serverWidget.enabled && <ServerWidget config={config} />}
 | 
				
			||||||
        <div className="absolute bottom-4 right-4">
 | 
					 | 
				
			||||||
          <ServerWidget config={config} />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      {(editingWebsite || addingWebsite) && (
 | 
					      {(editingWebsite || addingWebsite) && (
 | 
				
			||||||
        <WebsiteEditModal
 | 
					        <WebsiteEditModal
 | 
				
			||||||
@@ -341,6 +282,7 @@ const App: React.FC = () => {
 | 
				
			|||||||
          currentConfig={config}
 | 
					          currentConfig={config}
 | 
				
			||||||
          onClose={() => setIsConfigModalOpen(false)}
 | 
					          onClose={() => setIsConfigModalOpen(false)}
 | 
				
			||||||
          onSave={handleSaveConfig}
 | 
					          onSave={handleSaveConfig}
 | 
				
			||||||
 | 
					          onWallpaperChange={handleWallpaperChange}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										94
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								README.md
									
									
									
									
									
								
							@@ -1,22 +1,90 @@
 | 
				
			|||||||
# Vision Start
 | 
					# Vision Start
 | 
				
			||||||
#### Small startpage 
 | 
					#### A glassmorphism-looking like, modern and customizable startpage built with React.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Predefined themes
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Abstract
 | 
					
 | 
				
			||||||
2. Aurora (Vista vibes)
 | 
					
 | 
				
			||||||
3. Mountain
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Run Locally
 | 
					## Installing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Vision Start is not yet available on Chrome Web Store, but it can be installed manually:
 | 
				
			||||||
 | 
					1. Go to https://git.ivanch.me/ivanch/vision-start/releases/latest
 | 
				
			||||||
 | 
					2. Download the latest `vision-start-[version].zip` file
 | 
				
			||||||
 | 
					3. Extract the zip file, you will have a `vision-start` folder
 | 
				
			||||||
 | 
					4. Go to chrome://extensions/
 | 
				
			||||||
 | 
					5. Enable "Developer mode" in the top right corner
 | 
				
			||||||
 | 
					6. Click on "Load unpacked" and select the `vision-start` folder you extracted in step 3
 | 
				
			||||||
 | 
					7. The extension should now be installed! Just open a new tab to see it in action.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **Customizable Website Tiles:** Add, edit, and organize your favorite websites for quick access.
 | 
				
			||||||
 | 
					* **Elegant Clock:** A clock because all startpages have one.
 | 
				
			||||||
 | 
					* **Server Status Widgets:** Monitor the status of services directly from the startpage.
 | 
				
			||||||
 | 
					* **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.
 | 
				
			||||||
 | 
					* **Future**: a long to do list :(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Backgrounds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It comes with a selection of some nice pre-defined backgrounds: **Abstract**, **Abstract Red**, **Beach**, **Dark**, **Mountain**, **Waves**.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					You can also upload your own images on it (or fetch it from the web).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Running Locally
 | 
				
			||||||
 | 
					
 | 
				
			||||||
**Prerequisites:** Node.js
 | 
					**Prerequisites:** Node.js
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Install dependencies:
 | 
					1.  Clone the repository:
 | 
				
			||||||
   `npm install`
 | 
					```bash
 | 
				
			||||||
2. Run the app:
 | 
					git clone https://gitea.com/ivan/vision-start.git
 | 
				
			||||||
   `npm run dev`
 | 
					cd vision-start
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					2.  Install dependencies:
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm install
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					3.  Run the development server:
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					npm run dev
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## to-do
 | 
					## To-do
 | 
				
			||||||
* [] Multiple wallpapers
 | 
					
 | 
				
			||||||
 | 
					* [x] Multiple Wallpapers
 | 
				
			||||||
* [x] Remake icons
 | 
					* [x] Remake icons
 | 
				
			||||||
* [] Increase offline compatibility
 | 
					* [/] Increase offline compatibility (might not be possible)
 | 
				
			||||||
 | 
					  - [x] Use chrome.storage.local for user wallpapers -- this one is
 | 
				
			||||||
 | 
					  - [ ] Use chrome.storage.local for some logos -- a bit hard
 | 
				
			||||||
 | 
					    - Some logos have CORS enabled, we can add `"<all_urls>"` to the manifest.json file and cache them on storage local
 | 
				
			||||||
 | 
					* Dynamic Weather Widget
 | 
				
			||||||
 | 
					  * A box with information about the current weather, with manual entry on the location
 | 
				
			||||||
 | 
					  * Display current temperature, weather condition (e.g., "Sunny," "Cloudy"), and a corresponding icon
 | 
				
			||||||
 | 
					  * Optionally, show a 3-day forecast when clicked or hovered
 | 
				
			||||||
 | 
					* Search Bar Widget
 | 
				
			||||||
 | 
					  * Positioned to the right or left side of the clock, display a nice search bar
 | 
				
			||||||
 | 
					  * Behaviour:
 | 
				
			||||||
 | 
					    * When not in focus, it could be highly transparent with just a faint border and a search icon.
 | 
				
			||||||
 | 
					    * When clicked, it would smoothly expand and become slightly more opaque, with a soft glow around the border (similar to the existing ones)
 | 
				
			||||||
 | 
					  * Config to allow changing the default search engine
 | 
				
			||||||
 | 
					* Draggable & Resizable Grid System
 | 
				
			||||||
 | 
					  * Allow users to drag and drop all widgets (Clock, Website Tiles, Weather, Title, etc.) into any position on a grid
 | 
				
			||||||
 | 
					* Notes / Scratchpad Widget
 | 
				
			||||||
 | 
					  * A simple text area that saves its content to local storage automatically.
 | 
				
			||||||
 | 
					  * Maybe some extra formatting (bold, italic, increase font size, etc).
 | 
				
			||||||
 | 
					* Theme-ing
 | 
				
			||||||
 | 
					  * A Light/Dark Mode toggle
 | 
				
			||||||
 | 
					  * Custom Accent Colors
 | 
				
			||||||
 | 
					    * Selection of 6-8 accent colors that are guaranteed to look good with both Light and Dark themes
 | 
				
			||||||
 | 
					    * Define CSS variables for the accent color
 | 
				
			||||||
 | 
					  * Dynamic Wallpaper-Based Theming
 | 
				
			||||||
 | 
					    * Automatically adapt the UI's accent color to match the current wallpaper
 | 
				
			||||||
 | 
					  * Minimal feel toggle
 | 
				
			||||||
 | 
					    * Disable title & subtitle and search widget
 | 
				
			||||||
 | 
					    * Tiles become small stylish lines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					From a technical side:
 | 
				
			||||||
 | 
					* Refactor everything :(
 | 
				
			||||||
 | 
					* Add small nginx demo (with docker)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useState } from 'react';
 | 
					import { useState } from 'react';
 | 
				
			||||||
import { Category } from '../types';
 | 
					import { Category } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CategoryEditModalProps {
 | 
					interface CategoryEditModalProps {
 | 
				
			||||||
@@ -12,10 +12,6 @@ interface CategoryEditModalProps {
 | 
				
			|||||||
const CategoryEditModal: React.FC<CategoryEditModalProps> = ({ category, edit, onClose, onSave, onDelete }) => {
 | 
					const CategoryEditModal: React.FC<CategoryEditModalProps> = ({ category, edit, onClose, onSave, onDelete }) => {
 | 
				
			||||||
  const [name, setName] = useState(category ? category.name : '');
 | 
					  const [name, setName] = useState(category ? category.name : '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSave = () => {
 | 
					 | 
				
			||||||
    onSave(name);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
 | 
					  const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
 | 
				
			||||||
    if (e.target === e.currentTarget) {
 | 
					    if (e.target === e.currentTarget) {
 | 
				
			||||||
      onClose();
 | 
					      onClose();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,14 +5,16 @@ import { Server, Wallpaper } from '../types';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import Dropdown from './Dropdown';
 | 
					import Dropdown from './Dropdown';
 | 
				
			||||||
import { baseWallpapers } from './utils/baseWallpapers';
 | 
					import { baseWallpapers } from './utils/baseWallpapers';
 | 
				
			||||||
 | 
					import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ConfigurationModalProps {
 | 
					interface ConfigurationModalProps {
 | 
				
			||||||
  onClose: () => void;
 | 
					  onClose: () => void;
 | 
				
			||||||
  onSave: (config: any) => void;
 | 
					  onSave: (config: any) => void;
 | 
				
			||||||
  currentConfig: any;
 | 
					  currentConfig: any;
 | 
				
			||||||
 | 
					  onWallpaperChange: (newConfig: Partial<any>) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave, currentConfig }) => {
 | 
					const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave, currentConfig, onWallpaperChange }) => {
 | 
				
			||||||
  const [config, setConfig] = useState({
 | 
					  const [config, setConfig] = useState({
 | 
				
			||||||
    ...currentConfig,
 | 
					    ...currentConfig,
 | 
				
			||||||
    titleSize: currentConfig.titleSize || 'medium',
 | 
					    titleSize: currentConfig.titleSize || 'medium',
 | 
				
			||||||
@@ -36,7 +38,9 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
      format: 'h:mm A',
 | 
					      format: 'h:mm A',
 | 
				
			||||||
      ...currentConfig.clock,
 | 
					      ...currentConfig.clock,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    backgroundUrls: currentConfig.backgroundUrls || [],
 | 
					    currentWallpapers: Array.isArray(currentConfig.currentWallpapers)
 | 
				
			||||||
 | 
					      ? currentConfig.currentWallpapers.filter((name: string) => typeof name === 'string')
 | 
				
			||||||
 | 
					      : [],
 | 
				
			||||||
    wallpaperFrequency: currentConfig.wallpaperFrequency || '1d',
 | 
					    wallpaperFrequency: currentConfig.wallpaperFrequency || '1d',
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  const [activeTab, setActiveTab] = useState('general');
 | 
					  const [activeTab, setActiveTab] = useState('general');
 | 
				
			||||||
@@ -45,11 +49,14 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
  const [newWallpaperName, setNewWallpaperName] = useState('');
 | 
					  const [newWallpaperName, setNewWallpaperName] = useState('');
 | 
				
			||||||
  const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
 | 
					  const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
 | 
				
			||||||
  const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
 | 
					  const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
 | 
				
			||||||
 | 
					  const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false);
 | 
				
			||||||
  const menuRef = useRef<HTMLDivElement>(null);
 | 
					  const menuRef = useRef<HTMLDivElement>(null);
 | 
				
			||||||
  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
					  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
				
			||||||
 | 
					  const isSaving = useRef(false);
 | 
				
			||||||
  const [isVisible, setIsVisible] = useState(false);
 | 
					  const [isVisible, setIsVisible] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    setChromeStorageAvailable(checkChromeStorageLocalAvailable());
 | 
				
			||||||
    const storedUserWallpapers = localStorage.getItem('userWallpapers');
 | 
					    const storedUserWallpapers = localStorage.getItem('userWallpapers');
 | 
				
			||||||
    if (storedUserWallpapers) {
 | 
					    if (storedUserWallpapers) {
 | 
				
			||||||
      setUserWallpapers(JSON.parse(storedUserWallpapers));
 | 
					      setUserWallpapers(JSON.parse(storedUserWallpapers));
 | 
				
			||||||
@@ -57,23 +64,33 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    // A small timeout to allow the component to mount before starting the transition
 | 
					 | 
				
			||||||
    const timer = setTimeout(() => {
 | 
					    const timer = setTimeout(() => {
 | 
				
			||||||
      setIsVisible(true);
 | 
					      setIsVisible(true);
 | 
				
			||||||
    }, 10);
 | 
					    }, 10);
 | 
				
			||||||
    return () => clearTimeout(timer);
 | 
					    return () => clearTimeout(timer);
 | 
				
			||||||
  }, []);
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      if (!isSaving.current) {
 | 
				
			||||||
 | 
					        onWallpaperChange({ currentWallpapers: currentConfig.currentWallpapers });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClose = () => {
 | 
					  const handleClose = () => {
 | 
				
			||||||
    setIsVisible(false);
 | 
					    setIsVisible(false);
 | 
				
			||||||
    setTimeout(() => {
 | 
					    setTimeout(() => {
 | 
				
			||||||
      onClose();
 | 
					      onClose();
 | 
				
			||||||
    }, 300); // This duration should match the transition duration
 | 
					    }, 300);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | { target: { name: string; value: string | string[] } }) => {
 | 
					  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | { target: { name: string; value: string | string[] } }) => {
 | 
				
			||||||
    const { name, value } = e.target;
 | 
					    const { name, value } = e.target;
 | 
				
			||||||
    if (name.startsWith('serverWidget.')) {
 | 
					    if (name === 'currentWallpapers') {
 | 
				
			||||||
 | 
					      const wallpaperNames = Array.isArray(value) ? value : [value];
 | 
				
			||||||
 | 
					      setConfig({ ...config, currentWallpapers: wallpaperNames });
 | 
				
			||||||
 | 
					    } else if (name.startsWith('serverWidget.')) {
 | 
				
			||||||
      const field = name.split('.')[1];
 | 
					      const field = name.split('.')[1];
 | 
				
			||||||
      setConfig({
 | 
					      setConfig({
 | 
				
			||||||
        ...config,
 | 
					        ...config,
 | 
				
			||||||
@@ -90,6 +107,15 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    onWallpaperChange({ currentWallpapers: config.currentWallpapers });
 | 
				
			||||||
 | 
					    // Set wallpaperState in localStorage with lastWallpaperChange datetime
 | 
				
			||||||
 | 
					    localStorage.setItem('wallpaperState', JSON.stringify({
 | 
				
			||||||
 | 
					      lastWallpaperChange: new Date().toISOString(),
 | 
				
			||||||
 | 
					      currentIndex: 0,
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }, [config.currentWallpapers]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleClockToggleChange = (checked: boolean) => {
 | 
					  const handleClockToggleChange = (checked: boolean) => {
 | 
				
			||||||
    setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
 | 
					    setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -148,21 +174,21 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleAddWallpaper = () => {
 | 
					  const handleAddWallpaper = async () => {
 | 
				
			||||||
    if (newWallpaperName.trim() === '' || newWallpaperUrl.trim() === '') return;
 | 
					    if (newWallpaperUrl.trim() === '') return;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
    const newWallpaper: Wallpaper = {
 | 
					      const finalName = await addWallpaperToChromeStorageLocal(newWallpaperName, newWallpaperUrl);
 | 
				
			||||||
      name: newWallpaperName,
 | 
					      const newWallpaper: Wallpaper = { name: finalName };
 | 
				
			||||||
      url: newWallpaperUrl,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
					      const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
				
			||||||
      setUserWallpapers(updatedUserWallpapers);
 | 
					      setUserWallpapers(updatedUserWallpapers);
 | 
				
			||||||
      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
					      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
				
			||||||
    setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, newWallpaperUrl] });
 | 
					      setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
 | 
				
			||||||
 | 
					 | 
				
			||||||
      setNewWallpaperName('');
 | 
					      setNewWallpaperName('');
 | 
				
			||||||
      setNewWallpaperUrl('');
 | 
					      setNewWallpaperUrl('');
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      alert('Error adding wallpaper. Please check the URL and try again.');
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
					  const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
				
			||||||
@@ -172,36 +198,43 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
        alert('File size exceeds 4MB. Please choose a smaller file.');
 | 
					        alert('File size exceeds 4MB. Please choose a smaller file.');
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      const reader = new FileReader();
 | 
					      const reader = new FileReader();
 | 
				
			||||||
      reader.onload = () => {
 | 
					      reader.onload = async () => {
 | 
				
			||||||
        const base64 = reader.result as string;
 | 
					        const base64 = reader.result as string;
 | 
				
			||||||
        if (base64.length > 4.5 * 1024 * 1024) {
 | 
					        if (base64.length > 4.5 * 1024 * 1024) {
 | 
				
			||||||
          alert('The uploaded image is too large. Please choose a smaller file.');
 | 
					          alert('The uploaded image is too large. Please choose a smaller file.');
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
        const updatedUserWallpapers = userWallpapers.filter(w => !w.base64);
 | 
					          const finalName = await addWallpaperToChromeStorageLocal(file.name, base64);
 | 
				
			||||||
        const newWallpaper: Wallpaper = {
 | 
					          const newWallpaper: Wallpaper = { name: finalName };
 | 
				
			||||||
          name: file.name,
 | 
					          const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
				
			||||||
          base64,
 | 
					          setUserWallpapers(updatedUserWallpapers);
 | 
				
			||||||
        };
 | 
					          localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
				
			||||||
        setUserWallpapers([...updatedUserWallpapers, newWallpaper]);
 | 
					          setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
 | 
				
			||||||
        localStorage.setItem('userWallpapers', JSON.stringify([...updatedUserWallpapers, newWallpaper]));
 | 
					        } catch (error) {
 | 
				
			||||||
        setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, base64] });
 | 
					          alert('Error adding wallpaper. Please try again.');
 | 
				
			||||||
 | 
					          console.error(error);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      reader.readAsDataURL(file);
 | 
					      reader.readAsDataURL(file);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleDeleteWallpaper = (wallpaper: Wallpaper) => {
 | 
					  const handleDeleteUserWallpaper = async (wallpaper: Wallpaper) => {
 | 
				
			||||||
    const wallpaperIdentifier = wallpaper.url || wallpaper.base64;
 | 
					    try {
 | 
				
			||||||
    const updatedUserWallpapers = userWallpapers.filter(w => (w.url || w.base64) !== wallpaperIdentifier);
 | 
					      await removeWallpaperFromChromeStorageLocal(wallpaper.name);
 | 
				
			||||||
 | 
					      const updatedUserWallpapers = userWallpapers.filter(w => w.name !== wallpaper.name);
 | 
				
			||||||
      setUserWallpapers(updatedUserWallpapers);
 | 
					      setUserWallpapers(updatedUserWallpapers);
 | 
				
			||||||
      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
					      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
				
			||||||
 | 
					      const newcurrentWallpapers = config.currentWallpapers.filter((name: string) => name !== wallpaper.name);
 | 
				
			||||||
    const newBackgroundUrls = config.backgroundUrls.filter((url: string) => url !== wallpaperIdentifier);
 | 
					      const newConfig = { ...config, currentWallpapers: newcurrentWallpapers };
 | 
				
			||||||
    setConfig({ ...config, backgroundUrls: newBackgroundUrls });
 | 
					      setConfig(newConfig);
 | 
				
			||||||
 | 
					      onWallpaperChange({ currentWallpapers: newcurrentWallpapers });
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      alert('Error deleting wallpaper. Please try again.');
 | 
				
			||||||
 | 
					      console.error(error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const allWallpapers = [...baseWallpapers, ...userWallpapers];
 | 
					  const allWallpapers = [...baseWallpapers, ...userWallpapers];
 | 
				
			||||||
@@ -348,35 +381,17 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
              <div className="flex items-center justify-between">
 | 
					              <div className="flex items-center justify-between">
 | 
				
			||||||
                <label className="text-slate-300 text-sm font-semibold">Background</label>
 | 
					                <label className="text-slate-300 text-sm font-semibold">Background</label>
 | 
				
			||||||
                <Dropdown
 | 
					                <Dropdown
 | 
				
			||||||
                  name="backgroundUrls"
 | 
					                  name="currentWallpapers"
 | 
				
			||||||
                  value={config.backgroundUrls}
 | 
					                  value={config.currentWallpapers}
 | 
				
			||||||
                  onChange={handleChange}
 | 
					                  onChange={handleChange}
 | 
				
			||||||
                  multiple
 | 
					                  multiple
 | 
				
			||||||
                  options={allWallpapers.map(w => ({
 | 
					                  options={allWallpapers.map(w => ({
 | 
				
			||||||
                    value: w.url || w.base64 || '', 
 | 
					                    value: w.name,
 | 
				
			||||||
                    label: (
 | 
					                    label: w.name
 | 
				
			||||||
                      <div className="flex items-center justify-between w-full">
 | 
					 | 
				
			||||||
                        <span>{w.name}</span>
 | 
					 | 
				
			||||||
                        {!baseWallpapers.find(bw => (bw.url || bw.base64) === (w.url || w.base64)) && (
 | 
					 | 
				
			||||||
                          <button 
 | 
					 | 
				
			||||||
                            onClick={(e) => {
 | 
					 | 
				
			||||||
                              e.stopPropagation();
 | 
					 | 
				
			||||||
                              handleDeleteWallpaper(w);
 | 
					 | 
				
			||||||
                            }}
 | 
					 | 
				
			||||||
                            className="text-red-500 hover:text-red-400 ml-4 p-1 rounded-full flex items-center justify-center"
 | 
					 | 
				
			||||||
                          >
 | 
					 | 
				
			||||||
                            <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
 | 
				
			||||||
              {Array.isArray(config.backgroundUrls) && config.backgroundUrls.length > 1 && (
 | 
					              {Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
 | 
				
			||||||
                <div className="flex items-center justify-between">
 | 
					                <div className="flex items-center justify-between">
 | 
				
			||||||
                  <label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
 | 
					                  <label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
 | 
				
			||||||
                  <Dropdown
 | 
					                  <Dropdown
 | 
				
			||||||
@@ -439,12 +454,33 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
                  <span>{config.wallpaperOpacity}%</span>
 | 
					                  <span>{config.wallpaperOpacity}%</span>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </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>
 | 
					                  <div>
 | 
				
			||||||
                    <h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
 | 
					                    <h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
 | 
				
			||||||
                    <div className="flex flex-col gap-2">
 | 
					                    <div className="flex flex-col gap-2">
 | 
				
			||||||
                      <input
 | 
					                      <input
 | 
				
			||||||
                        type="text"
 | 
					                        type="text"
 | 
				
			||||||
                    placeholder="Wallpaper Name"
 | 
					                        placeholder="Wallpaper Name (optional for URLs)"
 | 
				
			||||||
                        value={newWallpaperName}
 | 
					                        value={newWallpaperName}
 | 
				
			||||||
                        onChange={(e) => setNewWallpaperName(e.target.value)}
 | 
					                        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"
 | 
					                        className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
 | 
				
			||||||
@@ -481,6 +517,8 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </div>
 | 
					                  </div>
 | 
				
			||||||
 | 
					                </>
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -629,7 +667,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
				
			|||||||
        </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 justify-end gap-4">
 | 
				
			||||||
                <button onClick={() => onSave(config)} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
 | 
					                <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">
 | 
				
			||||||
                    Save & Close
 | 
					                    Save & Close
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
                <button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
 | 
					                <button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useState, useEffect } from 'react';
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
import { Server } from '../types';
 | 
					import { Server } from '../types';
 | 
				
			||||||
import ping from './utils/jsping.js';
 | 
					import ping from './utils/jsping.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React from 'react';
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ToggleSwitchProps {
 | 
					interface ToggleSwitchProps {
 | 
				
			||||||
  checked: boolean;
 | 
					  checked: boolean;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										106
									
								
								components/Wallpaper.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								components/Wallpaper.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
 | 
					import { baseWallpapers } from './utils/baseWallpapers';
 | 
				
			||||||
 | 
					import { Wallpaper as WallpaperType } from '../types';
 | 
				
			||||||
 | 
					import { getWallpaperFromChromeStorageLocal } from './utils/StorageLocalManager';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface WallpaperProps {
 | 
				
			||||||
 | 
					  wallpaperNames: string[];
 | 
				
			||||||
 | 
					  blur: number;
 | 
				
			||||||
 | 
					  brightness: number;
 | 
				
			||||||
 | 
					  opacity: number;
 | 
				
			||||||
 | 
					  wallpaperFrequency: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getWallpaperUrlByName = async (name: string): Promise<string | undefined> => {
 | 
				
			||||||
 | 
					  const foundInBase = baseWallpapers.find((w: WallpaperType) => w.name === name);
 | 
				
			||||||
 | 
					  if (foundInBase) {
 | 
				
			||||||
 | 
					    return foundInBase.url || foundInBase.base64;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const userWallpapers: WallpaperType[] = JSON.parse(localStorage.getItem('userWallpapers') || '[]');
 | 
				
			||||||
 | 
					  const foundInUser = userWallpapers.find((w: WallpaperType) => w.name === name);
 | 
				
			||||||
 | 
					  if (foundInUser) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const wallpaperData = await getWallpaperFromChromeStorageLocal(name);
 | 
				
			||||||
 | 
					      if (wallpaperData && wallpaperData.startsWith('http')) {
 | 
				
			||||||
 | 
					        return wallpaperData;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return wallpaperData || undefined;
 | 
				
			||||||
 | 
					    } catch (error) {
 | 
				
			||||||
 | 
					      console.error('Error getting wallpaper from chrome storage', error);
 | 
				
			||||||
 | 
					      return undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness, opacity, wallpaperFrequency }) => {
 | 
				
			||||||
 | 
					  const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
 | 
				
			||||||
 | 
					  const [currentWallpaperIndex, setCurrentWallpaperIndex] = useState<number>(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Helper to parse wallpaperFrequency string to ms
 | 
				
			||||||
 | 
					  const parseFrequencyToMs = (freq: string): number => {
 | 
				
			||||||
 | 
					    if (!freq) return 24 * 60 * 60 * 1000; // default 1 day
 | 
				
			||||||
 | 
					    const match = freq.match(/(\d+)(h|d)/);
 | 
				
			||||||
 | 
					    if (!match) return 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					    const value = parseInt(match[1], 10);
 | 
				
			||||||
 | 
					    const unit = match[2];
 | 
				
			||||||
 | 
					    if (unit === 'h') return value * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					    if (unit === 'd') return value * 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					    return 24 * 60 * 60 * 1000;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    const updateWallpaper = async () => {
 | 
				
			||||||
 | 
					      if (wallpaperNames.length === 0) return;
 | 
				
			||||||
 | 
					      // Read wallpaperState from localStorage
 | 
				
			||||||
 | 
					      const wallpaperState = JSON.parse(localStorage.getItem('wallpaperState') || '{}');
 | 
				
			||||||
 | 
					      const lastChange = wallpaperState.lastWallpaperChange ? new Date(wallpaperState.lastWallpaperChange).getTime() : 0;
 | 
				
			||||||
 | 
					      const now = Date.now();
 | 
				
			||||||
 | 
					      const freqMs = parseFrequencyToMs(wallpaperFrequency);
 | 
				
			||||||
 | 
					      let currentIndex = typeof wallpaperState.currentIndex === 'number' ? wallpaperState.currentIndex : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // If enough time has passed, pick a new wallpaper
 | 
				
			||||||
 | 
					      if (now - lastChange >= freqMs) {
 | 
				
			||||||
 | 
					        currentIndex = (currentIndex + 1) % wallpaperNames.length;
 | 
				
			||||||
 | 
					        localStorage.setItem('wallpaperState', JSON.stringify({
 | 
				
			||||||
 | 
					          lastWallpaperChange: new Date().toISOString(),
 | 
				
			||||||
 | 
					          currentIndex
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Keep currentIndex in sync with localStorage if not updating
 | 
				
			||||||
 | 
					        localStorage.setItem('wallpaperState', JSON.stringify({
 | 
				
			||||||
 | 
					          lastWallpaperChange: wallpaperState.lastWallpaperChange || new Date().toISOString(),
 | 
				
			||||||
 | 
					          currentIndex
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      setCurrentWallpaperIndex(currentIndex);
 | 
				
			||||||
 | 
					      const wallpaperName = wallpaperNames[currentIndex];
 | 
				
			||||||
 | 
					      const url = await getWallpaperUrlByName(wallpaperName);
 | 
				
			||||||
 | 
					      setImageUrl(url);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    updateWallpaper();
 | 
				
			||||||
 | 
					    // No timer, just run on render/dependency change
 | 
				
			||||||
 | 
					  }, [wallpaperNames, wallpaperFrequency]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!imageUrl) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className="fixed inset-0 -z-10 w-full h-full"
 | 
				
			||||||
 | 
					      style={{
 | 
				
			||||||
 | 
					        backgroundImage: `url(${imageUrl})`,
 | 
				
			||||||
 | 
					        backgroundSize: 'cover',
 | 
				
			||||||
 | 
					        backgroundPosition: 'center',
 | 
				
			||||||
 | 
					        filter: `blur(${blur}px) brightness(${brightness / 100})`,
 | 
				
			||||||
 | 
					        opacity: opacity / 100,
 | 
				
			||||||
 | 
					        transition: 'filter 0.3s, opacity 0.3s',
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					      aria-label="Wallpaper background"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Wallpaper;
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useState, useEffect } from 'react';
 | 
					import { useState, useEffect } from 'react';
 | 
				
			||||||
import { Website } from '../types';
 | 
					import { Website } from '../types';
 | 
				
			||||||
import { getWebsiteIcon } from './utils/iconService';
 | 
					import { getWebsiteIcon } from './utils/iconService';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import React, { useState } from 'react';
 | 
					import React, { useState } from 'react';
 | 
				
			||||||
import { Website } from '../types';
 | 
					import { Website } from '../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
interface WebsiteTileProps {
 | 
					interface WebsiteTileProps {
 | 
				
			||||||
  website: Website;
 | 
					  website: Website;
 | 
				
			||||||
  isEditing: boolean;
 | 
					  isEditing: boolean;
 | 
				
			||||||
@@ -23,18 +22,34 @@ const getTileSizeClass = (size: string | undefined) => {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getIconSize = (size: string | undefined) => {
 | 
					
 | 
				
			||||||
 | 
					// Returns normal icon size in px
 | 
				
			||||||
 | 
					const getIconPixelSize = (size: string | undefined): number => {
 | 
				
			||||||
  switch (size) {
 | 
					  switch (size) {
 | 
				
			||||||
    case 'small':
 | 
					    case 'small':
 | 
				
			||||||
      return 8;
 | 
					      return 34;
 | 
				
			||||||
    case 'medium':
 | 
					    case 'medium':
 | 
				
			||||||
      return 10;
 | 
					      return 42;
 | 
				
			||||||
    case 'large':
 | 
					    case 'large':
 | 
				
			||||||
      return 12;
 | 
					      return 48;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
      return 10;
 | 
					      return 40;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Returns loading icon size in px
 | 
				
			||||||
 | 
					const getIconLoadingPixelSize = (size: string | undefined): number => {
 | 
				
			||||||
 | 
					  switch (size) {
 | 
				
			||||||
 | 
					    case 'small':
 | 
				
			||||||
 | 
					      return 24;
 | 
				
			||||||
 | 
					    case 'medium':
 | 
				
			||||||
 | 
					      return 32;
 | 
				
			||||||
 | 
					    case 'large':
 | 
				
			||||||
 | 
					      return 40;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return 32;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, onMove, tileSize }) => {
 | 
					const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, onMove, tileSize }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -54,8 +69,8 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
 | 
				
			|||||||
    // }, 3500); // Small delay to show spinner before navigation
 | 
					    // }, 3500); // Small delay to show spinner before navigation
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const iconSizeClass = `w-${getIconSize(tileSize)} h-${getIconSize(tileSize)}`;
 | 
					  const iconSizeClass = `w-[${getIconPixelSize(tileSize)}px] h-[${getIconPixelSize(tileSize)}px]`;
 | 
				
			||||||
  const iconSizeLoadingClass = `w-${getIconSize(tileSize) - 4} h-${getIconSize(tileSize) - 4}`;
 | 
					  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-300 ease-in-out`}>
 | 
				
			||||||
@@ -76,7 +91,7 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
 | 
				
			|||||||
        )}
 | 
					        )}
 | 
				
			||||||
        <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-300 ease-in ${isLoading ? 'mt-18' : 'flex-col'} ${isLoading ? 'gap-2' : ''}`}>
 | 
				
			||||||
          <div className={`transition-all duration-300 ease-in ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
 | 
					          <div className={`transition-all duration-300 ease-in ${isLoading ? iconSizeLoadingClass : iconSizeClass}`}>
 | 
				
			||||||
            <img src={website.icon} alt={`${website.name} icon`} className="object-contain" />
 | 
					            <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-300 ease-in ${isLoading ? 'text-sm' : ''}`}>
 | 
				
			||||||
            {website.name}
 | 
					            {website.name}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import React from 'react';
 | 
					 | 
				
			||||||
import WebsiteTile from '../WebsiteTile';
 | 
					import WebsiteTile from '../WebsiteTile';
 | 
				
			||||||
import { Category, Website } from '../../types';
 | 
					import { Category, Website } from '../../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -41,7 +40,7 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
 | 
				
			|||||||
            className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
 | 
					            className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
 | 
					            <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
 | 
				
			||||||
              <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zM1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
 | 
					              <path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
 | 
				
			||||||
            </svg>
 | 
					            </svg>
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React from 'react';
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ConfigurationButtonProps {
 | 
					interface ConfigurationButtonProps {
 | 
				
			||||||
  onClick: () => void;
 | 
					  onClick: () => void;
 | 
				
			||||||
@@ -11,8 +11,9 @@ const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) =>
 | 
				
			|||||||
        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 transition-colors"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-gear-wide" viewBox="0 0 16 16">
 | 
					          <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
				
			||||||
          <path d="M8.932.727c-.243-.97-1.62-.97-1.864 0l-.071.286a.96.96 0 0 1-1.622.434l-.205-.211c-.695-.719-1.888-.03-1.613.931l.08.284a.96.96 0 0 1-1.186 1.187l-.284-.081c-.96-.275-1.65.918-.931 1.613l.211.205a.96.96 0 0 1-.434 1.622l-.286.071c-.97.243-.97 1.62 0 1.864l.286.071a.96.96 0 0 1 .434 1.622l-.211.205c-.719.695-.03 1.888.931 1.613l.284-.08a.96.96 0 0 1 1.187 1.187l-.081.283c-.275.96.918 1.65 1.613.931l.205-.211a.96.96 0 0 1 1.622.434l.071.286c.243.97 1.62.97 1.864 0l.071-.286a.96.96 0 0 1 1.622-.434l.205.211c.695.719 1.888.03 1.613-.931l-.08-.284a.96.96 0 0 1 1.187-1.187l.283.081c.96.275 1.65-.918-.931-1.613l-.211-.205a.96.96 0 0 1 .434-1.622l.286-.071c.97-.243.97-1.62 0-1.864l-.286-.071a.96.96 0 0 1-.434-1.622l.211-.205c.719-.695.03-1.888-.931-1.613l-.284.08a.96.96 0 0 1-1.187-1.186l.081-.284c.275-.96-.918-1.65-1.613-.931l-.205.211a.96.96 0 0 1-1.622-.434zM8 12.997a4.998 4.998 0 1 1 0-9.995 4.998 4.998 0 0 1 0 9.996z"/>
 | 
					            <circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
 | 
				
			||||||
 | 
					            <path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09c.7 0 1.31-.4 1.51-1a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06c.51.51 1.31.61 1.82.33.51-.28 1-.81 1-1.51V3a2 2 0 1 1 4 0v.09c0 .7.49 1.23 1 1.51.51.28 1.31.18 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82c.2.6.81 1 1.51 1H21a2 2 0 1 1 0 4h-.09c-.7 0-1.31.4-1.51 1z"/>
 | 
				
			||||||
          </svg>
 | 
					          </svg>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React from 'react';
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface EditButtonProps {
 | 
					interface EditButtonProps {
 | 
				
			||||||
  isEditing: boolean;
 | 
					  isEditing: boolean;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import React from 'react';
 | 
					 | 
				
			||||||
import Clock from '../Clock';
 | 
					import Clock from '../Clock';
 | 
				
			||||||
import { Config } from '../../types';
 | 
					import { Config } from '../../types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										164
									
								
								components/utils/StorageLocalManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								components/utils/StorageLocalManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
				
			|||||||
 | 
					// TypeScript interface for window.chrome
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface Window {
 | 
				
			||||||
 | 
					    chrome?: {
 | 
				
			||||||
 | 
					      storage?: {
 | 
				
			||||||
 | 
					        local?: {
 | 
				
			||||||
 | 
					          set: (items: object, callback?: () => void) => void;
 | 
				
			||||||
 | 
					          get: (keys: string[] | string, callback: (items: { [key: string]: string }) => void) => void;
 | 
				
			||||||
 | 
					          remove: (keys: string | string[], callback?: () => void) => void;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      runtime?: {
 | 
				
			||||||
 | 
					        lastError?: { message: string };
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let isChromeStorageLocalAvailable: boolean | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Checks if chrome.storage.local is available and caches the result.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export function checkChromeStorageLocalAvailable(): boolean {
 | 
				
			||||||
 | 
					  if (isChromeStorageLocalAvailable !== null) return isChromeStorageLocalAvailable;
 | 
				
			||||||
 | 
					  isChromeStorageLocalAvailable =
 | 
				
			||||||
 | 
					    typeof window !== 'undefined' &&
 | 
				
			||||||
 | 
					    typeof window.chrome !== 'undefined' &&
 | 
				
			||||||
 | 
					    typeof window.chrome.storage !== 'undefined' &&
 | 
				
			||||||
 | 
					    typeof window.chrome.storage.local !== 'undefined';
 | 
				
			||||||
 | 
					  return isChromeStorageLocalAvailable;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Adds a new wallpaper to chrome.storage.local.
 | 
				
			||||||
 | 
					 * If the URL is fetchable, it will be stored as base64 and the name will be derived from the URL.
 | 
				
			||||||
 | 
					 * If the URL is not fetchable (e.g., CORS), it will be stored as a URL and the provided name will be used.
 | 
				
			||||||
 | 
					 * @param name Wallpaper name (string), used as a fallback.
 | 
				
			||||||
 | 
					 * @param url Wallpaper image URL (string) or base64 data URL.
 | 
				
			||||||
 | 
					 * @returns Promise<string> The name under which the wallpaper was stored.
 | 
				
			||||||
 | 
					 * @throws Error if chrome.storage.local is unavailable or if a name is not provided for a non-fetchable URL.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function addWallpaperToChromeStorageLocal(name: string, url: string): Promise<string> {
 | 
				
			||||||
 | 
					  if (!checkChromeStorageLocalAvailable()) {
 | 
				
			||||||
 | 
					    throw new Error('chrome.storage.local is not available');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (url.startsWith('data:')) {
 | 
				
			||||||
 | 
					    // This is a base64 encoded image from a file upload.
 | 
				
			||||||
 | 
					    // The name is the file name.
 | 
				
			||||||
 | 
					    return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					      if (window.chrome?.storage?.local) {
 | 
				
			||||||
 | 
					        window.chrome.storage.local.set({ [name]: url }, function () {
 | 
				
			||||||
 | 
					          if (window.chrome?.runtime?.lastError) {
 | 
				
			||||||
 | 
					            reject(new Error(window.chrome.runtime.lastError.message));
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            resolve();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        reject(new Error('chrome.storage.local is not available'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(() => name);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This is a URL. Let's try to fetch it.
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    const response = await fetch(url);
 | 
				
			||||||
 | 
					    if (!response.ok) throw new Error('Failed to fetch image');
 | 
				
			||||||
 | 
					    const imageBlob = await response.blob();
 | 
				
			||||||
 | 
					    const reader = new FileReader();
 | 
				
			||||||
 | 
					    const base64 = await new Promise<string>((resolve, reject) => {
 | 
				
			||||||
 | 
					      reader.onloadend = () => resolve(reader.result as string);
 | 
				
			||||||
 | 
					      reader.onerror = reject;
 | 
				
			||||||
 | 
					      reader.readAsDataURL(imageBlob);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If successful, use the filename from URL as the name.
 | 
				
			||||||
 | 
					    const finalName = url.substring(url.lastIndexOf('/') + 1).replace(/[?#].*$/, '') || name;
 | 
				
			||||||
 | 
					    return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					      if (window.chrome?.storage?.local) {
 | 
				
			||||||
 | 
					        window.chrome.storage.local.set({ [finalName]: base64 }, function () {
 | 
				
			||||||
 | 
					          if (window.chrome?.runtime?.lastError) {
 | 
				
			||||||
 | 
					            reject(new Error(window.chrome.runtime.lastError.message));
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            resolve();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        reject(new Error('chrome.storage.local is not available'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(() => finalName);
 | 
				
			||||||
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
					    // If fetch fails (e.g., CORS), store the URL directly with the user-provided name.
 | 
				
			||||||
 | 
					    console.warn('Could not fetch wallpaper, storing URL instead. Error:', error);
 | 
				
			||||||
 | 
					    if (!name) {
 | 
				
			||||||
 | 
					      throw new Error("A name for the wallpaper is required when the URL can't be accessed.");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					      if (window.chrome?.storage?.local) {
 | 
				
			||||||
 | 
					        window.chrome.storage.local.set({ [name]: url }, function () {
 | 
				
			||||||
 | 
					          if (window.chrome?.runtime?.lastError) {
 | 
				
			||||||
 | 
					            reject(new Error(window.chrome.runtime.lastError.message));
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            resolve();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        reject(new Error('chrome.storage.local is not available'));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }).then(() => name);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Gets a specific wallpaper from chrome.storage.local by name.
 | 
				
			||||||
 | 
					 * @param name Wallpaper name (string)
 | 
				
			||||||
 | 
					 * @returns Promise<string | null> (base64 string or null)
 | 
				
			||||||
 | 
					 * @throws Error if chrome.storage.local is unavailable
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function getWallpaperFromChromeStorageLocal(name: string): Promise<string | null> {
 | 
				
			||||||
 | 
					  if (!checkChromeStorageLocalAvailable()) {
 | 
				
			||||||
 | 
					    throw new Error('chrome.storage.local is not available');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return new Promise<string | null>((resolve, reject) => {
 | 
				
			||||||
 | 
					    if (window.chrome?.storage?.local) {
 | 
				
			||||||
 | 
					      window.chrome.storage.local.get([name], function (result: { [key: string]: string }) {
 | 
				
			||||||
 | 
					        if (window.chrome?.runtime?.lastError) {
 | 
				
			||||||
 | 
					          reject(new Error(window.chrome.runtime.lastError.message));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          resolve(result[name] || null);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      reject(new Error('chrome.storage.local is not available'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Removes a wallpaper from chrome.storage.local by name.
 | 
				
			||||||
 | 
					 * @param name Wallpaper name (string)
 | 
				
			||||||
 | 
					 * @returns Promise<void>
 | 
				
			||||||
 | 
					 * @throws Error if chrome.storage.local is unavailable
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					export async function removeWallpaperFromChromeStorageLocal(name: string): Promise<void> {
 | 
				
			||||||
 | 
					  if (!checkChromeStorageLocalAvailable()) {
 | 
				
			||||||
 | 
					    throw new Error('chrome.storage.local is not available');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					    if (window.chrome?.storage?.local) {
 | 
				
			||||||
 | 
					      window.chrome.storage.local.remove(name, function () {
 | 
				
			||||||
 | 
					        if (window.chrome?.runtime?.lastError) {
 | 
				
			||||||
 | 
					          reject(new Error(window.chrome.runtime.lastError.message));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          resolve();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      reject(new Error('chrome.storage.local is not available'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -14,12 +14,16 @@ export const baseWallpapers: Wallpaper[] = [
 | 
				
			|||||||
    name: 'Beach',
 | 
					    name: 'Beach',
 | 
				
			||||||
    url: 'https://wallpapershome.com/images/pages/pic_h/615.jpg'
 | 
					    url: 'https://wallpapershome.com/images/pages/pic_h/615.jpg'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    name: 'Dark',
 | 
				
			||||||
 | 
					    url: 'https://i.imgur.com/qHlRO0s.jpeg'
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Mountain',
 | 
					    name: 'Mountain',
 | 
				
			||||||
    url: 'https://i.imgur.com/yHfOZUd.jpeg'
 | 
					    url: 'https://i.imgur.com/yHfOZUd.jpeg'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: 'Waves',
 | 
					    name: 'Waves',
 | 
				
			||||||
    url: '/waves.jpg',
 | 
					    url: 'https://i.imgur.com/E8uxZ7R.png',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								screenshots/configuration-abstract-red.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								screenshots/configuration-abstract-red.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 744 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								screenshots/dark-page.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								screenshots/dark-page.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 1.3 MiB  | 
							
								
								
									
										
											BIN
										
									
								
								screenshots/editing-abstract-red.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								screenshots/editing-abstract-red.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 3.5 MiB  | 
							
								
								
									
										113
									
								
								scripts/check_virustotal.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										113
									
								
								scripts/check_virustotal.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,113 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Script to check a file against VirusTotal API
 | 
				
			||||||
 | 
					# Requires: curl, jq
 | 
				
			||||||
 | 
					# Environment variable: virustotal_apikey
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Configuration
 | 
				
			||||||
 | 
					FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}"
 | 
				
			||||||
 | 
					API_KEY="${virustotal_apikey}"
 | 
				
			||||||
 | 
					BASE_URL="https://www.virustotal.com/vtapi/v2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Check if API key is set
 | 
				
			||||||
 | 
					if [ -z "$API_KEY" ]; then
 | 
				
			||||||
 | 
					    echo "Error: virustotal_apikey environment variable is not set"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Check if file exists
 | 
				
			||||||
 | 
					if [ ! -f "$FILE_PATH" ]; then
 | 
				
			||||||
 | 
					    echo "Error: File $FILE_PATH not found"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Check if required tools are available
 | 
				
			||||||
 | 
					if ! command -v curl &> /dev/null; then
 | 
				
			||||||
 | 
					    echo "Error: curl is required but not installed"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if ! command -v jq &> /dev/null; then
 | 
				
			||||||
 | 
					    echo "Error: jq is required but not installed"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Uploading $FILE_PATH to VirusTotal for analysis..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Upload file to VirusTotal
 | 
				
			||||||
 | 
					UPLOAD_RESPONSE=$(curl -s -X POST \
 | 
				
			||||||
 | 
					    -F "apikey=$API_KEY" \
 | 
				
			||||||
 | 
					    -F "file=@$FILE_PATH" \
 | 
				
			||||||
 | 
					    "$BASE_URL/file/scan")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Extract scan_id from response
 | 
				
			||||||
 | 
					SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.scan_id')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then
 | 
				
			||||||
 | 
					    echo "Error: Failed to upload file or get scan ID"
 | 
				
			||||||
 | 
					    echo "Response: $UPLOAD_RESPONSE"
 | 
				
			||||||
 | 
					    exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "File uploaded successfully. Scan ID: $SCAN_ID"
 | 
				
			||||||
 | 
					echo "Waiting for analysis to complete..."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Wait for analysis to complete and get results
 | 
				
			||||||
 | 
					MAX_ATTEMPTS=30
 | 
				
			||||||
 | 
					ATTEMPT=0
 | 
				
			||||||
 | 
					SLEEP_INTERVAL=10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
 | 
				
			||||||
 | 
					    echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Get scan report
 | 
				
			||||||
 | 
					    REPORT_RESPONSE=$(curl -s -X POST \
 | 
				
			||||||
 | 
					        -d "apikey=$API_KEY" \
 | 
				
			||||||
 | 
					        -d "resource=$SCAN_ID" \
 | 
				
			||||||
 | 
					        "$BASE_URL/file/report")
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    # Check if analysis is complete
 | 
				
			||||||
 | 
					    RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.response_code')
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    if [ "$RESPONSE_CODE" == "1" ]; then
 | 
				
			||||||
 | 
					        # Analysis complete
 | 
				
			||||||
 | 
					        echo "Analysis completed!"
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Extract results
 | 
				
			||||||
 | 
					        POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.positives')
 | 
				
			||||||
 | 
					        TOTAL=$(echo "$REPORT_RESPONSE" | jq -r '.total')
 | 
				
			||||||
 | 
					        PERMALINK=$(echo "$REPORT_RESPONSE" | jq -r '.permalink')
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        echo "Analysis URL: $PERMALINK"
 | 
				
			||||||
 | 
					        echo "Detection ratio: $POSITIVES/$TOTAL"
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        # Check if file is safe
 | 
				
			||||||
 | 
					        if [ "$POSITIVES" -eq 0 ]; then
 | 
				
			||||||
 | 
					            echo "✅ File is clean (no threats detected)"
 | 
				
			||||||
 | 
					            exit 0
 | 
				
			||||||
 | 
					        else
 | 
				
			||||||
 | 
					            echo "❌ File contains threats ($POSITIVES detections out of $TOTAL scanners)"
 | 
				
			||||||
 | 
					            exit 1
 | 
				
			||||||
 | 
					        fi
 | 
				
			||||||
 | 
					    elif [ "$RESPONSE_CODE" == "0" ]; 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..."
 | 
				
			||||||
 | 
					    else
 | 
				
			||||||
 | 
					        echo "Unexpected response code: $RESPONSE_CODE"
 | 
				
			||||||
 | 
					        echo "Response: $REPORT_RESPONSE"
 | 
				
			||||||
 | 
					        exit 1
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    ATTEMPT=$((ATTEMPT + 1))
 | 
				
			||||||
 | 
					    if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
 | 
				
			||||||
 | 
					        sleep $SLEEP_INTERVAL
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "Timeout: Analysis did not complete within expected time"
 | 
				
			||||||
 | 
					exit 1
 | 
				
			||||||
@@ -8,4 +8,15 @@ export default {
 | 
				
			|||||||
    extend: {},
 | 
					    extend: {},
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  plugins: [],
 | 
					  plugins: [],
 | 
				
			||||||
 | 
					  safelist: [
 | 
				
			||||||
 | 
					    'w-[24px]', 'h-[24px]',
 | 
				
			||||||
 | 
					    'w-[28px]', 'h-[28px]',
 | 
				
			||||||
 | 
					    'w-[32px]', 'h-[32px]',
 | 
				
			||||||
 | 
					    'w-[34px]', 'h-[34px]',
 | 
				
			||||||
 | 
					    'w-[36px]', 'h-[36px]',
 | 
				
			||||||
 | 
					    'w-[40px]', 'h-[40px]',
 | 
				
			||||||
 | 
					    'w-[42px]', 'h-[42px]',
 | 
				
			||||||
 | 
					    'w-[48px]', 'h-[48px]',
 | 
				
			||||||
 | 
					    // add any other sizes you use
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										2
									
								
								types.ts
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								types.ts
									
									
									
									
									
								
							@@ -27,7 +27,7 @@ export interface Wallpaper {
 | 
				
			|||||||
export interface Config {
 | 
					export interface Config {
 | 
				
			||||||
  title: string;
 | 
					  title: string;
 | 
				
			||||||
  subtitle: string;
 | 
					  subtitle: string;
 | 
				
			||||||
  backgroundUrls: string[];
 | 
					  currentWallpapers: string[];
 | 
				
			||||||
  wallpaperFrequency: string;
 | 
					  wallpaperFrequency: string;
 | 
				
			||||||
  wallpaperBlur: number;
 | 
					  wallpaperBlur: number;
 | 
				
			||||||
  wallpaperBrightness: number;
 | 
					  wallpaperBrightness: number;
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user