Compare commits
	
		
			6 Commits
		
	
	
		
			v-0.1.0
			...
			c60cb24dd4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c60cb24dd4 | |||
| 2f3949c2e3 | |||
| 9b818b05f9 | |||
| 008d2321e5 | |||
| 3aff7ffed6 | |||
| 9e80818fc5 | 
@@ -4,8 +4,6 @@ on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - main
 | 
			
		||||
    tags:
 | 
			
		||||
      - v*
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
@@ -18,29 +16,3 @@ jobs:
 | 
			
		||||
        run: npm install
 | 
			
		||||
      - name: Run build
 | 
			
		||||
        run: npm run build
 | 
			
		||||
 | 
			
		||||
  release:
 | 
			
		||||
    if: gitea.event_name == 'tag'
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out repository code
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
      - name: Setup zip
 | 
			
		||||
        run: sudo apt-get install zip -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: Release zip
 | 
			
		||||
        uses: akkuman/gitea-release-action@v1
 | 
			
		||||
        with:
 | 
			
		||||
          name: ${{ gitea.ref_name }}
 | 
			
		||||
          tag_name: ${{ gitea.ref_name }}
 | 
			
		||||
          files: vision-start-${{ gitea.ref_name }}.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
 | 
			
		||||
							
								
								
									
										88
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								App.tsx
									
									
									
									
									
								
							@@ -1,21 +1,20 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
import ConfigurationModal from './components/ConfigurationModal';
 | 
			
		||||
import ServerWidget from './components/ServerWidget';
 | 
			
		||||
import { DEFAULT_CATEGORIES } from './constants';
 | 
			
		||||
import { Category, Website, Wallpaper, Config } from './types';
 | 
			
		||||
import { Category, Website, Config } from './types';
 | 
			
		||||
import WebsiteEditModal from './components/WebsiteEditModal';
 | 
			
		||||
import CategoryEditModal from './components/CategoryEditModal';
 | 
			
		||||
import Header from './components/layout/Header';
 | 
			
		||||
import EditButton from './components/layout/EditButton';
 | 
			
		||||
import ConfigurationButton from './components/layout/ConfigurationButton';
 | 
			
		||||
import CategoryGroup from './components/layout/CategoryGroup';
 | 
			
		||||
 | 
			
		||||
import { baseWallpapers } from './components/utils/baseWallpapers';
 | 
			
		||||
import Wallpaper from './components/Wallpaper';
 | 
			
		||||
 | 
			
		||||
