big performance changes
All checks were successful
Build and Release to Staging / Build Vision Start (push) Successful in 8s
Build and Release to Staging / Build Vision Start Image (push) Successful in 1m3s
Build and Release to Staging / Deploy Vision Start (staging) (push) Successful in 2s

This commit is contained in:
2026-07-03 14:28:26 -03:00
parent 83dcf65069
commit 1580187689
14 changed files with 298 additions and 444 deletions

View File

@@ -16,9 +16,26 @@ const Clock: React.FC<ClockProps> = ({ config, getClockSizeClass }) => {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timerId = setInterval(() => setTime(new Date()), 1000);
return () => clearInterval(timerId);
}, []);
if (!config.clock.enabled) return;
const scheduleNext = () => {
const now = new Date();
const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds();
timeoutId = window.setTimeout(() => {
setTime(new Date());
intervalId = window.setInterval(() => setTime(new Date()), 60_000);
}, msToNextMinute);
};
let timeoutId = 0;
let intervalId = 0;
scheduleNext();
return () => {
if (timeoutId) window.clearTimeout(timeoutId);
if (intervalId) window.clearInterval(intervalId);
};
}, [config.clock.enabled]);
if (!config.clock.enabled) {
return null;

View File

@@ -1,255 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import { DragDropContext, Droppable, Draggable, DropResult } from '@hello-pangea/dnd';
import { getWebsiteIcon } from './utils/iconService';
import { Category, Website } from '../types';
import IconPicker from './IconPicker';
import { icons } from 'lucide-react';
interface EditModalProps {
categories: Category[];
onClose: () => void;
onSave: (categories: Category[]) => void;
}
const EditModal: React.FC<EditModalProps> = ({ categories, onClose, onSave }) => {
const [localCategories, setLocalCategories] = useState(categories);
const [selectedCategoryId, setSelectedCategoryId] = useState(categories[0]?.id || null);
const [newCategoryName, setNewCategoryName] = useState('');
const [newWebsite, setNewWebsite] = useState({ name: '', url: '', icon: '' });
const [showIconPicker, setShowIconPicker] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [onClose]);
const handleOnDragEnd = (result: DropResult) => {
if (!result.destination) return;
const { source, destination } = result;
if (source.droppableId === destination.droppableId) {
const category = localCategories.find(cat => cat.id === source.droppableId);
if (category) {
const items = Array.from(category.websites);
const [reorderedItem] = items.splice(source.index, 1);
items.splice(destination.index, 0, reorderedItem);
const updatedCategories = localCategories.map(cat =>
cat.id === category.id ? { ...cat, websites: items } : cat
);
setLocalCategories(updatedCategories);
}
} else {
const sourceCategory = localCategories.find(cat => cat.id === source.droppableId);
const destCategory = localCategories.find(cat => cat.id === destination.droppableId);
if (sourceCategory && destCategory) {
const sourceItems = Array.from(sourceCategory.websites);
const [movedItem] = sourceItems.splice(source.index, 1);
const destItems = Array.from(destCategory.websites);
destItems.splice(destination.index, 0, { ...movedItem, categoryId: destCategory.id });
const updatedCategories = localCategories.map(cat => {
if (cat.id === sourceCategory.id) return { ...cat, websites: sourceItems };
if (cat.id === destCategory.id) return { ...cat, websites: destItems };
return cat;
});
setLocalCategories(updatedCategories);
}
}
};
const handleAddCategory = () => {
if (newCategoryName.trim() === '') return;
const newCategory: Category = {
id: Date.now().toString(),
name: newCategoryName,
websites: [],
};
setLocalCategories([...localCategories, newCategory]);
setNewCategoryName('');
};
const handleRemoveCategory = (id: string) => {
const updatedCategories = localCategories.filter(cat => cat.id !== id);
setLocalCategories(updatedCategories);
if (selectedCategoryId === id) {
setSelectedCategoryId(updatedCategories[0]?.id || null);
}
};
const handleAddWebsite = async () => {
if (!selectedCategoryId || !newWebsite.name || !newWebsite.url) return;
let icon = newWebsite.icon;
if (!icon || !Object.keys(icons).includes(icon)) {
icon = await getWebsiteIcon(newWebsite.url);
}
const newWebsiteData: Website = {
id: Date.now().toString(),
name: newWebsite.name,
url: newWebsite.url,
icon,
categoryId: selectedCategoryId,
};
const updatedCategories = localCategories.map(cat => {
if (cat.id === selectedCategoryId) {
return { ...cat, websites: [...cat.websites, newWebsiteData] };
}
return cat;
});
setLocalCategories(updatedCategories);
setNewWebsite({ name: '', url: '', icon: '' });
};
const handleRemoveWebsite = (categoryId: string, websiteId: string) => {
const updatedCategories = localCategories.map(cat => {
if (cat.id === categoryId) {
return { ...cat, websites: cat.websites.filter(web => web.id !== websiteId) };
}
return cat;
});
setLocalCategories(updatedCategories);
};
const selectedCategory = localCategories.find(cat => cat.id === selectedCategoryId);
return (
<div className="fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center z-50">
<div ref={modalRef} className="bg-black/25 backdrop-blur-md border border-white/10 rounded-2xl p-8 w-full max-w-4xl text-white">
<h2 className="text-3xl font-bold mb-6">Edit Bookmarks</h2>
<div className="grid grid-cols-3 gap-8">
<div className="col-span-1">
<h3 className="text-xl font-semibold mb-4">Categories</h3>
<div className="flex flex-col gap-2 mb-4">
{localCategories.map(category => (
<div
key={category.id}
className={`flex justify-between items-center p-3 rounded-lg cursor-pointer ${selectedCategoryId === category.id ? 'bg-cyan-500/50' : 'bg-white/10'}`}
onClick={() => setSelectedCategoryId(category.id)}
>
<span>{category.name}</span>
<button onClick={() => handleRemoveCategory(category.id)} className="text-red-500 hover:text-red-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</div>
))}
</div>
<div className="flex gap-2">
<input
type="text"
placeholder="New Category"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
className="bg-white/10 p-2 rounded-lg w-full focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<button onClick={handleAddCategory} className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-2 px-4 rounded-lg">
Add
</button>
</div>
</div>
<div className="col-span-2">
<h3 className="text-xl font-semibold mb-4">Websites</h3>
{selectedCategory && (
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId={selectedCategory.id}>
{(provided) => (
<ul {...provided.droppableProps} ref={provided.innerRef} className="mb-8">
{selectedCategory.websites.map((website, index) => (
<Draggable key={website.id} draggableId={website.id} index={index}>
{(provided) => (
<li
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
className="flex items-center justify-between bg-white/10 p-3 rounded-lg mb-3"
>
<div className="flex items-center">
{Object.keys(icons).includes(website.icon) ? (
React.createElement(icons[website.icon as keyof typeof icons], { className: "h-8 w-8 mr-4" })
) : (
<img src={website.icon} alt={website.name} className="h-8 w-8 mr-4" />
)}
<span>{website.name}</span>
</div>
<button onClick={() => handleRemoveWebsite(selectedCategory.id, website.id)} className="text-red-500 hover:text-red-400">
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
</li>
)}
</Draggable>
))}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
)}
<div>
<h3 className="text-xl font-semibold mb-4">Add New Bookmark</h3>
<div className="flex flex-col gap-4">
<input
type="text"
placeholder="Name"
value={newWebsite.name}
onChange={(e) => setNewWebsite({ ...newWebsite, name: e.target.value })}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<input
type="text"
placeholder="URL"
value={newWebsite.url}
onChange={(e) => setNewWebsite({ ...newWebsite, url: e.target.value })}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400"
/>
<div className="flex items-center gap-2">
<input
type="text"
placeholder="Icon URL or name"
value={newWebsite.icon}
onChange={(e) => setNewWebsite({ ...newWebsite, icon: e.target.value })}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full"
/>
<button onClick={() => setShowIconPicker(!showIconPicker)} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-3 px-4 rounded-lg">
{showIconPicker ? 'Close' : 'Select Icon'}
</button>
</div>
{showIconPicker && (
<IconPicker
onSelect={(iconName) => {
setNewWebsite({ ...newWebsite, icon: iconName });
setShowIconPicker(false);
}}
/>
)}
<button onClick={handleAddWebsite} className="bg-cyan-500 hover:bg-cyan-400 text-white font-bold py-3 px-4 rounded-lg">
Add Bookmark
</button>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-4 mt-8">
<button onClick={() => onSave(localCategories)} className="bg-green-500 hover:bg-green-400 text-white font-bold py-2 px-6 rounded-lg">
Save
</button>
<button onClick={onClose} className="bg-gray-600 hover:bg-gray-500 text-white font-bold py-2 px-6 rounded-lg">
Close
</button>
</div>
</div>
</div>
);
};
export default EditModal;

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Server } from '../types';
import ping from './utils/jsping.js';
@@ -12,45 +12,64 @@ interface ServerWidgetProps {
};
}
const getStatusColor = (status: string) => {
switch (status) {
case 'online':
return 'bg-green-500';
case 'offline':
return 'bg-red-500';
default:
return 'bg-gray-500';
}
};
const ServerWidget: React.FC<ServerWidgetProps> = ({ config }) => {
const [serverStatus, setServerStatus] = useState<Record<string, string>>({});
const serversRef = useRef(config.serverWidget.servers);
serversRef.current = config.serverWidget.servers;
const serversSignature = config.serverWidget.servers
.map(s => `${s.id}:${s.address}`)
.join('|');
const serverCount = config.serverWidget.servers.length;
useEffect(() => {
if (!config.serverWidget.enabled) return;
const pingServers = () => {
config.serverWidget.servers.forEach((server) => {
setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'pending' }));
const pending: Record<string, string> = {};
for (const s of serversRef.current) pending[s.id] = 'pending';
setServerStatus(prev => ({ ...prev, ...pending }));
serversRef.current.forEach((server) => {
ping(server.address)
.then(() => {
setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'online' }));
setServerStatus(prev =>
prev[server.id] === 'online' ? prev : { ...prev, [server.id]: 'online' }
);
})
.catch(() => {
setServerStatus((prevStatus) => ({ ...prevStatus, [server.id]: 'offline' }));
setServerStatus(prev =>
prev[server.id] === 'offline' ? prev : { ...prev, [server.id]: 'offline' }
);
});
});
};
if (config.serverWidget.enabled) {
pingServers();
const interval = setInterval(pingServers, config.serverWidget.pingFrequency * 1000);
return () => clearInterval(interval);
}
}, [config.serverWidget.enabled, config.serverWidget.servers, config.serverWidget.pingFrequency]);
pingServers();
const interval = setInterval(pingServers, config.serverWidget.pingFrequency * 1000);
return () => clearInterval(interval);
}, [
config.serverWidget.enabled,
config.serverWidget.pingFrequency,
serversSignature,
serverCount,
]);
if (!config.serverWidget.enabled) {
return null;
}
const getStatusColor = (status: string) => {
switch (status) {
case 'online':
return 'bg-green-500';
case 'offline':
return 'bg-red-500';
default:
return 'bg-gray-500';
}
};
return (
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 w-auto max-w-full">
<div className="flex items-center gap-4 bg-black/25 backdrop-blur-md border border-white/20 px-4 py-2 shadow-lg"

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { baseWallpapers } from './utils/baseWallpapers';
import { Wallpaper as WallpaperType } from '../types';
import { getWallpaperFromChromeStorageLocal } from './utils/StorageLocalManager';
@@ -13,50 +13,58 @@ interface WallpaperProps {
wallpaperVersion: number;
}
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;
};
const wallpaperUrlCache = new Map<string, string | undefined>();
const getWallpaperUrlByName = async (name: string): Promise<string | undefined> => {
if (!name) return undefined;
if (wallpaperUrlCache.has(name)) return wallpaperUrlCache.get(name);
let resolved: string | undefined;
const foundInBase = baseWallpapers.find((w: WallpaperType) => w.name === name);
if (foundInBase) {
return foundInBase.url || foundInBase.base64;
}
try {
const storedUserWallpapers: WallpaperType[] =
JSON.parse(localStorage.getItem('userWallpapers') || '[]');
const foundInUser = storedUserWallpapers.find((w: WallpaperType) => w.name === name);
if (foundInUser) {
try {
const wallpaperData = await getWallpaperFromChromeStorageLocal(name);
if (wallpaperData && wallpaperData.startsWith('http')) {
return wallpaperData;
resolved = foundInBase.url || foundInBase.base64;
} else {
try {
const storedUserWallpapers: WallpaperType[] =
JSON.parse(localStorage.getItem('userWallpapers') || '[]');
const foundInUser = storedUserWallpapers.find((w: WallpaperType) => w.name === name);
if (foundInUser) {
try {
const wallpaperData = await getWallpaperFromChromeStorageLocal(name);
if (wallpaperData && wallpaperData.startsWith('http')) {
resolved = wallpaperData;
} else {
resolved = wallpaperData || undefined;
}
} catch (error) {
console.error('Error getting wallpaper from chrome storage', error);
resolved = undefined;
}
return wallpaperData || undefined;
} catch (error) {
console.error('Error getting wallpaper from chrome storage', error);
return undefined;
}
} catch (error) {
console.error('Error reading userWallpapers from localStorage', error);
resolved = undefined;
}
} catch (error) {
console.error('Error reading userWallpapers from localStorage', error);
}
return undefined;
wallpaperUrlCache.set(name, resolved);
return resolved;
};
const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness, opacity, wallpaperFrequency, wallpaperVersion }) => {
const [imageUrl, setImageUrl] = useState<string | undefined>(undefined);
// 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;
};
const resolvedRef = useRef(false);
useEffect(() => {
const updateWallpaper = async () => {
@@ -111,6 +119,7 @@ const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness,
}),
);
resolvedRef.current = true;
setImageUrl(resolvedUrl);
};
updateWallpaper();
@@ -120,14 +129,13 @@ const Wallpaper: React.FC<WallpaperProps> = ({ wallpaperNames, blur, brightness,
return (
<div
className="fixed inset-0 -z-10 w-full h-full"
className="fixed inset-0 -z-10 w-full h-full wallpaper-transition"
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"
/>

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { Website } from '../types';
import { getWebsiteIcon } from './utils/iconService';
@@ -22,47 +22,64 @@ interface IconMetadata {
name: string;
};
};
colors: any; // this can be anything I guess
colors: any;
}
let iconMetadataCache: IconMetadata[] | null = null;
const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onClose, onSave, onDelete }) => {
const [name, setName] = useState(website ? website.name : '');
const [url, setUrl] = useState(website ? website.url : '');
const [icon, setIcon] = useState(website ? website.icon : '');
const [iconQuery, setIconQuery] = useState('');
const [iconMetadata, setIconMetadata] = useState<IconMetadata[]>([]);
const [filteredIcons, setFilteredIcons] = useState<IconMetadata[]>([]);
const [iconMetadata, setIconMetadata] = useState<IconMetadata[]>([]);
const [iconsFetched, setIconsFetched] = useState(false);
const debounceRef = useRef<number | null>(null);
useEffect(() => {
fetch('/icon-metadata.json')
const ensureIconMetadata = () => {
if (iconMetadataCache || iconsFetched) return;
setIconsFetched(true);
fetch('/icon-metadata.json', { cache: 'force-cache' })
.then(response => response.json())
.then(data => {
const iconsArray = Object.entries(data).map(([name, details]) => ({
const iconsArray: IconMetadata[] = Object.entries(data).map(([name, details]) => ({
name,
...details
}));
// Expand colors into separate entries
iconsArray.forEach(icon => {
if (icon.colors) {
const colors = Object.values(icon.colors).filter(key => key !== icon.name);
for (const color of colors) {
const newIcon = { ...icon };
newIcon.name = color;
iconsArray.push(newIcon);
}
}
});
...(details as object),
})) as IconMetadata[];
iconMetadataCache = iconsArray;
setIconMetadata(iconsArray);
});
}, []);
})
.catch(err => console.error('Failed to load icon metadata', err));
};
useEffect(() => {
if (iconQuery && Array.isArray(iconMetadata)) {
const lowerCaseQuery = iconQuery.toLowerCase();
const filtered = iconMetadata
.filter(icon => icon.name.toLowerCase().includes(lowerCaseQuery))
.slice(0, 50);
setFilteredIcons(filtered);
if (iconQuery && Array.isArray(iconMetadata) && iconMetadata.length > 0) {
if (debounceRef.current) window.clearTimeout(debounceRef.current);
debounceRef.current = window.setTimeout(() => {
const lowerCaseQuery = iconQuery.toLowerCase();
const filtered: IconMetadata[] = [];
for (const ic of iconMetadata) {
if (ic.name.toLowerCase().includes(lowerCaseQuery)) {
filtered.push(ic);
if (filtered.length >= 50) break;
}
if (ic.colors) {
const colors = Object.values(ic.colors).filter(key => key !== ic.name);
for (const color of colors) {
if (typeof color === 'string' && color.toLowerCase().includes(lowerCaseQuery)) {
filtered.push({ ...ic, name: color });
if (filtered.length >= 50) break;
}
}
if (filtered.length >= 50) break;
}
}
setFilteredIcons(filtered);
}, 150);
return () => {
if (debounceRef.current) window.clearTimeout(debounceRef.current);
};
} else {
setFilteredIcons([]);
}
@@ -76,7 +93,6 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
};
const handleSave = () => {
console.log({ id: website?.id, name, url, icon });
onSave({ id: website?.id, name, url, icon });
};
@@ -128,6 +144,7 @@ const WebsiteEditModal: React.FC<WebsiteEditModalProps> = ({ website, edit, onCl
setIcon(e.target.value);
setIconQuery(e.target.value);
}}
onFocus={ensureIconMetadata}
className="bg-white/10 p-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-cyan-400 w-full"
/>
{filteredIcons.length > 0 && (

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { memo, useState } from 'react';
import { Website } from '../types';
interface WebsiteTileProps {
@@ -23,7 +23,6 @@ const getTileSizeClass = (size: string | undefined) => {
};
// Returns normal icon size in px
const getIconPixelSize = (size: string | undefined): number => {
switch (size) {
case 'small':
@@ -37,7 +36,6 @@ const getIconPixelSize = (size: string | undefined): number => {
}
};
// Returns loading icon size in px
const getIconLoadingPixelSize = (size: string | undefined): number => {
switch (size) {
case 'small':
@@ -115,4 +113,4 @@ const WebsiteTile: React.FC<WebsiteTileProps> = ({ website, isEditing, onEdit, o
);
};
export default WebsiteTile;
export default memo(WebsiteTile);

View File

@@ -1,3 +1,4 @@
import React, { memo } from 'react';
import WebsiteTile from '../WebsiteTile';
import { Category, Website } from '../../types';
@@ -10,10 +11,8 @@ interface CategoryGroupProps {
setEditingWebsite: (website: Website) => void;
handleMoveWebsite: (website: Website, direction: 'left' | 'right') => void;
getHorizontalAlignmentClass: (alignment: string) => string;
config: {
horizontalAlignment: string;
tileSize?: string;
};
horizontalAlignment: string;
tileSize?: string;
}
const CategoryGroup: React.FC<CategoryGroupProps> = ({
@@ -25,12 +24,13 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
setEditingWebsite,
handleMoveWebsite,
getHorizontalAlignmentClass,
config,
horizontalAlignment,
tileSize,
}) => {
return (
<div key={category.id} className="w-full">
<div className={`flex ${getHorizontalAlignmentClass(config.horizontalAlignment)} items-center mb-4 w-full ${config.horizontalAlignment !== 'middle' ? 'px-8' : ''}`}>
<h2 className={`text-2xl font-bold text-white ${config.horizontalAlignment === 'left' ? 'text-left' : config.horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${config.horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
<div className={`flex ${getHorizontalAlignmentClass(horizontalAlignment)} items-center mb-4 w-full ${horizontalAlignment !== 'middle' ? 'px-8' : ''}`}>
<h2 className={`text-2xl font-bold text-white ${horizontalAlignment === 'left' ? 'text-left' : horizontalAlignment === 'right' ? 'text-right' : 'text-center'} ${horizontalAlignment !== 'middle' ? 'w-full' : ''}`}>{category.name}</h2>
{isEditing && (
<button
onClick={() => {
@@ -45,7 +45,7 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
</button>
)}
</div>
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(config.horizontalAlignment)} gap-6`}>
<div className={`flex flex-wrap ${getHorizontalAlignmentClass(horizontalAlignment)} gap-6`}>
{category.websites.map((website) => (
<WebsiteTile
key={website.id}
@@ -53,7 +53,7 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
isEditing={isEditing}
onEdit={setEditingWebsite}
onMove={handleMoveWebsite}
tileSize={config.tileSize}
tileSize={tileSize}
/>
))}
{isEditing && (
@@ -72,4 +72,4 @@ const CategoryGroup: React.FC<CategoryGroupProps> = ({
);
};
export default CategoryGroup;
export default memo(CategoryGroup);

View File

@@ -50,6 +50,8 @@ const getSubtitleSizeClass = (size: string) => {
}
};
export { getClockSizeClass, getTitleSizeClass, getSubtitleSizeClass };
const Header: React.FC<HeaderProps> = ({ config }) => {
return (
<>

View File

@@ -1,24 +1,38 @@
function request_image(url) {
return new Promise(function(resolve, reject) {
var img = new Image();
img.onload = function() { resolve(img); };
img.onerror = function() { reject(url); };
var settled = false;
function settleOk() {
if (settled) return;
settled = true;
clearTimeout(failTimer);
img.onload = img.onerror = null;
resolve(img);
}
function settleFail() {
if (settled) return;
settled = true;
clearTimeout(failTimer);
img.onload = img.onerror = null;
reject(url);
}
img.onload = settleOk;
img.onerror = settleFail;
img.src = url + '?random-no-cache=' + Math.floor((1 + Math.random()) * 0x10000).toString(16);
var failTimer = setTimeout(settleFail, 5000);
});
}
function ping(url, multiplier) {
return new Promise(function(resolve, reject) {
var start = (new Date()).getTime();
var response = function() {
var response = function() {
var delta = ((new Date()).getTime() - start);
delta *= (multiplier || 1);
resolve(delta);
resolve(delta);
};
request_image(url).then(response).catch(response);
setTimeout(function() { reject(Error('Timeout')); }, 5000);
});
}
export default ping;
export default ping;