First refactor without AI
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { NewTab } from './components/NewTab';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
return <NewTab />;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ClockWidgetData, WidgetExport } from '../types';
|
||||
|
||||
interface ClockWidgetProps {
|
||||
widget: ClockWidgetData;
|
||||
updateWidget: (id: string, update: Partial<ClockWidgetData>) => void
|
||||
}
|
||||
|
||||
export const ClockWidgetComponent: React.FC<ClockWidgetProps> = ({ widget, updateWidget }) => {
|
||||
const [time, setTime] = useState(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setTime(new Date()), 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const formatTime = () => {
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
timeZone: widget.timezone,
|
||||
hour12: !widget.use24Hour
|
||||
};
|
||||
if (widget.showSeconds) {
|
||||
options.second = '2-digit';
|
||||
}
|
||||
return new Intl.DateTimeFormat('en-US', options).format(time);
|
||||
};
|
||||
|
||||
return <div className="clock-time">{formatTime()}</div>;
|
||||
};
|
||||
|
||||
export const ClockWidgetSettings = ({ widget, updateWidget }: ClockWidgetProps) => {
|
||||
const handleToggleSeconds = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateWidget(widget.id, { showSeconds: e.target.checked });
|
||||
};
|
||||
|
||||
const handleToggle24Hour = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateWidget(widget.id, { use24Hour: e.target.checked });
|
||||
};
|
||||
|
||||
const handleTimezoneChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
updateWidget(widget.id, { timezone: e.target.value });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="seconds-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={widget.showSeconds}
|
||||
onChange={handleToggleSeconds}
|
||||
/>
|
||||
Show seconds
|
||||
</div>
|
||||
<div className="hours-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={widget.use24Hour}
|
||||
onChange={handleToggle24Hour}
|
||||
/>
|
||||
Use 24-hour format
|
||||
</div>
|
||||
<label>
|
||||
Timezone:
|
||||
<select value={widget.timezone} onChange={handleTimezoneChange}>
|
||||
{Intl.supportedValuesOf('timeZone').map(tz => (
|
||||
<option key={tz} value={tz}>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClockWidget:WidgetExport = {
|
||||
widget: ClockWidgetComponent,
|
||||
settings: ClockWidgetSettings
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
type CogwheelIconProps = {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export function CogwheelIcon({size}: CogwheelIconProps) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-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 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l-.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v-.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l-.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { WidgetFactory } from './WidgetFactory';
|
||||
import { SettingsPanel } from './SettingsPanel';
|
||||
import { loadableWidgetsState } from '../state/widget.state';
|
||||
import { showGridState } from '../state/showGrid.state';
|
||||
import { WidgetData } from '../types';
|
||||
import { loadableBackgroundImageState } from '../state/backgroundImage.state';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
|
||||
const GRID_COLS = 36;
|
||||
const GRID_ROWS = 18;
|
||||
|
||||
export const NewTab: React.FC = () => {
|
||||
const [gridSize, setGridSize] = useState({ width: window.innerWidth / GRID_COLS, height: window.innerHeight / GRID_ROWS });
|
||||
const [widgets, setWidgets] = useState<WidgetData[]>([])
|
||||
const [backgroundImage, setBackgroundImage] = useState<string>('')
|
||||
|
||||
const showGrid = useAtomValue(showGridState)
|
||||
const loadingWidgets = useAtomValue(loadableWidgetsState)
|
||||
const loadingBackgroundImage = useAtomValue(loadableBackgroundImageState)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setGridSize({
|
||||
width: window.innerWidth / GRID_COLS,
|
||||
height: window.innerHeight / GRID_ROWS
|
||||
});
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
})
|
||||
|
||||
useEffect(() => loadingBackgroundImage.state === 'hasData' ? setBackgroundImage(loadingBackgroundImage.data) : undefined, [loadingBackgroundImage]);
|
||||
useEffect(() => loadingWidgets.state === 'hasData' ? setWidgets(loadingWidgets.data) : undefined, [loadingWidgets]);
|
||||
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
|
||||
|
||||
const updateWidget = <T extends WidgetData>(id: string, updates: Partial<T>) => {
|
||||
const updatedWidgets = widgets.map(w =>
|
||||
w.id === id ? { ...w, ...updates } : w
|
||||
);
|
||||
setWidgets(updatedWidgets);
|
||||
settingsService.saveSetting('widgets', updatedWidgets)
|
||||
console.log('Updated widget:', { id, updates })
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsPanel
|
||||
backgroundImage={backgroundImage}
|
||||
setBackgroundImage={setBackgroundImage}
|
||||
/>
|
||||
{showGrid && (
|
||||
<div className="grid-overlay">
|
||||
{Array.from({ length: GRID_COLS + 1 }).map((_, i) => (
|
||||
<div key={`v${i}`} className="grid-line vertical" style={{ left: `${(i / GRID_COLS) * 100}%` }} />
|
||||
))}
|
||||
{Array.from({ length: GRID_ROWS + 1 }).map((_, i) => (
|
||||
<div key={`h${i}`} className="grid-line horizontal" style={{ top: `${(i / GRID_ROWS) * 100}%` }} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{widgets.map(widget => (
|
||||
<WidgetFactory
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
gridSize={gridSize}
|
||||
updateWidget={updateWidget}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { SearchWidgetData, WidgetExport } from '../types';
|
||||
|
||||
interface SearchWidgetProps {
|
||||
widget: SearchWidgetData;
|
||||
updateWidget: (id: string, update: Partial<SearchWidgetData>) => void
|
||||
}
|
||||
|
||||
const searchEngines: { [key in SearchWidgetData['engine']]: string } = {
|
||||
Google: 'https://www.google.com/search?q=',
|
||||
Bing: 'https://www.bing.com/search?q=',
|
||||
DuckDuckGo: 'https://duckduckgo.com/?q=',
|
||||
Yandex: 'https://yandex.com/search/?text='
|
||||
};
|
||||
|
||||
const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget }) => {
|
||||
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value;
|
||||
if (query) {
|
||||
setTimeout(() => {
|
||||
window.open(`${searchEngines[widget.engine]}${encodeURIComponent(query)}`, '_blank')
|
||||
}, 200)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearch}>
|
||||
<input type="text" name="query" placeholder="Search..." />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
|
||||
const handleEngineChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
updateWidget(widget.id, { engine: e.target.value as SearchWidgetData['engine'] });
|
||||
};
|
||||
|
||||
return (
|
||||
<label>
|
||||
Search Engine:
|
||||
<select value={widget.engine} onChange={handleEngineChange}>
|
||||
{Object.keys(searchEngines).map((engine: any) =>
|
||||
<option value={engine}>{engine}</option>
|
||||
)}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchWidget:WidgetExport = {
|
||||
widget: SearchWidgetComponent,
|
||||
settings: SearchWidgetSettings
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState } from 'react';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
import { useAtom } from 'jotai';
|
||||
import { draggableState } from '../state/draggable.state';
|
||||
import { showGridState } from '../state/showGrid.state';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
|
||||
interface SettingsPanelProps {
|
||||
backgroundImage: string,
|
||||
setBackgroundImage: (url: string) => void
|
||||
}
|
||||
|
||||
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, setBackgroundImage }) => {
|
||||
//const [backgroundUrl, setBackgroundUrl] = useAtom(backgroundImageState);
|
||||
const [dragEnabled, setDragEnabled] = useAtom(draggableState);
|
||||
const [showGrid, setShowGrid] = useAtom(showGridState);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
//settingsService.saveSetting('backgroundUrl', backgroundUrl)
|
||||
settingsService.saveSetting('dragEnabled', dragEnabled)
|
||||
settingsService.saveSetting('showGrid', showGrid)
|
||||
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const addWidget = (type: string) => {
|
||||
//const newWidget = createBaseWidget(type, Math.floor(gridCols / 2), Math.floor(gridRows / 2), dragEnabled);
|
||||
//saveSettings({ widgets: [...settings.widgets, newWidget] });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="settings-toggle" onClick={() => setIsOpen(!isOpen)}>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="settings-modal">
|
||||
<div className="modal-content">
|
||||
<h2>Settings</h2>
|
||||
<label>
|
||||
Background URL:
|
||||
<input
|
||||
type="text"
|
||||
value={backgroundImage}
|
||||
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Drag Enabled:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dragEnabled}
|
||||
onChange={(e) => setDragEnabled(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Show Grid:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showGrid}
|
||||
onChange={(e) => setShowGrid(e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
<div className="widget-buttons">
|
||||
<button onClick={() => addWidget('clock')}>Add Clock</button>
|
||||
<button onClick={() => addWidget('search')}>Add Search</button>
|
||||
</div>
|
||||
<button className="close-modal" onClick={handleSave}>Save & Close</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { WidgetData } from '../types';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { draggableState } from '../state/draggable.state';
|
||||
|
||||
interface WidgetProps {
|
||||
widget: WidgetData;
|
||||
gridSize: { width: number; height: number };
|
||||
children: React.ReactNode;
|
||||
settingsContent?: React.ReactNode;
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void
|
||||
}
|
||||
|
||||
export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, settingsContent, updateWidget }) => {
|
||||
const widgetRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const [widthInput, setWidthInput] = useState(widget.gridWidth);
|
||||
const [heightInput, setHeightInput] = useState(widget.gridHeight);
|
||||
|
||||
const isDraggable = useAtomValue(draggableState)
|
||||
const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if(!isDraggable || isSettingsOpen){
|
||||
return
|
||||
}
|
||||
|
||||
if (widgetRef.current && e.target === widgetRef.current) {
|
||||
setIsDragging(true);
|
||||
const rect = widgetRef.current.getBoundingClientRect();
|
||||
setDragOffset({
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (isDragging && widgetRef.current) {
|
||||
e.preventDefault();
|
||||
const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width);
|
||||
const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height);
|
||||
|
||||
widgetRef.current.style.left = `${newGridX * gridSize.width}px`;
|
||||
widgetRef.current.style.top = `${newGridY * gridSize.height}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (isDragging && widgetRef.current) {
|
||||
setIsDragging(false);
|
||||
const rect = widgetRef.current.getBoundingClientRect();
|
||||
const newGridX = snapToGrid(rect.left, gridSize.width);
|
||||
const newGridY = snapToGrid(rect.top, gridSize.height);
|
||||
updateWidget(widget.id, { gridX: newGridX, gridY: newGridY });
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
||||
setIsSettingsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const handleSizeChange = () => {
|
||||
const newWidth = Math.max(1, widthInput);
|
||||
const newHeight = Math.max(1, heightInput);
|
||||
updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight });
|
||||
setWidthInput(newWidth);
|
||||
setHeightInput(newHeight);
|
||||
setIsSettingsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.userSelect = 'none';
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
}, [isDragging, dragOffset, gridSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSettingsOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [isSettingsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (widgetRef.current && !isDragging) {
|
||||
widgetRef.current.style.left = `${widget.gridX * gridSize.width}px`;
|
||||
widgetRef.current.style.top = `${widget.gridY * gridSize.height}px`;
|
||||
widgetRef.current.style.width = `${(widget.gridWidth) * gridSize.width}px`;
|
||||
widgetRef.current.style.height = `${(widget.gridHeight) * gridSize.height}px`;
|
||||
}
|
||||
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]);
|
||||
|
||||
const genericSettingsContent = (
|
||||
<>
|
||||
<label>
|
||||
Width (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={widthInput}
|
||||
onChange={(e) => setWidthInput(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Height (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={heightInput}
|
||||
onChange={(e) => setHeightInput(parseInt(e.target.value) || 1)}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={handleSizeChange}>Apply</button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={widgetRef}
|
||||
className="container"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
|
||||
>
|
||||
<button className="close-button" onClick={() => undefined /*removeWidget(widget.id)*/} onMouseDown={stopPropagation}>×</button>
|
||||
<button
|
||||
className="settings-button"
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
onMouseDown={stopPropagation}
|
||||
>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
{children}
|
||||
{isSettingsOpen && (
|
||||
<div className="widget-settings-modal" onMouseDown={stopPropagation}>
|
||||
<div ref={modalRef} className="widget-modal-content">
|
||||
<button className="close-button" onClick={() => setIsSettingsOpen(false)} onMouseDown={stopPropagation}>×</button>
|
||||
{settingsContent}
|
||||
{genericSettingsContent}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { WidgetData, SearchWidgetData, ClockWidgetData, WidgetExport, WidgetTypes } from '../types';
|
||||
import { Widget } from './Widget';
|
||||
import { SearchWidget } from './SearchWidget';
|
||||
import { ClockWidget } from './ClockWidget';
|
||||
|
||||
interface WidgetFactoryProps {
|
||||
widget: WidgetData;
|
||||
gridSize: { width: number; height: number };
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void
|
||||
}
|
||||
|
||||
const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
|
||||
'clock': ClockWidget,
|
||||
'search': SearchWidget
|
||||
}
|
||||
|
||||
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, gridSize, updateWidget }) => {
|
||||
const Wdgt = WidgetMap[widget.type]
|
||||
const component = <Wdgt.widget widget={widget} updateWidget={updateWidget}></Wdgt.widget>
|
||||
const settings = Wdgt.settings({ widget: widget, updateWidget })
|
||||
|
||||
return (
|
||||
<Widget
|
||||
widget={widget}
|
||||
gridSize={gridSize}
|
||||
settingsContent={settings}
|
||||
updateWidget={updateWidget}
|
||||
>
|
||||
{component}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { GlobalSettings, SearchWidgetData, ClockWidgetData } from './types';
|
||||
|
||||
export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
backgroundUrl: '',
|
||||
widgets: [
|
||||
{
|
||||
id: 'clock-1',
|
||||
type: 'clock',
|
||||
gridX: 15,
|
||||
gridY: 7,
|
||||
gridWidth: 6, // Clock: 6x3
|
||||
gridHeight: 3,
|
||||
dragEnabled: true,
|
||||
showSeconds: true,
|
||||
use24Hour: false,
|
||||
timezone: 'UTC' // Default timezone
|
||||
} as ClockWidgetData,
|
||||
{
|
||||
id: 'search-1',
|
||||
type: 'search',
|
||||
gridX: 13,
|
||||
gridY: 10,
|
||||
gridWidth: 10, // Search: 10x2
|
||||
gridHeight: 2,
|
||||
dragEnabled: true,
|
||||
engine: 'Google'
|
||||
} as SearchWidgetData
|
||||
],
|
||||
dragEnabled: true,
|
||||
showGrid: false
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
const root = createRoot(document.getElementById('root')!);
|
||||
root.render(<App />);
|
||||
@@ -0,0 +1,16 @@
|
||||
import { GlobalSettings, WidgetData } from '../types';
|
||||
import { DEFAULT_SETTINGS } from '../defaults';
|
||||
|
||||
export const settingsService = {
|
||||
saveSetting: async <K extends keyof GlobalSettings>(key: K, value: GlobalSettings[K]) => {
|
||||
return await chrome.storage.sync.set({
|
||||
[key]: value
|
||||
})
|
||||
},
|
||||
|
||||
getSetting: async <K extends keyof GlobalSettings>(key: K): Promise<GlobalSettings[K]> => {
|
||||
const loaded = await chrome.storage.sync.get([key])
|
||||
return loaded[key] as GlobalSettings[K]
|
||||
},
|
||||
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { atomWithObservable, loadable } from "jotai/utils";
|
||||
import { settingsService } from "../services/settingsService";
|
||||
import { from } from 'rxjs';
|
||||
|
||||
const backgroundImageState = atomWithObservable(() =>
|
||||
from(settingsService.getSetting('backgroundUrl'))
|
||||
);
|
||||
|
||||
export const loadableBackgroundImageState = loadable(backgroundImageState)
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const draggableState = atom<boolean>(false)
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const showGridState = atom<boolean>(false)
|
||||
@@ -0,0 +1,9 @@
|
||||
import { atomWithObservable, loadable } from "jotai/utils";
|
||||
import { settingsService } from "../services/settingsService";
|
||||
import { from } from 'rxjs';
|
||||
|
||||
const widgetsState = atomWithObservable(() =>
|
||||
from(settingsService.getSetting('widgets'))
|
||||
);
|
||||
|
||||
export const loadableWidgetsState = loadable(widgetsState)
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface WidgetData {
|
||||
id: string;
|
||||
type: WidgetTypes;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SearchWidgetData extends WidgetData {
|
||||
type: 'search';
|
||||
engine: 'Google' | 'Bing' | 'DuckDuckGo' | 'Yandex';
|
||||
}
|
||||
|
||||
export interface ClockWidgetData extends WidgetData {
|
||||
type: 'clock';
|
||||
showSeconds: boolean;
|
||||
use24Hour: boolean;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type WidgetTypes = (SearchWidgetData | ClockWidgetData)['type']
|
||||
|
||||
export interface GlobalSettings {
|
||||
backgroundUrl: string;
|
||||
widgets: WidgetData[];
|
||||
dragEnabled: boolean;
|
||||
showGrid: boolean;
|
||||
}
|
||||
|
||||
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
|
||||
widget: W,
|
||||
settings: S
|
||||
}
|
||||
Reference in New Issue
Block a user