Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
008d2321e5 | |||
3aff7ffed6 | |||
9e80818fc5 | |||
140119cb99 | |||
af7b778561 | |||
2849ed3bb2 | |||
f1c1b0c6c6 |
@@ -1,29 +1,18 @@
|
|||||||
name: Check scripts syntax
|
name: Build and Release
|
||||||
on: [push]
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-release:
|
build:
|
||||||
|
if: gitea.event_name == 'push'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup zip
|
|
||||||
run: |
|
|
||||||
sudo apt-get install zip -y
|
|
||||||
- name: Install JS dependencies
|
- name: Install JS dependencies
|
||||||
run: |
|
run: npm install
|
||||||
npm install
|
|
||||||
- name: Run scripts
|
|
||||||
run: |
|
|
||||||
bash download-icons.sh
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: |
|
run: npm run build
|
||||||
npm run build
|
|
||||||
zip -r vision-start.zip dist
|
|
||||||
- name: Release zip
|
|
||||||
uses: akkuman/gitea-release-action@v1
|
|
||||||
with:
|
|
||||||
name: latest
|
|
||||||
tag_name: latest
|
|
||||||
files: |-
|
|
||||||
vision-start.zip
|
|
||||||
|
90
.gitea/workflows/release.yaml
Normal file
90
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
name: Build and Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
zip-file: vision-start-${{ gitea.ref_name }}.zip
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup required tools
|
||||||
|
run: sudo apt-get install zip jq curl -y
|
||||||
|
- name: Install JS dependencies
|
||||||
|
run: npm install
|
||||||
|
- name: Run build
|
||||||
|
run: npm run build
|
||||||
|
- name: Prepare release
|
||||||
|
run: |
|
||||||
|
bash scripts/prepare_release.sh
|
||||||
|
mv dist vision-start/
|
||||||
|
mv manifest.json vision-start/
|
||||||
|
- name: Create zip archive
|
||||||
|
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
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@v4
|
||||||
|
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@v4
|
||||||
|
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
|
30
App.tsx
30
App.tsx
@@ -94,7 +94,8 @@ const App: React.FC = () => {
|
|||||||
const updateWallpaper = () => {
|
const updateWallpaper = () => {
|
||||||
const availableWallpapers = allWallpapers.filter(w => config.backgroundUrls.includes(w.url || w.base64));
|
const availableWallpapers = allWallpapers.filter(w => config.backgroundUrls.includes(w.url || w.base64));
|
||||||
if (availableWallpapers.length > 0) {
|
if (availableWallpapers.length > 0) {
|
||||||
const currentIndex = availableWallpapers.findIndex(w => (w.url || w.base64) === wallpaperState.current);
|
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 nextIndex = (currentIndex + 1) % availableWallpapers.length;
|
||||||
const newWallpaper = availableWallpapers[nextIndex];
|
const newWallpaper = availableWallpapers[nextIndex];
|
||||||
const newWallpaperUrl = newWallpaper.url || newWallpaper.base64;
|
const newWallpaperUrl = newWallpaper.url || newWallpaper.base64;
|
||||||
@@ -102,24 +103,20 @@ const App: React.FC = () => {
|
|||||||
localStorage.setItem('wallpaperState', JSON.stringify({ current: newWallpaper.name, lastChanged: new Date().toISOString() }));
|
localStorage.setItem('wallpaperState', JSON.stringify({ current: newWallpaper.name, lastChanged: new Date().toISOString() }));
|
||||||
} else {
|
} else {
|
||||||
setCurrentWallpaper('');
|
setCurrentWallpaper('');
|
||||||
|
localStorage.removeItem('wallpaperState');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Date.now() - lastChanged > frequency) {
|
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();
|
updateWallpaper();
|
||||||
|
} else if (currentWallpaperDetails) {
|
||||||
|
setCurrentWallpaper(currentWallpaperDetails.url || currentWallpaperDetails.base64 || '');
|
||||||
} else {
|
} else {
|
||||||
const currentWallpaperName = wallpaperState.current;
|
// Fallback for when there's no valid wallpaper state
|
||||||
const wallpaper = allWallpapers.find(w => w.name === currentWallpaperName);
|
updateWallpaper();
|
||||||
if (wallpaper) {
|
|
||||||
setCurrentWallpaper(wallpaper.url || wallpaper.base64 || '');
|
|
||||||
} else {
|
|
||||||
const firstWallpaperUrl = config.backgroundUrls[0] || '';
|
|
||||||
const firstWallpaper = allWallpapers.find(w => (w.url || w.base64) === firstWallpaperUrl);
|
|
||||||
setCurrentWallpaper(firstWallpaperUrl);
|
|
||||||
if (firstWallpaper) {
|
|
||||||
localStorage.setItem('wallpaperState', JSON.stringify({ current: firstWallpaper.name, lastChanged: new Date().toISOString() }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [config.backgroundUrls, config.wallpaperFrequency, allWallpapers]);
|
}, [config.backgroundUrls, config.wallpaperFrequency, allWallpapers]);
|
||||||
|
|
||||||
@@ -133,6 +130,10 @@ const App: React.FC = () => {
|
|||||||
setIsConfigModalOpen(false);
|
setIsConfigModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWallpaperChange = (newConfig: Partial<Config>) => {
|
||||||
|
setConfig(prev => ({ ...prev, ...newConfig }));
|
||||||
|
};
|
||||||
|
|
||||||
const handleSaveWebsite = (website: Partial<Website>) => {
|
const handleSaveWebsite = (website: Partial<Website>) => {
|
||||||
if (editingWebsite) {
|
if (editingWebsite) {
|
||||||
const newCategories = categories.map(category => ({
|
const newCategories = categories.map(category => ({
|
||||||
@@ -341,6 +342,7 @@ const App: React.FC = () => {
|
|||||||
currentConfig={config}
|
currentConfig={config}
|
||||||
onClose={() => setIsConfigModalOpen(false)}
|
onClose={() => setIsConfigModalOpen(false)}
|
||||||
onSave={handleSaveConfig}
|
onSave={handleSaveConfig}
|
||||||
|
onWallpaperChange={handleWallpaperChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
101
README.md
101
README.md
@@ -1,22 +1,95 @@
|
|||||||
# Vision Start
|
# Vision Start
|
||||||
#### Small startpage
|
#### A glassmorphism-looking like, modern and customizable startpage built with React.
|
||||||
|
|
||||||
## Predefined themes
|
## Screenshots
|
||||||
|
|
||||||
1. Abstract
|

|
||||||
2. Aurora (Vista vibes)
|

|
||||||
3. Mountain
|

|
||||||
|
|
||||||
## Run Locally
|
## Installing
|
||||||
|
|
||||||
**Prerequisites:** Node.js
|
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.
|
||||||
|
|
||||||
1. Install dependencies:
|
## Backgrounds
|
||||||
`npm install`
|
|
||||||
2. Run the app:
|
|
||||||
`npm run dev`
|
|
||||||
|
|
||||||
## to-do
|
It comes with a selection of some nice pre-defined backgrounds. You can also upload up to one image to it.
|
||||||
* [] Multiple wallpapers
|
|
||||||
|
* **Abstract**
|
||||||
|
* **Abstract Red**
|
||||||
|
* **Beach**
|
||||||
|
* **Dark**
|
||||||
|
* **Mountain**
|
||||||
|
* **Waves**
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* **Customizable Website Tiles:** Add, edit, and organize your favorite websites for quick access.
|
||||||
|
* **Elegant Clock:** A clock because all startpages have one.
|
||||||
|
* **Server Status Widgets:** Monitor the status of services directly from the startpage.
|
||||||
|
* **Glassmorphism UI:** A modern and stylish interface with a frosted glass effect.
|
||||||
|
* **Icon Library:** It uses the [Dashboard Icon library](https://dashboardicons.com/) for a better look and feel. It also supports auto-fetch for some websites.
|
||||||
|
* **Future**: a long to do list :(
|
||||||
|
|
||||||
|
## Running Locally
|
||||||
|
|
||||||
|
**Prerequisites:** Node.js
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.com/ivan/vision-start.git
|
||||||
|
cd vision-start
|
||||||
|
```
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
3. Run the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## To-do
|
||||||
|
|
||||||
|
* [x] Multiple Wallpapers
|
||||||
* [x] Remake icons
|
* [x] Remake icons
|
||||||
* [] Increase offline compatibility
|
* [] 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
|
||||||
|
* Dynamic Weather Widget
|
||||||
|
* A box with information about the current weather, with manual entry on the location
|
||||||
|
* Display current temperature, weather condition (e.g., "Sunny," "Cloudy"), and a corresponding icon
|
||||||
|
* Optionally, show a 3-day forecast when clicked or hovered
|
||||||
|
* Search Bar Widget
|
||||||
|
* Positioned to the right or left side of the clock, display a nice search bar
|
||||||
|
* Behaviour:
|
||||||
|
* When not in focus, it could be highly transparent with just a faint border and a search icon.
|
||||||
|
* When clicked, it would smoothly expand and become slightly more opaque, with a soft glow around the border (similar to the existing ones)
|
||||||
|
* Config to allow changing the default search engine
|
||||||
|
* Draggable & Resizable Grid System
|
||||||
|
* Allow users to drag and drop all widgets (Clock, Website Tiles, Weather, Title, etc.) into any position on a grid
|
||||||
|
* Notes / Scratchpad Widget
|
||||||
|
* A simple text area that saves its content to local storage automatically.
|
||||||
|
* Maybe some extra formatting (bold, italic, increase font size, etc).
|
||||||
|
* Theme-ing
|
||||||
|
* A Light/Dark Mode toggle
|
||||||
|
* Custom Accent Colors
|
||||||
|
* Selection of 6-8 accent colors that are guaranteed to look good with both Light and Dark themes
|
||||||
|
* Define CSS variables for the accent color
|
||||||
|
* Dynamic Wallpaper-Based Theming
|
||||||
|
* Automatically adapt the UI's accent color to match the current wallpaper
|
||||||
|
* Minimal feel toggle
|
||||||
|
* Disable title & subtitle and search widget
|
||||||
|
* Tiles become small stylish lines
|
||||||
|
|
||||||
|
From a technical side:
|
||||||
|
* Refactor everything :(
|
||||||
|
* Add small nginx demo (with docker)
|
||||||
|
@@ -10,9 +10,10 @@ interface ConfigurationModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onSave: (config: any) => void;
|
onSave: (config: any) => void;
|
||||||
currentConfig: any;
|
currentConfig: any;
|
||||||
|
onWallpaperChange: (newConfig: Partial<any>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave, currentConfig }) => {
|
const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave, currentConfig, onWallpaperChange }) => {
|
||||||
const [config, setConfig] = useState({
|
const [config, setConfig] = useState({
|
||||||
...currentConfig,
|
...currentConfig,
|
||||||
titleSize: currentConfig.titleSize || 'medium',
|
titleSize: currentConfig.titleSize || 'medium',
|
||||||
@@ -47,6 +48,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
|
const [userWallpapers, setUserWallpapers] = useState<Wallpaper[]>([]);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const isSaving = useRef(false);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -64,6 +66,14 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (!isSaving.current) {
|
||||||
|
onWallpaperChange({ backgroundUrls: currentConfig.backgroundUrls });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsVisible(false);
|
setIsVisible(false);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -90,6 +100,10 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onWallpaperChange({ backgroundUrls: config.backgroundUrls });
|
||||||
|
}, [config.backgroundUrls]);
|
||||||
|
|
||||||
const handleClockToggleChange = (checked: boolean) => {
|
const handleClockToggleChange = (checked: boolean) => {
|
||||||
setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
|
setConfig({ ...config, clock: { ...config.clock, enabled: checked } });
|
||||||
};
|
};
|
||||||
@@ -201,7 +215,10 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
|
localStorage.setItem('userWallpapers', JSON.stringify(updatedUserWallpapers));
|
||||||
|
|
||||||
const newBackgroundUrls = config.backgroundUrls.filter((url: string) => url !== wallpaperIdentifier);
|
const newBackgroundUrls = config.backgroundUrls.filter((url: string) => url !== wallpaperIdentifier);
|
||||||
setConfig({ ...config, backgroundUrls: newBackgroundUrls });
|
|
||||||
|
const newConfig = { ...config, backgroundUrls: newBackgroundUrls };
|
||||||
|
setConfig(newConfig);
|
||||||
|
onWallpaperChange({ backgroundUrls: newBackgroundUrls });
|
||||||
};
|
};
|
||||||
|
|
||||||
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
const allWallpapers = [...baseWallpapers, ...userWallpapers];
|
||||||
@@ -629,7 +646,7 @@ const ConfigurationModal: React.FC<ConfigurationModalProps> = ({ onClose, onSave
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-8 border-t border-white/10">
|
<div className="p-8 border-t border-white/10">
|
||||||
<div className="flex justify-end gap-4">
|
<div className="flex justify-end gap-4">
|
||||||
<button onClick={() => onSave(config)} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
|
<button onClick={() => { isSaving.current = true; onSave(config); }} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
|
||||||
Save & Close
|
Save & Close
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
|
<button onClick={handleClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
|
||||||
|
@@ -23,18 +23,34 @@ const getTileSizeClass = (size: string | undefined) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getIconSize = (size: string | undefined) => {
|
|
||||||
|
// Returns normal icon size in px
|
||||||
|
const getIconPixelSize = (size: string | undefined): number => {
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'small':
|
case 'small':
|
||||||
return 8;
|
return 32;
|
||||||
case 'medium':
|
case 'medium':
|
||||||
return 10;
|
return 40;
|
||||||
case 'large':
|
case 'large':
|
||||||
return 12;
|
return 48;
|
||||||
default:
|
default:
|
||||||
return 10;
|
return 40;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Returns loading icon size in px
|
||||||
|
const getIconLoadingPixelSize = (size: string | undefined): number => {
|
||||||
|
switch (size) {
|
||||||
|
case 'small':
|
||||||
|
return 24;
|
||||||
|
case 'medium':
|
||||||
|
return 32;
|
||||||
|
case 'large':
|
||||||
|
return 40;
|
||||||
|
default:
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, onMove, tileSize }) => {
|
const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, onMove, tileSize }) => {
|
||||||
|
|
||||||
@@ -54,8 +70,8 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
|
|||||||
// }, 3500); // Small delay to show spinner before navigation
|
// }, 3500); // Small delay to show spinner before navigation
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconSizeClass = `w-${getIconSize(tileSize)} h-${getIconSize(tileSize)}`;
|
const iconSizeClass = `w-[${getIconPixelSize(tileSize)}px] h-[${getIconPixelSize(tileSize)}px]`;
|
||||||
const iconSizeLoadingClass = `w-${getIconSize(tileSize) - 4} h-${getIconSize(tileSize) - 4}`;
|
const iconSizeLoadingClass = `w-[${getIconLoadingPixelSize(tileSize)}px] h-[${getIconLoadingPixelSize(tileSize)}px]`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-300 ease-in-out`}>
|
<div className={`relative ${getTileSizeClass(tileSize)} transition-all duration-300 ease-in-out`}>
|
||||||
@@ -86,14 +102,14 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
|
|||||||
{isEditing && (
|
{isEditing && (
|
||||||
<div className="absolute bottom-2 left-0 right-0 flex justify-center gap-2">
|
<div className="absolute bottom-2 left-0 right-0 flex justify-center gap-2">
|
||||||
<button onClick={() => onMove(website, 'left')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-left" viewBox="0 0 16 16">
|
<button onClick={() => onMove(website, 'left')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-left" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/>
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z" />
|
||||||
</svg></button>
|
</svg></button>
|
||||||
<button onClick={() => onEdit(website)} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
<button onClick={() => onEdit(website)} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||||
</svg></button>
|
</svg></button>
|
||||||
<button onClick={() => onMove(website, 'right')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-right" viewBox="0 0 16 16">
|
<button onClick={() => onMove(website, 'right')} className="text-white/50 hover:text-white transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-arrow-right" viewBox="0 0 16 16">
|
||||||
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
|
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z" />
|
||||||
</svg></button>
|
</svg></button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -41,7 +41,7 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
|
|||||||
className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
className={`ml-2 text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" className="bi bi-pencil" viewBox="0 0 16 16">
|
||||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zM1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -63,8 +63,8 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
|
|||||||
className={`text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
className={`text-white/50 hover:text-white transition-all duration-300 ease-in-out transform ${isEditing ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" className="bi bi-plus-circle" viewBox="0 0 16 16">
|
||||||
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
|
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z" />
|
||||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
@@ -11,9 +11,10 @@ const ConfigurationButton: React.FC<ConfigurationButtonProps> = ({ onClick }) =>
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
className="bg-black/25 backdrop-blur-md border border-white/10 rounded-xl p-3 text-white flex items-center gap-2 hover:bg-white/25 transition-colors"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" className="bi bi-gear-wide" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path d="M8.932.727c-.243-.97-1.62-.97-1.864 0l-.071.286a.96.96 0 0 1-1.622.434l-.205-.211c-.695-.719-1.888-.03-1.613.931l.08.284a.96.96 0 0 1-1.186 1.187l-.284-.081c-.96-.275-1.65.918-.931 1.613l.211.205a.96.96 0 0 1-.434 1.622l-.286.071c-.97.243-.97 1.62 0 1.864l.286.071a.96.96 0 0 1 .434 1.622l-.211.205c-.719.695-.03 1.888.931 1.613l.284-.08a.96.96 0 0 1 1.187 1.187l-.081.283c-.275.96.918 1.65 1.613.931l.205-.211a.96.96 0 0 1 1.622.434l.071.286c.243.97 1.62.97 1.864 0l.071-.286a.96.96 0 0 1 1.622-.434l.205.211c.695.719 1.888.03 1.613-.931l-.08-.284a.96.96 0 0 1 1.187-1.187l.283.081c.96.275 1.65-.918-.931-1.613l-.211-.205a.96.96 0 0 1 .434-1.622l.286-.071c.97-.243.97-1.62 0-1.864l-.286-.071a.96.96 0 0 1-.434-1.622l.211-.205c.719-.695.03-1.888-.931-1.613l-.284.08a.96.96 0 0 1-1.187-1.186l.081-.284c.275-.96-.918-1.65-1.613-.931l-.205.211a.96.96 0 0 1-1.622-.434zM8 12.997a4.998 4.998 0 1 1 0-9.995 4.998 4.998 0 0 1 0 9.996z"/>
|
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" fill="none"/>
|
||||||
</svg>
|
<path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09c.7 0 1.31-.4 1.51-1a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06c.51.51 1.31.61 1.82.33.51-.28 1-.81 1-1.51V3a2 2 0 1 1 4 0v.09c0 .7.49 1.23 1 1.51.51.28 1.31.18 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82c.2.6.81 1 1.51 1H21a2 2 0 1 1 0 4h-.09c-.7 0-1.31.4-1.51 1z"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -14,12 +14,16 @@ export const baseWallpapers: Wallpaper[] = [
|
|||||||
name: 'Beach',
|
name: 'Beach',
|
||||||
url: 'https://wallpapershome.com/images/pages/pic_h/615.jpg'
|
url: 'https://wallpapershome.com/images/pages/pic_h/615.jpg'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Dark',
|
||||||
|
url: 'https://i.imgur.com/qHlRO0s.jpeg'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Mountain',
|
name: 'Mountain',
|
||||||
url: 'https://i.imgur.com/yHfOZUd.jpeg'
|
url: 'https://i.imgur.com/yHfOZUd.jpeg'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Waves',
|
name: 'Waves',
|
||||||
url: '/waves.jpg',
|
url: 'https://i.imgur.com/E8uxZ7R.png',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
BIN
screenshots/configuration-abstract-red.png
Normal file
BIN
screenshots/configuration-abstract-red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 744 KiB |
BIN
screenshots/dark-page.png
Normal file
BIN
screenshots/dark-page.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/editing-abstract-red.png
Normal file
BIN
screenshots/editing-abstract-red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 MiB |
113
scripts/check_virustotal.sh
Executable file
113
scripts/check_virustotal.sh
Executable file
@@ -0,0 +1,113 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script to check a file against VirusTotal API
|
||||||
|
# Requires: curl, jq
|
||||||
|
# Environment variable: virustotal_apikey
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
FILE_PATH="${VIRUS_TOTAL_FILE:-vision-start.zip}"
|
||||||
|
API_KEY="${virustotal_apikey}"
|
||||||
|
BASE_URL="https://www.virustotal.com/vtapi/v2"
|
||||||
|
|
||||||
|
# Check if API key is set
|
||||||
|
if [ -z "$API_KEY" ]; then
|
||||||
|
echo "Error: virustotal_apikey environment variable is not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if file exists
|
||||||
|
if [ ! -f "$FILE_PATH" ]; then
|
||||||
|
echo "Error: File $FILE_PATH not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if required tools are available
|
||||||
|
if ! command -v curl &> /dev/null; then
|
||||||
|
echo "Error: curl is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo "Error: jq is required but not installed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Uploading $FILE_PATH to VirusTotal for analysis..."
|
||||||
|
|
||||||
|
# Upload file to VirusTotal
|
||||||
|
UPLOAD_RESPONSE=$(curl -s -X POST \
|
||||||
|
-F "apikey=$API_KEY" \
|
||||||
|
-F "file=@$FILE_PATH" \
|
||||||
|
"$BASE_URL/file/scan")
|
||||||
|
|
||||||
|
# Extract scan_id from response
|
||||||
|
SCAN_ID=$(echo "$UPLOAD_RESPONSE" | jq -r '.scan_id')
|
||||||
|
|
||||||
|
if [ "$SCAN_ID" == "null" ] || [ -z "$SCAN_ID" ]; then
|
||||||
|
echo "Error: Failed to upload file or get scan ID"
|
||||||
|
echo "Response: $UPLOAD_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "File uploaded successfully. Scan ID: $SCAN_ID"
|
||||||
|
echo "Waiting for analysis to complete..."
|
||||||
|
|
||||||
|
# Wait for analysis to complete and get results
|
||||||
|
MAX_ATTEMPTS=30
|
||||||
|
ATTEMPT=0
|
||||||
|
SLEEP_INTERVAL=10
|
||||||
|
|
||||||
|
while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do
|
||||||
|
echo "Checking analysis status (attempt $((ATTEMPT + 1))/$MAX_ATTEMPTS)..."
|
||||||
|
|
||||||
|
# Get scan report
|
||||||
|
REPORT_RESPONSE=$(curl -s -X POST \
|
||||||
|
-d "apikey=$API_KEY" \
|
||||||
|
-d "resource=$SCAN_ID" \
|
||||||
|
"$BASE_URL/file/report")
|
||||||
|
|
||||||
|
# Check if analysis is complete
|
||||||
|
RESPONSE_CODE=$(echo "$REPORT_RESPONSE" | jq -r '.response_code')
|
||||||
|
|
||||||
|
if [ "$RESPONSE_CODE" == "1" ]; then
|
||||||
|
# Analysis complete
|
||||||
|
echo "Analysis completed!"
|
||||||
|
|
||||||
|
# Extract results
|
||||||
|
POSITIVES=$(echo "$REPORT_RESPONSE" | jq -r '.positives')
|
||||||
|
TOTAL=$(echo "$REPORT_RESPONSE" | jq -r '.total')
|
||||||
|
PERMALINK=$(echo "$REPORT_RESPONSE" | jq -r '.permalink')
|
||||||
|
|
||||||
|
echo "Analysis URL: $PERMALINK"
|
||||||
|
echo "Detection ratio: $POSITIVES/$TOTAL"
|
||||||
|
|
||||||
|
# Check if file is safe
|
||||||
|
if [ "$POSITIVES" -eq 0 ]; then
|
||||||
|
echo "✅ File is clean (no threats detected)"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "❌ File contains threats ($POSITIVES detections out of $TOTAL scanners)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
elif [ "$RESPONSE_CODE" == "0" ]; then
|
||||||
|
# File not found or analysis not complete yet
|
||||||
|
echo "Analysis still in progress..."
|
||||||
|
elif [ "$RESPONSE_CODE" == "-2" ]; then
|
||||||
|
# Still queued for analysis
|
||||||
|
echo "File still queued for analysis..."
|
||||||
|
else
|
||||||
|
echo "Unexpected response code: $RESPONSE_CODE"
|
||||||
|
echo "Response: $REPORT_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ATTEMPT=$((ATTEMPT + 1))
|
||||||
|
if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then
|
||||||
|
sleep $SLEEP_INTERVAL
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Timeout: Analysis did not complete within expected time"
|
||||||
|
exit 1
|
Reference in New Issue
Block a user