Fix all the general issues in sizing, placement, dragging, architecture and so on

This commit is contained in:
Your Name
2025-04-13 17:11:08 -04:00
parent 3e4105aca3
commit f569bb2240
19 changed files with 309 additions and 231 deletions
@@ -1,29 +1,27 @@
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { CogwheelIcon } from './CogwheelIcon';
import { useAtom, useSetAtom } from 'jotai';
import { useAtom } from 'jotai';
import { editState } from '../state/edit.state';
import { showGridState } from '../state/showGrid.state';
import { intitialWidgetData } from '../defaults';
import { WidgetData, WidgetTypes } from '../types';
import { modalContent, modalVisible } from '../state/modalOverlay.state';
import { ModalOverlay } from './ModalOverlay';
import { backgroundImageState } from '../state/backgroundImage.state';
interface SettingsPanelProps {
backgroundImage: string,
setBackgroundImage: (url: string) => void,
addWidget: (widget: WidgetData) => void,
}
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, setBackgroundImage, addWidget }) => {
const [editEnabled, setEditEnabled] = useAtom(editState);
const [showGrid, setShowGrid] = useAtom(showGridState);
export const GlobalSettingsPanel: React.FC<SettingsPanelProps> = ({
addWidget
}) => {
const [isModalShowing, setModalShowing] = useState(false);
const createWidget = (type: WidgetTypes) => {
const newWidgetData = intitialWidgetData(type);
addWidget(newWidgetData)
};
const [editEnabled, setEditEnabled] = useAtom(editState);
const [showGrid, setShowGrid] = useAtom(showGridState);
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
const createWidget = (type: WidgetTypes) => addWidget(intitialWidgetData(type));
return (
<>
@@ -49,7 +47,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, s
<input
type="checkbox"
checked={editEnabled}
onChange={() => setEditEnabled(b => !b)}
onChange={() => setEditEnabled(!editEnabled)}
/>
</label>
<label>
@@ -57,7 +55,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, s
<input
type="checkbox"
checked={showGrid}
onChange={() => setShowGrid(b => !b)}
onChange={() => setShowGrid(!showGrid)}
/>
</label>
<div className="widget-buttons">
+60 -35
View File
@@ -1,25 +1,30 @@
import React, { useState, useEffect } from 'react';
import { useAtomValue } from 'jotai'
import React, { useState, useEffect, useCallback, Suspense, startTransition } from 'react';
import { useAtom, useAtomValue } from 'jotai'
import { WidgetFactory } from './WidgetFactory';
import { SettingsPanel } from './SettingsPanel';
import { loadableWidgetsState } from '../state/widget.state';
import { GlobalSettingsPanel } from './GlobalSettingsPanel';
import { showGridState } from '../state/showGrid.state';
import { WidgetData } from '../types';
import { loadableBackgroundImageState } from '../state/backgroundImage.state';
import { settingsService } from '../services/settingsService';
import { ModalOverlay } from './ModalOverlay';
import { backgroundImageState } from '../state/backgroundImage.state';
import { widgetState } from '../state/widget.state';
const GRID_COLS = 36;
const GRID_ROWS = 18;
const coloredGrindlines = (i:number, max: number) => {
let color;
if(i === max/2){
color = '#FF3333'
}
return { background: color }
}
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)
const [showGrid] = useAtom(showGridState)
const [widgets, setWidgets] = useAtom(widgetState)
const [backgroundImage] = useAtom(backgroundImageState)
useEffect(() => {
const handleResize = () => {
@@ -30,41 +35,60 @@ export const NewTab: React.FC = () => {
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
})
}, [window.innerWidth, window.innerHeight])
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)
};
const updateWidget = useCallback(
<T extends WidgetData>(id: string, updates: Partial<T>) => {
console.log(id, updates)
const addWidget = (data: WidgetData) => {
const updatedWidgets = [...widgets, data]
setWidgets([...widgets, data])
settingsService.saveSetting('widgets', updatedWidgets)
}
const updatedWidgets = widgets.map(w =>
w.id === id ? { ...w, ...updates } : w
);
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const addWidget = useCallback(
(data: WidgetData) => {
const updatedWidgets = [...widgets, data]
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
const removeWidget = useCallback(
(widgetId: string) => {
const updatedWidgets = widgets.filter(widget => widget.id !== widgetId)
setWidgets(updatedWidgets)
},
[widgets, setWidgets, settingsService]
);
return (
<>
<ModalOverlay />
<SettingsPanel
backgroundImage={backgroundImage}
setBackgroundImage={setBackgroundImage}
<Suspense fallback={<></>}>
<GlobalSettingsPanel
addWidget={addWidget}
/>
{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}%` }} />
<div key={`v${i}`}
className="grid-line vertical"
style={{
left: `${(i / GRID_COLS) * 100}%`,
...coloredGrindlines(i, GRID_COLS)
}} />
))}
{Array.from({ length: GRID_ROWS + 1 }).map((_, i) => (
<div key={`h${i}`} className="grid-line horizontal" style={{ top: `${(i / GRID_ROWS) * 100}%` }} />
<div key={`h${i}`}
className="grid-line horizontal"
style={{
top: `${(i / GRID_ROWS) * 100}%`,
...coloredGrindlines(i, GRID_ROWS),
}} />
))}
</div>
)}
@@ -74,8 +98,9 @@ export const NewTab: React.FC = () => {
widget={widget}
gridSize={gridSize}
updateWidget={updateWidget}
removeWidget={removeWidget}
/>
))}
</>
</ Suspense>
);
};
+56 -32
View File
@@ -1,54 +1,78 @@
import React from 'react';
import { SearchWidgetData, WidgetExport } from '../types';
import React, { useCallback, useEffect, useState } from 'react';
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../types';
import { searchEngineState } from '../state/searchEngines.state';
import { useAtom, useAtomValue } from 'jotai';
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 makeSearchString = (searchStringTemplate: string, query: string) => searchStringTemplate.replace('{s}', encodeURIComponent(query))
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)
}
};
const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidget }) => {
const [availableEngines] = useAtom(searchEngineState)
const handleSearch = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value;
if (query) {
widget.engines.forEach(engine => {
if (!(engine in availableEngines)) {
return
}
setTimeout(() => {
window.open(makeSearchString(availableEngines[engine], query), '_blank')
}, 200)
})
}
},
[availableEngines, widget]
);
return (
<form onSubmit={handleSearch}>
<input type="text" name="query" placeholder="Search..." />
<input type="text" name="query" placeholder={`Search ${widget.engines}`} autoComplete="off" />
</form>
);
};
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
const handleEngineChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
updateWidget(widget.id, { engine: e.target.value as SearchWidgetData['engine'] });
};
const [availableEngines] = useAtom(searchEngineState)
return (
<label>
Search Engine:
<select value={widget.engine} onChange={handleEngineChange}>
{Object.keys(searchEngines).map((engine: any) =>
<option value={engine}>{engine}</option>
)}
</select>
</label>
const handleEngineChange = useCallback(
(e: any) => {
let engines = [...widget.engines]
if (e.target.checked) {
engines.push(e.target.value)
} else {
engines = engines.filter(engine => engine !== e.target.value)
}
updateWidget(widget.id, { engines })
},
[updateWidget, widget]
);
return (<>
{'Search Engine(s):'}
{Object.keys(availableEngines).map((availableEngine: string) =>
<label>
<input
type='checkbox'
value={availableEngine}
checked={widget.engines.includes(availableEngine)}
onChange={e => handleEngineChange(e)}
/>
{availableEngine}
</label>
)}
</>);
};
export const SearchWidget:WidgetExport = {
export const SearchWidget: WidgetExport = {
widget: SearchWidgetComponent,
settings: SearchWidgetSettings
}
@@ -0,0 +1,44 @@
import { ReactElement } from "react"
import { WidgetData } from "../types"
type GenericSettingsProps = {
widgetData: WidgetData
updateWidget: (id: string, data: Partial<WidgetData>) => void
children: ReactElement | ReactElement[]
}
export function SettingsComponent({
widgetData,
updateWidget,
children
}: GenericSettingsProps) {
return <>
{children}
<label>
Width (grid cells):
<input
type="number"
min="1"
value={widgetData.gridWidth}
onChange={e => updateWidget(widgetData.id, { gridWidth: parseInt(e.target.value) })}
/>
</label>
<label>
Height (grid cells):
<input
type="number"
min="1"
value={widgetData.gridHeight}
onChange={e => updateWidget(widgetData.id, { gridHeight: parseInt(e.target.value) })}
/>
</label>
<label>
Background:
<input
type="checkbox"
checked={widgetData.background}
onChange={_ => updateWidget(widgetData.id, { background: !widgetData.background })}
/>
</label>
</>
}
+41 -78
View File
@@ -1,33 +1,35 @@
import React, { useRef, useState, useEffect, ReactElement } from 'react';
import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react';
import { WidgetData } from '../types';
import { CogwheelIcon } from './CogwheelIcon';
import { useAtomValue } from 'jotai';
import { editState } from '../state/edit.state';
import { CloseButton } from './Closebutton';
import { ModalOverlay } from './ModalOverlay';
import { SettingsComponent } from './SettingsComponent';
import { debounce, debounceTime } from 'rxjs';
interface WidgetProps {
widget: WidgetData;
widgetData: WidgetData;
gridSize: { width: number; height: number };
children: React.ReactNode;
settingsContent: ReactElement;
updateWidget: (id: string, data: Partial<WidgetData>) => void
updateWidget: (id: string, data: Partial<WidgetData>) => void;
removeWidget: (id: string) => void;
}
export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, settingsContent, updateWidget }) => {
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget }) => {
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, setSettingsOpen] = useState(false);
const [widthInput, setWidthInput] = useState(widget.gridWidth);
const [heightInput, setHeightInput] = useState(widget.gridHeight);
const isDraggable = useAtomValue(editState)
const isEditable = useAtomValue(editState)
const snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
const handleMouseDown = (e: React.MouseEvent) => {
if (!isDraggable || isSettingsOpen) {
if (!isEditable || isSettingsOpen) {
return
}
@@ -48,7 +50,6 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
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`;
}
@@ -58,9 +59,14 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
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 gridX = snapToGrid(rect.left, gridSize.width);
const gridY = snapToGrid(rect.top, gridSize.height);
if(gridX !== widgetData.gridX || gridY !== widgetData.gridY){
// overwrite the values so prevent a visual "bounce"
widgetData.gridX = gridX;
widgetData.gridY = gridY;
updateWidget(widgetData.id, { gridX, gridY });
}
}
};
@@ -70,13 +76,6 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
}
};
useEffect(() => {
const newWidth = Math.max(1, widthInput);
const newHeight = Math.max(1, heightInput);
updateWidget(widget.id, { gridWidth: newWidth, gridHeight: newHeight });
}, [heightInput, widthInput])
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
@@ -101,36 +100,33 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
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`;
widgetRef.current.style.left = `${widgetData.gridX * gridSize.width}px`;
widgetRef.current.style.top = `${widgetData.gridY * gridSize.height}px`;
widgetRef.current.style.width = `${(widgetData.gridWidth) * gridSize.width}px`;
widgetRef.current.style.height = `${(widgetData.gridHeight) * gridSize.height}px`;
}
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]);
return (
}, [widgetData, gridSize, isDragging]);
return (<>
<ModalOverlay
setVisible={setSettingsOpen}
visible={isSettingsOpen}
>
<SettingsComponent
widgetData={widgetData}
updateWidget={updateWidget}
>
{settingsContent}
</SettingsComponent>
</ModalOverlay>
<div
ref={widgetRef}
className="container"
className={`container ${widgetData.background ? 'aero' : ''}`}
onMouseDown={handleMouseDown}
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
style={{ cursor: isEditable && !isSettingsOpen ? 'grab' : 'default' }}
>
<ModalOverlay
setVisible={setSettingsOpen}
visible={isSettingsOpen}
>
<SettingsComponent
heightInput={heightInput}
setHeightInput={setHeightInput}
setWidthInput={setWidthInput}
widthInput={widthInput}
>
{settingsContent}
</SettingsComponent>
</ModalOverlay>
{isDraggable && <>
<CloseButton onClick={() => undefined /*removeWidget(widget.id)*/} />
{isEditable && <>
<CloseButton onClick={() => removeWidget(widgetData.id)} />
<button
className="settings-button"
onClick={() => setSettingsOpen(true)}
@@ -140,38 +136,5 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
</>}
{children}
</div>
);
};
type GenericSettingsProps = {
widthInput: number;
setWidthInput: (n: number) => void;
heightInput: number;
setHeightInput: (n: number) => void;
children: ReactElement | ReactElement[]
}
function SettingsComponent({ widthInput, setWidthInput, heightInput, setHeightInput, children }: GenericSettingsProps) {
return <>
{children}
<label>
Width (grid cells):
<input
type="number"
min="1"
value={widthInput}
onChange={(e) => { setWidthInput(parseInt(e.target.value)); }}
/>
</label>
<label>
Height (grid cells):
<input
type="number"
min="1"
value={heightInput}
onChange={(e) => { setHeightInput(parseInt(e.target.value)); }}
/>
</label>
</>
}
</>);
};
+5 -3
View File
@@ -7,7 +7,8 @@ import { ClockWidget } from './ClockWidget';
interface WidgetFactoryProps {
widget: WidgetData;
gridSize: { width: number; height: number };
updateWidget: (id: string, data: Partial<WidgetData>) => void
updateWidget: (id: string, data: Partial<WidgetData>) => void;
removeWidget: (id:string) => void;
}
const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
@@ -15,17 +16,18 @@ const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
'search': SearchWidget
}
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, gridSize, updateWidget }) => {
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, gridSize, updateWidget, removeWidget }) => {
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}
widgetData={widget}
gridSize={gridSize}
settingsContent={settings}
updateWidget={updateWidget}
removeWidget={removeWidget}
>
{component}
</Widget>
+24 -11
View File
@@ -1,35 +1,48 @@
import { GlobalSettings, SearchWidgetData, ClockWidgetData, WidgetTypes, WidgetData } from './types';
export const DEFAULT_SETTINGS: GlobalSettings = {
backgroundUrl: '',
widgets: [],
editEnabled: true,
showGrid: true
};
export const intitialWidgetData = (type: WidgetTypes): WidgetData => {
switch (type) {
case 'clock':
return {
gridHeight: 2,
gridHeight: 3,
gridWidth: 6,
gridX: 15,
gridY: 6,
timezone: 'UTC',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showSeconds: false,
use24Hour: true,
type: 'clock',
background: true,
id: `${type}-${Date.now()}`
} as ClockWidgetData
case 'search':
return {
gridHeight: 2,
gridHeight: 1,
gridWidth: 10,
gridX: 13,
gridY: 6,
id: `${type}-${Date.now()}`,
type: 'search',
engine: 'Google'
engines: ['Google'],
background: true,
} as SearchWidgetData
}
}
}
export const DEFAULT_SETTINGS: GlobalSettings = {
backgroundUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/2880px-Cat_August_2010-4.jpg',
widgets: [
intitialWidgetData('search'),
intitialWidgetData('clock')
],
editEnabled: true,
showGrid: true,
searchEngines: {
Google: 'https://www.google.com/search?q={s}',
Bing: 'https://www.bing.com/search?q={s}',
DuckDuckGo: 'https://duckduckgo.com/?q={s}',
Yandex: 'https://yandex.com/search/?text={s}'
}
};
-1
View File
@@ -1,4 +1,3 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';
+1 -1
View File
@@ -10,7 +10,7 @@ export const settingsService = {
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]
return loaded[key] !== undefined ? loaded[key] as GlobalSettings[K] : DEFAULT_SETTINGS[key]
},
};
+2 -8
View File
@@ -1,9 +1,3 @@
import { atomWithObservable, loadable } from "jotai/utils";
import { settingsService } from "../services/settingsService";
import { from } from 'rxjs';
import { globalSettingsAtom } from "./globalSettings.atom";
const backgroundImageState = atomWithObservable(() =>
from(settingsService.getSetting('backgroundUrl'))
);
export const loadableBackgroundImageState = loadable(backgroundImageState)
export const backgroundImageState = globalSettingsAtom('backgroundUrl')
+2 -3
View File
@@ -1,4 +1,3 @@
import { atom } from "jotai";
import { DEFAULT_SETTINGS } from "../defaults";
import { globalSettingsAtom } from "./globalSettings.atom";
export const editState = atom<boolean>(DEFAULT_SETTINGS.editEnabled)
export const editState = globalSettingsAtom("editEnabled")
+28
View File
@@ -0,0 +1,28 @@
import { settingsService } from "../services/settingsService";
import { atom } from "jotai";
import { GlobalSettings } from "../types";
import { DEFAULT_SETTINGS } from "../defaults";
export const globalSettingsAtom = <T extends keyof GlobalSettings>(key: T) => {
const storage = atom<GlobalSettings[T] | undefined>(undefined)
return atom(
async (get) => {
const existing = get(storage)
if (existing === undefined) {
const remoteValue = await settingsService.getSetting(key)
console.log("load", key, remoteValue)
return remoteValue
}
return existing
},
async (get, set, update: GlobalSettings[T]) => {
console.log("save", key, update)
await settingsService.saveSetting(key, update)
set(storage, update)
}
);
}
-5
View File
@@ -1,5 +0,0 @@
import { atom } from "jotai";
import { ReactNode } from "react";
export const modalVisible = atom<boolean>(false)
export const modalContent = atom<ReactNode>(undefined)
@@ -0,0 +1,3 @@
import { globalSettingsAtom } from "./globalSettings.atom";
export const searchEngineState = globalSettingsAtom('searchEngines')
+2 -3
View File
@@ -1,4 +1,3 @@
import { atom } from "jotai";
import { DEFAULT_SETTINGS } from "../defaults";
import { globalSettingsAtom } from "./globalSettings.atom";
export const showGridState = atom<boolean>(DEFAULT_SETTINGS.showGrid)
export const showGridState = globalSettingsAtom('showGrid')
+2 -8
View File
@@ -1,9 +1,3 @@
import { atomWithObservable, loadable } from "jotai/utils";
import { settingsService } from "../services/settingsService";
import { from } from 'rxjs';
import { globalSettingsAtom } from "./globalSettings.atom";
const widgetsState = atomWithObservable(() =>
from(settingsService.getSetting('widgets'))
);
export const loadableWidgetsState = loadable(widgetsState)
export const widgetState = globalSettingsAtom('widgets')
+3 -1
View File
@@ -5,12 +5,13 @@ export interface WidgetData {
gridY: number;
gridWidth: number;
gridHeight: number;
background: boolean;
[key: string]: any;
}
export interface SearchWidgetData extends WidgetData {
type: 'search';
engine: 'Google' | 'Bing' | 'DuckDuckGo' | 'Yandex';
engines: Array<string>;
}
export interface ClockWidgetData extends WidgetData {
@@ -27,6 +28,7 @@ export interface GlobalSettings {
widgets: WidgetData[];
editEnabled: boolean;
showGrid: boolean;
searchEngines: { [key in string]: string}
}
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {