Compare commits
6 Commits
254d8e26b6
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 48ec764880 | |||
| babd31548c | |||
| 95ae04ecd2 | |||
| 7a137abb66 | |||
| 023b2ffdc5 | |||
| c9dafd76d1 |
@@ -15,27 +15,38 @@ env:
|
||||
jobs:
|
||||
build:
|
||||
name: Build Vision Start
|
||||
if: gitea.event_name == 'push'
|
||||
runs-on: ubuntu-amd64
|
||||
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: Package dist as zip
|
||||
run: |
|
||||
cd dist
|
||||
zip -r ../vision-start-build.zip .
|
||||
bash scripts/prepare_release.sh
|
||||
npm run build
|
||||
|
||||
- name: Upload build artifact
|
||||
- name: Prepare release
|
||||
run: |
|
||||
mv dist vision-start/
|
||||
mv extension vision-start/
|
||||
mv manifest.json vision-start/
|
||||
|
||||
- name: Create zip archive
|
||||
run: |
|
||||
cd vision-start
|
||||
zip -r ../vision-start-${{ gitea.ref_name }}.zip *
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vision-start-build
|
||||
path: vision-start-build.zip
|
||||
retention-days: 30
|
||||
name: release-zip
|
||||
path: vision-start-${{ gitea.ref_name }}.zip
|
||||
|
||||
build_vision_start:
|
||||
name: Build Vision Start Image
|
||||
|
||||
@@ -19,19 +19,29 @@ jobs:
|
||||
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
|
||||
npm run build
|
||||
|
||||
- name: Prepare release
|
||||
run: |
|
||||
mv dist vision-start/
|
||||
mv extension vision-start/
|
||||
mv manifest.json vision-start/
|
||||
|
||||
- name: Create zip archive
|
||||
run: zip -r vision-start-${{ gitea.ref_name }}.zip vision-start
|
||||
run: |
|
||||
cd vision-start
|
||||
zip -r ../vision-start-${{ gitea.ref_name }}.zip *
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<div style="display: flex; justify-content: center; font-size: 2rem; font-weight: bold;">
|
||||
Vision Start
|
||||
<div style="display: flex; justify-content: center; align-items: center; font-size: 2rem; font-weight: bold;">
|
||||
<img src="extension/icons/vision-48.png" alt="Vision Start" width="32" height="32" style="margin-right: 1rem;"> Vision Start
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; font-size: 1.5rem;">
|
||||
A light liquid-glass, modern and customizable startpage built with React.
|
||||
A light, modern and customizable startpage built with React.
|
||||
</div>
|
||||
|
||||
<span style="display: block; text-align: center; font-size: 1.2rem;">Try it here: <a href="http://vision-start.ivanch.me">http://vision-start.ivanch.me</a></span>
|
||||
|
||||
@@ -13,15 +13,18 @@ interface WallpaperProps {
|
||||
wallpaperVersion: number;
|
||||
}
|
||||
|
||||
const MIN_WALLPAPER_FREQUENCY_MS = 60 * 60 * 1000;
|
||||
const MAX_WALLPAPER_FREQUENCY_MS = 48 * 60 * 60 * 1000;
|
||||
const DEFAULT_WALLPAPER_FREQUENCY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
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;
|
||||
if (!freq) return DEFAULT_WALLPAPER_FREQUENCY_MS;
|
||||
const match = freq.match(/^(\d+)(h|d)$/);
|
||||
if (!match) return DEFAULT_WALLPAPER_FREQUENCY_MS;
|
||||
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;
|
||||
const frequencyMs = unit === 'd' ? value * 24 * 60 * 60 * 1000 : value * 60 * 60 * 1000;
|
||||
return Math.min(MAX_WALLPAPER_FREQUENCY_MS, Math.max(MIN_WALLPAPER_FREQUENCY_MS, frequencyMs));
|
||||
};
|
||||
|
||||
const wallpaperUrlCache = new Map<string, string | undefined>();
|
||||
|
||||
@@ -21,6 +21,24 @@ const getRangeStyle = (value: number, min: number, max: number): RangeStyle => {
|
||||
return { '--range-progress': `${progress}%` };
|
||||
};
|
||||
|
||||
const MIN_WALLPAPER_FREQUENCY_HOURS = 1;
|
||||
const MAX_WALLPAPER_FREQUENCY_HOURS = 48;
|
||||
const DEFAULT_WALLPAPER_FREQUENCY_HOURS = 24;
|
||||
|
||||
const clampWallpaperFrequencyHours = (hours: number): number =>
|
||||
Math.min(MAX_WALLPAPER_FREQUENCY_HOURS, Math.max(MIN_WALLPAPER_FREQUENCY_HOURS, Math.round(hours)));
|
||||
|
||||
const getWallpaperFrequencyHours = (frequency: string): number => {
|
||||
const match = frequency.match(/^(\d+)(h|d)$/);
|
||||
if (!match) return DEFAULT_WALLPAPER_FREQUENCY_HOURS;
|
||||
const value = Number(match[1]);
|
||||
const hours = match[2] === 'd' ? value * 24 : value;
|
||||
return clampWallpaperFrequencyHours(hours);
|
||||
};
|
||||
|
||||
const formatWallpaperFrequency = (hours: number): string =>
|
||||
`${hours} ${hours === 1 ? 'hour' : 'hours'}`;
|
||||
|
||||
const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
@@ -35,6 +53,7 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
const [newWallpaperName, setNewWallpaperName] = useState('');
|
||||
const [newWallpaperUrl, setNewWallpaperUrl] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const wallpaperFrequencyHours = getWallpaperFrequencyHours(config.wallpaperFrequency);
|
||||
|
||||
const handleAddWallpaper = async () => {
|
||||
if (newWallpaperUrl.trim() === '') return;
|
||||
@@ -75,19 +94,25 @@ const ThemeTab: React.FC<ThemeTabProps> = ({
|
||||
{Array.isArray(config.currentWallpapers) && config.currentWallpapers.length > 1 && (
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<label className="text-slate-300 text-sm font-semibold">Change Frequency</label>
|
||||
<Dropdown
|
||||
name="wallpaperFrequency"
|
||||
value={config.wallpaperFrequency}
|
||||
onChange={(e) => onChange({ wallpaperFrequency: e.target.value as string })}
|
||||
options={[
|
||||
{ value: '1h', label: '1 hour' },
|
||||
{ value: '3h', label: '3 hours' },
|
||||
{ value: '6h', label: '6 hours' },
|
||||
{ value: '12h', label: '12 hours' },
|
||||
{ value: '1d', label: '1 day' },
|
||||
{ value: '2d', label: '2 days' },
|
||||
]}
|
||||
/>
|
||||
<div className="flex items-center gap-4">
|
||||
<input
|
||||
type="range"
|
||||
min={MIN_WALLPAPER_FREQUENCY_HOURS}
|
||||
max={MAX_WALLPAPER_FREQUENCY_HOURS}
|
||||
step="1"
|
||||
value={wallpaperFrequencyHours}
|
||||
onChange={(e) => onChange({ wallpaperFrequency: `${Number(e.target.value)}h` })}
|
||||
className="liquid-range"
|
||||
style={getRangeStyle(
|
||||
wallpaperFrequencyHours,
|
||||
MIN_WALLPAPER_FREQUENCY_HOURS,
|
||||
MAX_WALLPAPER_FREQUENCY_HOURS,
|
||||
)}
|
||||
/>
|
||||
<span className="w-20 text-right text-sm text-slate-200">
|
||||
{formatWallpaperFrequency(wallpaperFrequencyHours)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
|
||||
@@ -43,13 +43,37 @@ const safeParse = (value: string | null): unknown => {
|
||||
const toStorageString = (value: unknown): string =>
|
||||
typeof value === 'string' ? value : JSON.stringify(value);
|
||||
|
||||
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
||||
typeof v === 'object' && v !== null && !Array.isArray(v);
|
||||
|
||||
const deepMerge = <T>(base: T, stored: unknown): T => {
|
||||
if (isPlainObject(base)) {
|
||||
const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
||||
const storedObj = isPlainObject(stored) ? stored : {};
|
||||
for (const key of Object.keys(result)) {
|
||||
if (Object.prototype.hasOwnProperty.call(storedObj, key)) {
|
||||
result[key] = deepMerge(result[key], storedObj[key]);
|
||||
}
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
if (Array.isArray(base)) {
|
||||
return (Array.isArray(stored) ? stored : base) as T;
|
||||
}
|
||||
if (stored === null) return base;
|
||||
return typeof stored === typeof base ? (stored as T) : base;
|
||||
};
|
||||
|
||||
export const normalizeConfig = (stored: unknown): Config =>
|
||||
deepMerge(DEFAULT_CONFIG, stored);
|
||||
|
||||
export const ConfigurationService = {
|
||||
loadConfig(): Config {
|
||||
try {
|
||||
const stored = localStorage.getItem('config');
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return { ...DEFAULT_CONFIG, ...parsed };
|
||||
return normalizeConfig(parsed);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing config from localStorage', error);
|
||||
@@ -159,12 +183,12 @@ export const ConfigurationService = {
|
||||
throw new Error(`No required keys found. Expected: ${REQUIRED_LOCAL_STORAGE_KEYS.join(', ')}`);
|
||||
}
|
||||
|
||||
const importedConfig = (localStorageData as Record<string, unknown>).config as Config;
|
||||
const importedConfig = (localStorageData as Record<string, unknown>).config;
|
||||
const importedUserWallpapers = (localStorageData as Record<string, unknown>)
|
||||
.userWallpapers as Wallpaper[];
|
||||
.userWallpapers;
|
||||
|
||||
return {
|
||||
config: importedConfig || { ...DEFAULT_CONFIG },
|
||||
config: normalizeConfig(importedConfig),
|
||||
userWallpapers: Array.isArray(importedUserWallpapers) ? importedUserWallpapers : [],
|
||||
};
|
||||
},
|
||||
|
||||
BIN
extension/icons/vision-128.png
Normal file
BIN
extension/icons/vision-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
extension/icons/vision-16.png
Normal file
BIN
extension/icons/vision-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
extension/icons/vision-48.png
Normal file
BIN
extension/icons/vision-48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Vision Startpage",
|
||||
"version": "1.0",
|
||||
"description": "A beautiful and customizable startpage for your browser.",
|
||||
"version": "1.0.0",
|
||||
"description": "A light, modern and customizable startpage.",
|
||||
"chrome_url_overrides": {
|
||||
"newtab": "index.html"
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"icons": {
|
||||
"16": "extension/icons/vision-16.png",
|
||||
"48": "extension/icons/vision-48.png",
|
||||
"128": "extension/icons/vision-128.png"
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self'; object-src 'self';"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
project_name: Vision Start
|
||||
date: 2026-07-03
|
||||
date: 2026-07-08
|
||||
type: general_overview
|
||||
---
|
||||
|
||||
@@ -54,7 +54,7 @@ The startpage is composed of widgets and a configuration panel:
|
||||
- **Clock** — Optional header clock with selectable size, font, and 12h/24h format.
|
||||
- **Title** — Optional big header title (text + size configurable).
|
||||
- **Server Status Widget** — Bottom-center glass pill that periodically "pings" configured server addresses and shows online/offline indicators. Ping uses an image-load trick (`components/utils/jsping.js`) with a 5s timeout, at a configurable frequency.
|
||||
- **Wallpaper background** — Fullscreen background image with adjustable blur, brightness, and opacity, rendered behind a soft readability layer for the liquid-glass UI. Supports rotating through multiple wallpapers at a cadence (`1h`–`2d`).
|
||||
- **Wallpaper background** — Fullscreen background image with adjustable blur, brightness, and opacity, rendered behind a soft readability layer for the liquid-glass UI. Supports rotating through multiple wallpapers at an hourly cadence selected by slider (`1h`–`48h`).
|
||||
- Built-in wallpapers: Abstract, Abstract Red, Beach, Dark, Mountain, Waves (`components/utils/baseWallpapers.ts`).
|
||||
- User wallpapers: upload from a file (≤4MB, ≤4.5MB base64) or add by URL; stored in `chrome.storage.local` when available, falling back to storing the URL directly on CORS failure.
|
||||
- **Icon library & auto-fetch** — Website icons can be picked from the [Dashboard Icons](https://dashboardicons.com/) library (metadata pre-downloaded to `public/icon-metadata.json`) or auto-fetched from the target site's `apple-touch-icon`/`icon` link tags, with a fallback to Google's S2 favicon service.
|
||||
@@ -88,7 +88,6 @@ vision-start/
|
||||
├── types.ts # Core domain types (Config, Category, Website, Server, Wallpaper)
|
||||
├── constants.tsx # DEFAULT_CATEGORIES seed data
|
||||
├── manifest.json # Chrome MV3 manifest (newtab override, storage permission)
|
||||
├── icon.png # Extension icon source
|
||||
│
|
||||
├── components/
|
||||
│ ├── Clock.tsx # Header clock widget
|
||||
@@ -109,7 +108,7 @@ vision-start/
|
||||
│ │
|
||||
│ ├── configuration/
|
||||
│ │ ├── GeneralTab.tsx # Title, sizes, alignment, tile size
|
||||
│ │ ├── ThemeTab.tsx # Background selection, wallpaper mgmt, blur/brightness/opacity
|
||||
│ │ ├── ThemeTab.tsx # Background selection, wallpaper cadence, wallpaper mgmt, blur/brightness/opacity
|
||||
│ │ ├── ClockTab.tsx # Clock enable/size/font/format
|
||||
│ │ └── ServerWidgetTab.tsx # Server widget enable/ping/servers (drag-to-reorder)
|
||||
│ │
|
||||
@@ -158,9 +157,9 @@ The shape of all persisted data lives in `types.ts`:
|
||||
- **`Server`** — `id`, `name`, `address`
|
||||
- **`Category`** — `id`, `name`, `websites: Website[]`
|
||||
- **`Wallpaper`** — `name`, optional `url` or `base64`
|
||||
- **`Config`** — Everything else: title, wallpaper list + frequency/blur/brightness/opacity, titleSize, vertical & horizontal alignment, tileSize, `clock {enabled, size, font, format}`, `serverWidget {enabled, pingFrequency, servers}`
|
||||
- **`Config`** — Everything else: title, wallpaper list + hourly frequency/blur/brightness/opacity, titleSize, vertical & horizontal alignment, tileSize, `clock {enabled, size, font, format}`, `serverWidget {enabled, pingFrequency, servers}`
|
||||
|
||||
`ConfigurationService` (`components/services/ConfigurationService.ts`) is the source of truth for default config and persistence helpers.
|
||||
`ConfigurationService` (`components/services/ConfigurationService.ts`) is the source of truth for default config and persistence helpers. Config loaded from `localStorage` (and imported from export files) is run through `normalizeConfig()`, a schema-driven deep merge against `DEFAULT_CONFIG`: nested blocks (`clock`, `serverWidget`) inherit new sub-fields added to defaults, stored values with the wrong type fall back to defaults, and keys absent from the default schema are pruned. The merge is self-healing — `App.tsx`'s persist-`useEffect` writes the normalized shape back to `localStorage` on first run.
|
||||
|
||||
Storage layout (browser-side):
|
||||
|
||||
@@ -224,7 +223,7 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
||||
- **`EditModal.tsx` has been removed.** It was a legacy drag-and-drop editor that imported non-existent `lucide-react` and `./IconPicker`; it was never wired into `App.tsx`. Use `WebsiteEditModal.tsx` / `CategoryEditModal.tsx` instead.
|
||||
- **`@hello-pangea/dnd`** is used only in `ServerWidgetTab.tsx` (server reorder), which itself is imported by the lazy-loaded `ConfigurationModal`, so it lives in a separate chunk and is absent from the initial page load. `WebsiteTile` moves tiles within their own category via simple left/right buttons (no cross-category movement), not drag-and-drop.
|
||||
- **Chrome storage is optional.** `StorageLocalManager` checks availability once (`checkChromeStorageLocalAvailable`) and caches it. When unavailable (e.g., running as a plain web page), wallpaper upload/delete flows are gated off and `addWallpaperToChromeStorageLocal` throws.
|
||||
- **Wallpaper rotation** is time-based, evaluated on render/mount rather than via a timer. It reads `wallpaperState` from `localStorage`, advances the index if the frequency window has elapsed, and writes it back. The renderer clamps `currentIndex` to the valid range of the current selection and walks the list forward to find a wallpaper whose data actually resolves (so deleting the currently-displayed wallpaper, or shrinking the selection, never leaves the background blank); if no wallpaper resolves, the background layer is hidden. When the selection becomes empty, `wallpaperState` is reset and the background is hidden. A manual "Next Wallpaper" button in the Theme tab advances `currentIndex` (with wraparound) and bumps a `wallpaperVersion` nonce in `App.tsx` that retriggers the renderer.
|
||||
- **Wallpaper rotation** is time-based, evaluated on render/mount rather than via a timer. It reads `wallpaperState` from `localStorage`, advances the index if the frequency window has elapsed, and writes it back; the frequency is clamped to 1–48 hours, while older saved values like `1d`/`2d` still resolve to their hour equivalents. The renderer clamps `currentIndex` to the valid range of the current selection and walks the list forward to find a wallpaper whose data actually resolves (so deleting the currently-displayed wallpaper, or shrinking the selection, never leaves the background blank); if no wallpaper resolves, the background layer is hidden. When the selection becomes empty, `wallpaperState` is reset and the background is hidden. A manual "Next Wallpaper" button in the Theme tab advances `currentIndex` (with wraparound) and bumps a `wallpaperVersion` nonce in `App.tsx` that retriggers the renderer.
|
||||
- **Icon picker** in `WebsiteEditModal` loads `/icon-metadata.json` at runtime and expands each icon's `colors` into duplicate-name entries so color variants are searchable.
|
||||
- **`tsconfig.json` does not emit JS** (`noEmit: true`, bundler resolution); Vite handles all transpilation.
|
||||
- **`tailwind.config.js` safelists** a set of `w-[Npx]/h-[Npx]` classes because `WebsiteTile` generates tailwind classes dynamically from `tileSize` (`w-[42px]`, etc.).
|
||||
@@ -253,4 +252,4 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
||||
|
||||
---
|
||||
|
||||
_Last updated: 2026-07-03. Generated as a general project overview; not a coding-style guide._
|
||||
_Last updated: 2026-07-08. Generated as a general project overview; not a coding-style guide._
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 763 KiB After Width: | Height: | Size: 265 KiB |
Reference in New Issue
Block a user