Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
c60cb24dd4 | |||
2f3949c2e3 | |||
9b818b05f9 | |||
008d2321e5 |
@@ -6,13 +6,15 @@ on:
|
|||||||
- v*
|
- v*
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
zip-file: vision-start-${{ gitea.ref_name }}.zip
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup zip
|
- name: Setup required tools
|
||||||
run: sudo apt-get install zip -y
|
run: sudo apt-get install zip jq curl -y
|
||||||
- name: Install JS dependencies
|
- name: Install JS dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Run build
|
- name: Run build
|
||||||
@@ -24,9 +26,65 @@ jobs:
|
|||||||
mv manifest.json vision-start/
|
mv manifest.json vision-start/
|
||||||
- name: Create zip archive
|
- name: Create zip archive
|
||||||
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
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
|
- name: Release zip
|
||||||
uses: akkuman/gitea-release-action@v1
|
uses: akkuman/gitea-release-action@v1
|
||||||
with:
|
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 }}
|
name: ${{ gitea.ref_name }}
|
||||||
tag_name: ${{ gitea.ref_name }}
|
tag_name: ${{ gitea.ref_name }}
|
||||||
files: vision-start-${{ gitea.ref_name }}.zip
|
files: vision-start-${{ gitea.ref_name }}.zip
|
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 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,62 +66,12 @@ 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 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));
|
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);
|
||||||
};
|
};
|
||||||
@@ -201,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];
|
||||||
@@ -259,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)} />
|
||||||
|
|
||||||
@@ -305,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
|
||||||
|
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.
|
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
|
||||||
* **Abstract**
|
2. Download the latest `vision-start-[version].zip` file
|
||||||
* **Abstract Red**
|
3. Extract the zip file, you will have a `vision-start` folder
|
||||||
* **Beach**
|
4. Go to chrome://extensions/
|
||||||
* **Dark**
|
5. Enable "Developer mode" in the top right corner
|
||||||
* **Mountain**
|
6. Click on "Load unpacked" and select the `vision-start` folder you extracted in step 3
|
||||||
* **Waves**
|
7. The extension should now be installed! Just open a new tab to see it in action.
|
||||||
|
|
||||||
## Features
|
## 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.
|
* **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 :(
|
* **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
|
## Running Locally
|
||||||
|
|
||||||
**Prerequisites:** Node.js
|
**Prerequisites:** Node.js
|
||||||
@@ -49,10 +55,10 @@ npm run dev
|
|||||||
|
|
||||||
* [x] Multiple Wallpapers
|
* [x] Multiple Wallpapers
|
||||||
* [x] Remake icons
|
* [x] Remake icons
|
||||||
* [] Increase offline compatibility (might not be possible)
|
* [/] Increase offline compatibility (might not be possible)
|
||||||
* Use chrome.storage.local for user wallpapers -- this one is
|
- [x] Use chrome.storage.local for user wallpapers -- this one is
|
||||||
* Use chrome.storage.local for some logos -- a bit hard
|
- [ ] 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
|
- Some logos have CORS enabled, we can add `"<all_urls>"` to the manifest.json file and cache them on storage local
|
||||||
* Dynamic Weather Widget
|
* Dynamic Weather Widget
|
||||||
* A box with information about the current weather, with manual entry on the location
|
* 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
|
* 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';
|
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,6 +5,7 @@ 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;
|
||||||
@@ -37,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');
|
||||||
@@ -46,12 +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 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));
|
||||||
@@ -59,7 +64,6 @@ 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);
|
||||||
@@ -69,7 +73,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (!isSaving.current) {
|
if (!isSaving.current) {
|
||||||
onWallpaperChange({ backgroundUrls: currentConfig.backgroundUrls });
|
onWallpaperChange({ currentWallpapers: currentConfig.currentWallpapers });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
@@ -78,12 +82,15 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
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,
|
||||||
@@ -101,8 +108,13 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onWallpaperChange({ backgroundUrls: config.backgroundUrls });
|
onWallpaperChange({ currentWallpapers: config.currentWallpapers });
|
||||||
}, [config.backgroundUrls]);
|
// 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 } });
|
||||||
@@ -162,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>) => {
|
||||||
@@ -186,39 +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 };
|
||||||
|
|
||||||
const newConfig = { ...config, backgroundUrls: newBackgroundUrls };
|
|
||||||
setConfig(newConfig);
|
setConfig(newConfig);
|
||||||
onWallpaperChange({ backgroundUrls: newBackgroundUrls });
|
onWallpaperChange({ currentWallpapers: newcurrentWallpapers });
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error deleting wallpaper. Please try again.');
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
||||||
@@ -365,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
|
||||||
@@ -456,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"
|
||||||
@@ -498,6 +517,8 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
</div>
|
</div>
|
||||||
</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 { 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;
|
||||||
@@ -28,9 +27,9 @@ const getTileSizeClass = (size: string | undefined) => {
|
|||||||
const getIconPixelSize = (size: string | undefined): number => {
|
const getIconPixelSize = (size: string | undefined): number => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'small':
|
case 'small':
|
||||||
return 32;
|
return 34;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 40;
|
return 42;
|
||||||
case 'large':
|
case 'large':
|
||||||
return 48;
|
return 48;
|
||||||
default:
|
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={`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';
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
interface ConfigurationButtonProps {
|
interface ConfigurationButtonProps {
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
@@ -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'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
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