const defaultConfig: Config = {
 | 
			
		||||
  title: 'Vision Start',
 | 
			
		||||
  subtitle: 'Your personal portal to the web.',
 | 
			
		||||
  backgroundUrls: ['https://i.imgur.com/C6ynAtX.jpeg'],
 | 
			
		||||
  currentWallpapers: ['Abstract'],
 | 
			
		||||
  wallpaperFrequency: '1d',
 | 
			
		||||
  wallpaperBlur: 0,
 | 
			
		||||
  wallpaperBrightness: 100,
 | 
			
		||||
@@ -60,9 +59,6 @@ const App: React.FC = () => {
 | 
			
		||||
      const storedConfig = localStorage.getItem('config');
 | 
			
		||||
      if (storedConfig) {
 | 
			
		||||
        const parsedConfig = JSON.parse(storedConfig);
 | 
			
		||||
        if (!parsedConfig.backgroundUrls) {
 | 
			
		||||
          parsedConfig.backgroundUrls = [parsedConfig.backgroundUrl].filter(Boolean);
 | 
			
		||||
        }
 | 
			
		||||
        return { ...defaultConfig, ...parsedConfig };
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
@@ -70,62 +66,12 @@ const App: React.FC = () => {
 | 
			
		||||
    }
 | 
			
		||||
    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(() => {
 | 
			
		||||
    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 currentWallpaperFromState = allWallpapers.find(w => w.name === wallpaperState.current);
 | 
			
		||||
        const currentIndex = currentWallpaperFromState ? availableWallpapers.findIndex(w => w.name === currentWallpaperFromState.name) : -1;
 | 
			
		||||
        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('');
 | 
			
		||||
        localStorage.removeItem('wallpaperState');
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const currentWallpaperDetails = allWallpapers.find(w => w.name === wallpaperState.current);
 | 
			
		||||
    const isCurrentWallpaperValid = currentWallpaperDetails && config.backgroundUrls.includes(currentWallpaperDetails.url || currentWallpaperDetails.base64 || '');
 | 
			
		||||
 | 
			
		||||
    if (!isCurrentWallpaperValid || Date.now() - lastChanged > frequency) {
 | 
			
		||||
      updateWallpaper();
 | 
			
		||||
    } else if (currentWallpaperDetails) {
 | 
			
		||||
      setCurrentWallpaper(currentWallpaperDetails.url || currentWallpaperDetails.base64 || '');
 | 
			
		||||
    } else {
 | 
			
		||||
      // Fallback for when there's no valid wallpaper state
 | 
			
		||||
      updateWallpaper();
 | 
			
		||||
    }
 | 
			
		||||
  }, [config.backgroundUrls, config.wallpaperFrequency, allWallpapers]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    localStorage.setItem('categories', JSON.stringify(categories));
 | 
			
		||||
    localStorage.setItem('config', JSON.stringify(config));
 | 
			
		||||
  }, [categories, config]);
 | 
			
		||||
  }, [config]);
 | 
			
		||||
 | 
			
		||||
  const handleSaveConfig = (newConfig: any) => {
 | 
			
		||||
  const handleSaveConfig = (newConfig: Config) => {
 | 
			
		||||
    setConfig(newConfig);
 | 
			
		||||
    setIsConfigModalOpen(false);
 | 
			
		||||
  };
 | 
			
		||||
@@ -201,7 +147,6 @@ const App: React.FC = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleMoveWebsite = (website: Website, direction: 'left' | 'right') => {
 | 
			
		||||
    const categoryIndex = categories.findIndex(c => c.id === website.categoryId);
 | 
			
		||||
    if (categoryIndex === -1) return;
 | 
			
		||||
 | 
			
		||||
    const category = categories[categoryIndex];
 | 
			
		||||
@@ -259,14 +204,13 @@ const App: React.FC = () => {
 | 
			
		||||
    <main
 | 
			
		||||
      className={`min-h-screen w-full flex flex-col items-center ${getAlignmentClass(config.alignment)} p-4`}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        className="fixed inset-0 w-full h-full bg-cover bg-center bg-fixed -z-10"
 | 
			
		||||
        style={{
 | 
			
		||||
          backgroundImage: `url('${currentWallpaper}')`,
 | 
			
		||||
          filter: `blur(${config.wallpaperBlur}px) brightness(${config.wallpaperBrightness}%)`,
 | 
			
		||||
          opacity: `${config.wallpaperOpacity}%`,
 | 
			
		||||
        }}
 | 
			
		||||
      ></div>
 | 
			
		||||
      <Wallpaper
 | 
			
		||||
        wallpaperNames={config.currentWallpapers}
 | 
			
		||||
        blur={config.wallpaperBlur}
 | 
			
		||||
        brightness={config.wallpaperBrightness}
 | 
			
		||||
        opacity={config.wallpaperOpacity}
 | 
			
		||||
        wallpaperFrequency={config.wallpaperFrequency}
 | 
			
		||||
      />
 | 
			
		||||
      <EditButton isEditing={isEditing} onClick={() => setIsEditing(!isEditing)} />
 | 
			
		||||
      <ConfigurationButton onClick={() => setIsConfigModalOpen(true)} />
 | 
			
		||||
 | 
			
		||||
@@ -305,11 +249,7 @@ const App: React.FC = () => {
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {config.serverWidget.enabled && (
 | 
			
		||||
        <div className="absolute bottom-4 right-4">
 | 
			
		||||
          <ServerWidget config={config} />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      {config.serverWidget.enabled && <ServerWidget config={config} />}
 | 
			
		||||
 | 
			
		||||
      {(editingWebsite || addingWebsite) && (
 | 
			
		||||
        <WebsiteEditModal
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.md
									
									
									
									
									
								
							@@ -7,16 +7,16 @@
 | 
			
		||||

 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Backgrounds
 | 
			
		||||
## Installing
 | 
			
		||||
 | 
			
		||||
It comes with a selection of some nice pre-defined backgrounds. You can also upload up to one image to it.
 | 
			
		||||
 | 
			
		||||
* **Abstract**
 | 
			
		||||
* **Abstract Red**
 | 
			
		||||
* **Beach**
 | 
			
		||||
* **Dark**
 | 
			
		||||
* **Mountain**
 | 
			
		||||
* **Waves**
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@@ -27,6 +27,12 @@ It comes with a selection of some nice pre-defined backgrounds. You can also upl
 | 
			
		||||
* **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
 | 
			
		||||
@@ -49,10 +55,10 @@ npm run dev
 | 
			
		||||
 | 
			
		||||
* [x] Multiple Wallpapers
 | 
			
		||||
* [x] Remake icons
 | 
			
		||||
* [] Increase offline compatibility (might not be possible)
 | 
			
		||||
  * 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
 | 
			
		||||
* [/] 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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { useState } from 'react';
 | 
			
		||||
import { Category } from '../types';
 | 
			
		||||
 | 
			
		||||
interface CategoryEditModalProps {
 | 
			
		||||
@@ -12,10 +12,6 @@ interface CategoryEditModalProps {
 | 
			
		||||
const CategoryEditModal: React.FC<CategoryEditModalProps> = ({ category, edit, onClose, onSave, onDelete }) => {
 | 
			
		||||
  const [name, setName] = useState(category ? category.name : '');
 | 
			
		||||
 | 
			
		||||
  const handleSave = () => {
 | 
			
		||||
    onSave(name);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
    if (e.target === e.currentTarget) {
 | 
			
		||||
      onClose();
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import { Server, Wallpaper } from '../types';
 | 
			
		||||
 | 
			
		||||
import Dropdown from './Dropdown';
 | 
			
		||||
import { baseWallpapers } from './utils/baseWallpapers';
 | 
			
		||||
import { addWallpaperToChromeStorageLocal, removeWallpaperFromChromeStorageLocal, checkChromeStorageLocalAvailable } from './utils/StorageLocalManager';
 | 
			
		||||
 | 
			
		||||
interface ConfigurationModalProps {
 | 
			
		||||
  onClose: () => void;
 | 
			
		||||
@@ -37,7 +38,9 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
      format: 'h:mm A',
 | 
			
		||||
      ...currentConfig.clock,
 | 
			
		||||
    },
 | 
			
		||||
    backgroundUrls: currentConfig.backgroundUrls || [],
 | 
			
		||||
    currentWallpapers: Array.isArray(currentConfig.currentWallpapers)
 | 
			
		||||
      ? currentConfig.currentWallpapers.filter((name: string) => typeof name === 'string')
 | 
			
		||||
      : [],
 | 
			
		||||
    wallpaperFrequency: currentConfig.wallpaperFrequency || '1d',
 | 
			
		||||
  });
 | 
			
		||||
  const [activeTab, setActiveTab] = useState('general');
 | 
			
		||||
@@ -46,12 +49,14 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
  const [newWallpaperName, setNewWallpaperName] = useState('');
 | 
			
		||||
  const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
 | 
			
		||||
  const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
 | 
			
		||||
  const [chromeStorageAvailable, setChromeStorageAvailable] = useState(false);
 | 
			
		||||
  const menuRef = useRef<HTMLDivElement>(null);
 | 
			
		||||
  const fileInputRef = useRef<HTMLInputElement>(null);
 | 
			
		||||
  const isSaving = useRef(false);
 | 
			
		||||
  const [isVisible, setIsVisible] = useState(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setChromeStorageAvailable(checkChromeStorageLocalAvailable());
 | 
			
		||||
    const storedUserWallpapers = localStorage.getItem('userWallpapers');
 | 
			
		||||
    if (storedUserWallpapers) {
 | 
			
		||||
      setUserWallpapers(JSON.parse(storedUserWallpapers));
 | 
			
		||||
@@ -59,7 +64,6 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // A small timeout to allow the component to mount before starting the transition
 | 
			
		||||
    const timer = setTimeout(() => {
 | 
			
		||||
      setIsVisible(true);
 | 
			
		||||
    }, 10);
 | 
			
		||||
@@ -69,7 +73,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (!isSaving.current) {
 | 
			
		||||
        onWallpaperChange({ backgroundUrls: currentConfig.backgroundUrls });
 | 
			
		||||
        onWallpaperChange({ currentWallpapers: currentConfig.currentWallpapers });
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -78,12 +82,15 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
    setIsVisible(false);
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      onClose();
 | 
			
		||||
    }, 300); // This duration should match the transition duration
 | 
			
		||||
    }, 300);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement> | { target: { name: string; value: string | string[] } }) => {
 | 
			
		||||
    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];
 | 
			
		||||
      setConfig({
 | 
			
		||||
        ...config,
 | 
			
		||||
@@ -101,8 +108,13 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    onWallpaperChange({ backgroundUrls: config.backgroundUrls });
 | 
			
		||||
  }, [config.backgroundUrls]);
 | 
			
		||||
    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) => {
 | 
			
		||||
    setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
 | 
			
		||||
@@ -162,21 +174,21 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleAddWallpaper = () => {
 | 
			
		||||
    if (newWallpaperName.trim() === '' || newWallpaperUrl.trim() === '') return;
 | 
			
		||||
 | 
			
		||||
    const newWallpaper: Wallpaper = {
 | 
			
		||||
      name: newWallpaperName,
 | 
			
		||||
      url: newWallpaperUrl,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
			
		||||
    setUserWallpapers(updatedUserWallpapers);
 | 
			
		||||
    localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
			
		||||
    setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, newWallpaperUrl] });
 | 
			
		||||
 | 
			
		||||
    setNewWallpaperName('');
 | 
			
		||||
    setNewWallpaperUrl('');
 | 
			
		||||
  const handleAddWallpaper = async () => {
 | 
			
		||||
    if (newWallpaperUrl.trim() === '') return;
 | 
			
		||||
    try {
 | 
			
		||||
      const finalName = await addWallpaperToChromeStorageLocal(newWallpaperName, newWallpaperUrl);
 | 
			
		||||
      const newWallpaper: Wallpaper = { name: finalName };
 | 
			
		||||
      const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
			
		||||
      setUserWallpapers(updatedUserWallpapers);
 | 
			
		||||
      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
			
		||||
      setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
 | 
			
		||||
      setNewWallpaperName('');
 | 
			
		||||
      setNewWallpaperUrl('');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      alert('Error adding wallpaper. Please check the URL and try again.');
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
@@ -186,39 +198,43 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
        alert('File size exceeds 4MB. Please choose a smaller file.');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const reader = new FileReader();
 | 
			
		||||
      reader.onload = () => {
 | 
			
		||||
      reader.onload = async () => {
 | 
			
		||||
        const base64 = reader.result as string;
 | 
			
		||||
        if (base64.length > 4.5 * 1024 * 1024) {
 | 
			
		||||
          alert('The uploaded image is too large. Please choose a smaller file.');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const updatedUserWallpapers = userWallpapers.filter(w => !w.base64);
 | 
			
		||||
        const newWallpaper: Wallpaper = {
 | 
			
		||||
          name: file.name,
 | 
			
		||||
          base64,
 | 
			
		||||
        };
 | 
			
		||||
        setUserWallpapers([...updatedUserWallpapers, newWallpaper]);
 | 
			
		||||
        localStorage.setItem('userWallpapers', JSON.stringify([...updatedUserWallpapers, newWallpaper]));
 | 
			
		||||
        setConfig({ ...config, backgroundUrls: [...config.backgroundUrls, base64] });
 | 
			
		||||
        try {
 | 
			
		||||
          const finalName = await addWallpaperToChromeStorageLocal(file.name, base64);
 | 
			
		||||
          const newWallpaper: Wallpaper = { name: finalName };
 | 
			
		||||
          const updatedUserWallpapers = [...userWallpapers, newWallpaper];
 | 
			
		||||
          setUserWallpapers(updatedUserWallpapers);
 | 
			
		||||
          localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
			
		||||
          setConfig({ ...config, currentWallpapers: [...config.currentWallpapers, newWallpaper.name] });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          alert('Error adding wallpaper. Please try again.');
 | 
			
		||||
          console.error(error);
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      reader.readAsDataURL(file);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleDeleteWallpaper = (wallpaper: Wallpaper) => {
 | 
			
		||||
    const wallpaperIdentifier = wallpaper.url || wallpaper.base64;
 | 
			
		||||
    const updatedUserWallpapers = userWallpapers.filter(w => (w.url || w.base64) !== wallpaperIdentifier);
 | 
			
		||||
    setUserWallpapers(updatedUserWallpapers);
 | 
			
		||||
    localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
			
		||||
 | 
			
		||||
    const newBackgroundUrls = config.backgroundUrls.filter((url: string) => url !== wallpaperIdentifier);
 | 
			
		||||
    
 | 
			
		||||
    const newConfig = { ...config, backgroundUrls: newBackgroundUrls };
 | 
			
		||||
    setConfig(newConfig);
 | 
			
		||||
    onWallpaperChange({ backgroundUrls: newBackgroundUrls });
 | 
			
		||||
  const handleDeleteUserWallpaper = async (wallpaper: Wallpaper) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await removeWallpaperFromChromeStorageLocal(wallpaper.name);
 | 
			
		||||
      const updatedUserWallpapers = userWallpapers.filter(w => w.name !== wallpaper.name);
 | 
			
		||||
      setUserWallpapers(updatedUserWallpapers);
 | 
			
		||||
      localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
 | 
			
		||||
      const newcurrentWallpapers = config.currentWallpapers.filter((name: string) => name !== wallpaper.name);
 | 
			
		||||
      const newConfig = { ...config, currentWallpapers: newcurrentWallpapers };
 | 
			
		||||
      setConfig(newConfig);
 | 
			
		||||
      onWallpaperChange({ currentWallpapers: newcurrentWallpapers });
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      alert('Error deleting wallpaper. Please try again.');
 | 
			
		||||
      console.error(error);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const allWallpapers = [...baseWallpapers, ...userWallpapers];
 | 
			
		||||
@@ -365,35 +381,17 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
              <div className="flex items-center justify-between">
 | 
			
		||||
                <label className="text-slate-300 text-sm font-semibold">Background</label>
 | 
			
		||||
                <Dropdown
 | 
			
		||||
                  name="backgroundUrls"
 | 
			
		||||
                  value={config.backgroundUrls}
 | 
			
		||||
                  name="currentWallpapers"
 | 
			
		||||
                  value={config.currentWallpapers}
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                  multiple
 | 
			
		||||
                  options={allWallpapers.map(w => ({
 | 
			
		||||
                    value: w.url || w.base64 || '', 
 | 
			
		||||
                    label: (
 | 
			
		||||
                      <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>
 | 
			
		||||
                    )
 | 
			
		||||
                    value: w.name,
 | 
			
		||||
                    label: w.name
 | 
			
		||||
                  }))}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              {Array.isArray(config.backgroundUrls) && config.backgroundUrls.length > 1 && (
 | 
			
		||||
              {Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
 | 
			
		||||
                <div className="flex items-center justify-between">
 | 
			
		||||
                  <label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
 | 
			
		||||
                  <Dropdown
 | 
			
		||||
@@ -456,48 +454,71 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
 | 
			
		||||
                  <span>{config.wallpaperOpacity}%</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div>
 | 
			
		||||
                <h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
 | 
			
		||||
                <div className="flex flex-col gap-2">
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="text"
 | 
			
		||||
                    placeholder="Wallpaper Name"
 | 
			
		||||
                    value={newWallpaperName}
 | 
			
		||||
                    onChange={(e) => setNewWallpaperName(e.target.value)}
 | 
			
		||||
                    className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
 | 
			
		||||
                  />
 | 
			
		||||
                  <div className="flex gap-2">
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="text"
 | 
			
		||||
                      placeholder="Image URL"
 | 
			
		||||
                      value={newWallpaperUrl}
 | 
			
		||||
                      onChange={(e) => setNewWallpaperUrl(e.target.value)}
 | 
			
		||||
                      className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
 | 
			
		||||
                    />
 | 
			
		||||
                    <button
 | 
			
		||||
                      onClick={handleAddWallpaper}
 | 
			
		||||
                      className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
 | 
			
		||||
                    >
 | 
			
		||||
                      Add
 | 
			
		||||
                    </button>
 | 
			
		||||
              {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 className="flex items-center justify-center w-full">
 | 
			
		||||
                    <label
 | 
			
		||||
                      htmlFor="file-upload"
 | 
			
		||||
                      className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
 | 
			
		||||
                    >
 | 
			
		||||
                      <div className="flex flex-col items-center justify-center pt-5 pb-6">
 | 
			
		||||
                        <svg className="w-8 h-8 mb-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
 | 
			
		||||
                          <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
 | 
			
		||||
                        </svg>
 | 
			
		||||
                        <p className="mb-2 text-sm text-gray-400"><span className="font-semibold">Click to upload</span> or drag and drop</p>
 | 
			
		||||
                        <p className="text-xs text-gray-400">PNG, JPG, WEBP, etc.</p>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <h3 className="text-slate-300 text-sm font-semibold mb-2">Add New Wallpaper</h3>
 | 
			
		||||
                    <div className="flex flex-col gap-2">
 | 
			
		||||
                      <input
 | 
			
		||||
                        type="text"
 | 
			
		||||
                        placeholder="Wallpaper Name (optional for URLs)"
 | 
			
		||||
                        value={newWallpaperName}
 | 
			
		||||
                        onChange={(e) => setNewWallpaperName(e.target.value)}
 | 
			
		||||
                        className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
 | 
			
		||||
                      />
 | 
			
		||||
                      <div className="flex gap-2">
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="text"
 | 
			
		||||
                          placeholder="Image URL"
 | 
			
		||||
                          value={newWallpaperUrl}
 | 
			
		||||
                          onChange={(e) => setNewWallpaperUrl(e.target.value)}
 | 
			
		||||
                          className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
 | 
			
		||||
                        />
 | 
			
		||||
                        <button
 | 
			
		||||
                          onClick={handleAddWallpaper}
 | 
			
		||||
                          className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg"
 | 
			
		||||
                        >
 | 
			
		||||
                          Add
 | 
			
		||||
                        </button>
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <input id="file-upload" type="file" className="hidden" onChange={handleFileUpload} ref={fileInputRef} />
 | 
			
		||||
                    </label>
 | 
			
		||||
                      <div className="flex items-center justify-center w-full">
 | 
			
		||||
                        <label
 | 
			
		||||
                          htmlFor="file-upload"
 | 
			
		||||
                          className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-white/5 border-white/20 hover:bg-white/10"
 | 
			
		||||
                        >
 | 
			
		||||
                          <div className="flex flex-col items-center justify-center pt-5 pb-6">
 | 
			
		||||
                            <svg className="w-8 h-8 mb-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
 | 
			
		||||
                              <path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
 | 
			
		||||
                            </svg>
 | 
			
		||||
                            <p className="mb-2 text-sm text-gray-400"><span className="font-semibold">Click to upload</span> or drag and drop</p>
 | 
			
		||||
                            <p className="text-xs text-gray-400">PNG, JPG, WEBP, etc.</p>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <input id="file-upload" type="file" className="hidden" onChange={handleFileUpload} ref={fileInputRef} />
 | 
			
		||||
                        </label>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react';
 | 
			
		||||
import { useState, useEffect } from 'react';
 | 
			
		||||
import { Server } from '../types';
 | 
			
		||||
import ping from './utils/jsping.js';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface ToggleSwitchProps {
 | 
			
		||||
  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 { getWebsiteIcon } from './utils/iconService';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import { Website } from '../types';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface WebsiteTileProps {
 | 
			
		||||
  website: Website;
 | 
			
		||||
  isEditing: boolean;
 | 
			
		||||
@@ -28,9 +27,9 @@ const getTileSizeClass = (size: string | undefined) => {
 | 
			
		||||
const getIconPixelSize = (size: string | undefined): number => {
 | 
			
		||||
  switch (size) {
 | 
			
		||||
    case 'small':
 | 
			
		||||
      return 32;
 | 
			
		||||
      return 34;
 | 
			
		||||
    case 'medium':
 | 
			
		||||
      return 40;
 | 
			
		||||
      return 42;
 | 
			
		||||
    case 'large':
 | 
			
		||||
      return 48;
 | 
			
		||||
    default:
 | 
			
		||||
@@ -92,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={`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>
 | 
			
		||||
          <span className={`text-slate-100 font-medium text-base tracking-wide text-center transition-all duration-300 ease-in ${isLoading ? 'text-sm' : ''}`}>
 | 
			
		||||
            {website.name}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import WebsiteTile from '../WebsiteTile';
 | 
			
		||||
import { Category, Website } from '../../types';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface ConfigurationButtonProps {
 | 
			
		||||
  onClick: () => void;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface EditButtonProps {
 | 
			
		||||
  isEditing: boolean;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import Clock from '../Clock';
 | 
			
		||||
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'));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										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: {},
 | 
			
		||||
  },
 | 
			
		||||
  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 {
 | 
			
		||||
  title: string;
 | 
			
		||||
  subtitle: string;
 | 
			
		||||
  backgroundUrls: string[];
 | 
			
		||||
  currentWallpapers: string[];
 | 
			
		||||
  wallpaperFrequency: string;
 | 
			
		||||
  wallpaperBlur: number;
 | 
			
		||||
  wallpaperBrightness: number;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user