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
+2 -1
View File
@@ -11,5 +11,6 @@
], ],
"icons": { "icons": {
"128": "icon.png" "128": "icon.png"
} },
"incognito": "spanning"
} }
+22 -27
View File
@@ -16,17 +16,23 @@ body {
/* Base styles for widget containers */ /* Base styles for widget containers */
.container { .container {
position: absolute; position: absolute;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 5px;
padding: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
z-index: 0; z-index: 0;
container-type: size;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0);
padding: 10px;
border-radius: 5px;
}
.aero {
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-color: rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
} }
/* Styles for the close button */ /* Styles for the close button */
@@ -45,15 +51,6 @@ body {
line-height: 16px; line-height: 16px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
transition: background 0.2s;
}
.close-button:hover {
background: #ff4444;
}
.close-button:active {
background: #cc0000;
} }
/* Styles for the settings button */ /* Styles for the settings button */
@@ -72,7 +69,6 @@ body {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: background 0.2s;
} }
.settings-button:hover { .settings-button:hover {
@@ -89,6 +85,14 @@ body {
font-size: 48px; font-size: 48px;
font-family: 'IBM Plex Mono', monospace; font-family: 'IBM Plex Mono', monospace;
text-align: center; text-align: center;
font-size: 50cqmin;
}
.timezone-display{
font-size: small;
text-align: center;
color: #ffffff;
font-size: 15cqmin;
} }
/* SearchWidget form */ /* SearchWidget form */
@@ -145,7 +149,6 @@ body {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background 0.2s;
} }
.settings-toggle:hover { .settings-toggle:hover {
@@ -154,9 +157,7 @@ body {
/* Main settings modal */ /* Main settings modal */
.settings-modal { .settings-modal {
position: fixed; position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
@@ -259,9 +260,3 @@ body {
width: 100%; width: 100%;
height: 1px; height: 1px;
} }
.timezone-display{
font-size: small;
text-align: center;
color: #ffffff;
}
@@ -1,29 +1,27 @@
import React, { useState } from 'react'; import React, { useCallback, useState } from 'react';
import { CogwheelIcon } from './CogwheelIcon'; import { CogwheelIcon } from './CogwheelIcon';
import { useAtom, useSetAtom } from 'jotai'; import { useAtom } from 'jotai';
import { editState } from '../state/edit.state'; import { editState } from '../state/edit.state';
import { showGridState } from '../state/showGrid.state'; import { showGridState } from '../state/showGrid.state';
import { intitialWidgetData } from '../defaults'; import { intitialWidgetData } from '../defaults';
import { WidgetData, WidgetTypes } from '../types'; import { WidgetData, WidgetTypes } from '../types';
import { modalContent, modalVisible } from '../state/modalOverlay.state';
import { ModalOverlay } from './ModalOverlay'; import { ModalOverlay } from './ModalOverlay';
import { backgroundImageState } from '../state/backgroundImage.state';
interface SettingsPanelProps { interface SettingsPanelProps {
backgroundImage: string,
setBackgroundImage: (url: string) => void,
addWidget: (widget: WidgetData) => void, addWidget: (widget: WidgetData) => void,
} }
export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, setBackgroundImage, addWidget }) => { export const GlobalSettingsPanel: React.FC<SettingsPanelProps> = ({
const [editEnabled, setEditEnabled] = useAtom(editState); addWidget
const [showGrid, setShowGrid] = useAtom(showGridState); }) => {
const [isModalShowing, setModalShowing] = useState(false); const [isModalShowing, setModalShowing] = useState(false);
const createWidget = (type: WidgetTypes) => { const [editEnabled, setEditEnabled] = useAtom(editState);
const newWidgetData = intitialWidgetData(type); const [showGrid, setShowGrid] = useAtom(showGridState);
addWidget(newWidgetData) const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
};
const createWidget = (type: WidgetTypes) => addWidget(intitialWidgetData(type));
return ( return (
<> <>
@@ -49,7 +47,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, s
<input <input
type="checkbox" type="checkbox"
checked={editEnabled} checked={editEnabled}
onChange={() => setEditEnabled(b => !b)} onChange={() => setEditEnabled(!editEnabled)}
/> />
</label> </label>
<label> <label>
@@ -57,7 +55,7 @@ export const SettingsPanel: React.FC<SettingsPanelProps> = ({ backgroundImage, s
<input <input
type="checkbox" type="checkbox"
checked={showGrid} checked={showGrid}
onChange={() => setShowGrid(b => !b)} onChange={() => setShowGrid(!showGrid)}
/> />
</label> </label>
<div className="widget-buttons"> <div className="widget-buttons">
+60 -35
View File
@@ -1,25 +1,30 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useCallback, Suspense, startTransition } from 'react';
import { useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { WidgetFactory } from './WidgetFactory'; import { WidgetFactory } from './WidgetFactory';
import { SettingsPanel } from './SettingsPanel'; import { GlobalSettingsPanel } from './GlobalSettingsPanel';
import { loadableWidgetsState } from '../state/widget.state';
import { showGridState } from '../state/showGrid.state'; import { showGridState } from '../state/showGrid.state';
import { WidgetData } from '../types'; import { WidgetData } from '../types';
import { loadableBackgroundImageState } from '../state/backgroundImage.state';
import { settingsService } from '../services/settingsService'; 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_COLS = 36;
const GRID_ROWS = 18; 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 = () => { export const NewTab: React.FC = () => {
const [gridSize, setGridSize] = useState({ width: window.innerWidth / GRID_COLS, height: window.innerHeight / GRID_ROWS }); const [gridSize, setGridSize] = useState({ width: window.innerWidth / GRID_COLS, height: window.innerHeight / GRID_ROWS });
const [widgets, setWidgets] = useState<WidgetData[]>([]) const [showGrid] = useAtom(showGridState)
const [backgroundImage, setBackgroundImage] = useState<string>('') const [widgets, setWidgets] = useAtom(widgetState)
const [backgroundImage] = useAtom(backgroundImageState)
const showGrid = useAtomValue(showGridState)
const loadingWidgets = useAtomValue(loadableWidgetsState)
const loadingBackgroundImage = useAtomValue(loadableBackgroundImageState)
useEffect(() => { useEffect(() => {
const handleResize = () => { const handleResize = () => {
@@ -30,41 +35,60 @@ export const NewTab: React.FC = () => {
}; };
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
return () => window.removeEventListener('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]); useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
const updateWidget = <T extends WidgetData>(id: string, updates: Partial<T>) => { const updateWidget = useCallback(
const updatedWidgets = widgets.map(w => <T extends WidgetData>(id: string, updates: Partial<T>) => {
w.id === id ? { ...w, ...updates } : w console.log(id, updates)
);
setWidgets(updatedWidgets);
settingsService.saveSetting('widgets', updatedWidgets)
};
const addWidget = (data: WidgetData) => { const updatedWidgets = widgets.map(w =>
const updatedWidgets = [...widgets, data] w.id === id ? { ...w, ...updates } : w
setWidgets([...widgets, data]) );
settingsService.saveSetting('widgets', updatedWidgets) 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 ( return (
<> <Suspense fallback={<></>}>
<ModalOverlay /> <GlobalSettingsPanel
<SettingsPanel
backgroundImage={backgroundImage}
setBackgroundImage={setBackgroundImage}
addWidget={addWidget} addWidget={addWidget}
/> />
{showGrid && ( {showGrid && (
<div className="grid-overlay"> <div className="grid-overlay">
{Array.from({ length: GRID_COLS + 1 }).map((_, i) => ( {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) => ( {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> </div>
)} )}
@@ -74,8 +98,9 @@ export const NewTab: React.FC = () => {
widget={widget} widget={widget}
gridSize={gridSize} gridSize={gridSize}
updateWidget={updateWidget} updateWidget={updateWidget}
removeWidget={removeWidget}
/> />
))} ))}
</> </ Suspense>
); );
}; };
+56 -32
View File
@@ -1,54 +1,78 @@
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { SearchWidgetData, WidgetExport } from '../types'; import { GlobalSettings, SearchWidgetData, WidgetExport } from '../types';
import { searchEngineState } from '../state/searchEngines.state';
import { useAtom, useAtomValue } from 'jotai';
interface SearchWidgetProps { interface SearchWidgetProps {
widget: SearchWidgetData; widget: SearchWidgetData;
updateWidget: (id: string, update: Partial<SearchWidgetData>) => void updateWidget: (id: string, update: Partial<SearchWidgetData>) => void
} }
const searchEngines: { [key in SearchWidgetData['engine']]: string } = { const makeSearchString = (searchStringTemplate: string, query: string) => searchStringTemplate.replace('{s}', encodeURIComponent(query))
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 SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidget }) => {
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); const [availableEngines] = useAtom(searchEngineState)
const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value;
if (query) { const handleSearch = useCallback(
setTimeout(() => { (e: React.FormEvent<HTMLFormElement>) => {
window.open(`${searchEngines[widget.engine]}${encodeURIComponent(query)}`, '_blank') e.preventDefault();
}, 200) 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 ( return (
<form onSubmit={handleSearch}> <form onSubmit={handleSearch}>
<input type="text" name="query" placeholder="Search..." /> <input type="text" name="query" placeholder={`Search ${widget.engines}`} autoComplete="off" />
</form> </form>
); );
}; };
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => { const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
const handleEngineChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const [availableEngines] = useAtom(searchEngineState)
updateWidget(widget.id, { engine: e.target.value as SearchWidgetData['engine'] });
};
return ( const handleEngineChange = useCallback(
<label> (e: any) => {
Search Engine: let engines = [...widget.engines]
<select value={widget.engine} onChange={handleEngineChange}> if (e.target.checked) {
{Object.keys(searchEngines).map((engine: any) => engines.push(e.target.value)
<option value={engine}>{engine}</option> } else {
)} engines = engines.filter(engine => engine !== e.target.value)
</select> }
</label> 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, widget: SearchWidgetComponent,
settings: SearchWidgetSettings 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 { WidgetData } from '../types';
import { CogwheelIcon } from './CogwheelIcon'; import { CogwheelIcon } from './CogwheelIcon';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { editState } from '../state/edit.state'; import { editState } from '../state/edit.state';
import { CloseButton } from './Closebutton'; import { CloseButton } from './Closebutton';
import { ModalOverlay } from './ModalOverlay'; import { ModalOverlay } from './ModalOverlay';
import { SettingsComponent } from './SettingsComponent';
import { debounce, debounceTime } from 'rxjs';
interface WidgetProps { interface WidgetProps {
widget: WidgetData; widgetData: WidgetData;
gridSize: { width: number; height: number }; gridSize: { width: number; height: number };
children: React.ReactNode; children: React.ReactNode;
settingsContent: ReactElement; 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 widgetRef = useRef<HTMLDivElement>(null);
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
const [isSettingsOpen, setSettingsOpen] = useState(false); 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 snapToGrid = (pos: number, grid: number) => Math.round(pos / grid);
const handleMouseDown = (e: React.MouseEvent) => { const handleMouseDown = (e: React.MouseEvent) => {
if (!isDraggable || isSettingsOpen) { if (!isEditable || isSettingsOpen) {
return return
} }
@@ -48,7 +50,6 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
e.preventDefault(); e.preventDefault();
const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width); const newGridX = snapToGrid(e.clientX - dragOffset.x, gridSize.width);
const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height); const newGridY = snapToGrid(e.clientY - dragOffset.y, gridSize.height);
widgetRef.current.style.left = `${newGridX * gridSize.width}px`; widgetRef.current.style.left = `${newGridX * gridSize.width}px`;
widgetRef.current.style.top = `${newGridY * gridSize.height}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) { if (isDragging && widgetRef.current) {
setIsDragging(false); setIsDragging(false);
const rect = widgetRef.current.getBoundingClientRect(); const rect = widgetRef.current.getBoundingClientRect();
const newGridX = snapToGrid(rect.left, gridSize.width); const gridX = snapToGrid(rect.left, gridSize.width);
const newGridY = snapToGrid(rect.top, gridSize.height); const gridY = snapToGrid(rect.top, gridSize.height);
updateWidget(widget.id, { gridX: newGridX, gridY: newGridY }); 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(() => { useEffect(() => {
if (isDragging) { if (isDragging) {
window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousemove', handleMouseMove);
@@ -101,36 +100,33 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
useEffect(() => { useEffect(() => {
if (widgetRef.current && !isDragging) { if (widgetRef.current && !isDragging) {
widgetRef.current.style.left = `${widget.gridX * gridSize.width}px`; widgetRef.current.style.left = `${widgetData.gridX * gridSize.width}px`;
widgetRef.current.style.top = `${widget.gridY * gridSize.height}px`; widgetRef.current.style.top = `${widgetData.gridY * gridSize.height}px`;
widgetRef.current.style.width = `${(widget.gridWidth) * gridSize.width}px`; widgetRef.current.style.width = `${(widgetData.gridWidth) * gridSize.width}px`;
widgetRef.current.style.height = `${(widget.gridHeight) * gridSize.height}px`; widgetRef.current.style.height = `${(widgetData.gridHeight) * gridSize.height}px`;
} }
}, [widget.gridX, widget.gridY, widget.gridWidth, widget.gridHeight, gridSize, isDragging]); }, [widgetData, gridSize, isDragging]);
return (
return (<>
<ModalOverlay
setVisible={setSettingsOpen}
visible={isSettingsOpen}
>
<SettingsComponent
widgetData={widgetData}
updateWidget={updateWidget}
>
{settingsContent}
</SettingsComponent>
</ModalOverlay>
<div <div
ref={widgetRef} ref={widgetRef}
className="container" className={`container ${widgetData.background ? 'aero' : ''}`}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }} style={{ cursor: isEditable && !isSettingsOpen ? 'grab' : 'default' }}
> >
<ModalOverlay {isEditable && <>
setVisible={setSettingsOpen} <CloseButton onClick={() => removeWidget(widgetData.id)} />
visible={isSettingsOpen}
>
<SettingsComponent
heightInput={heightInput}
setHeightInput={setHeightInput}
setWidthInput={setWidthInput}
widthInput={widthInput}
>
{settingsContent}
</SettingsComponent>
</ModalOverlay>
{isDraggable && <>
<CloseButton onClick={() => undefined /*removeWidget(widget.id)*/} />
<button <button
className="settings-button" className="settings-button"
onClick={() => setSettingsOpen(true)} onClick={() => setSettingsOpen(true)}
@@ -140,38 +136,5 @@ export const Widget: React.FC<WidgetProps> = ({ widget, gridSize, children, sett
</>} </>}
{children} {children}
</div> </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 { interface WidgetFactoryProps {
widget: WidgetData; widget: WidgetData;
gridSize: { width: number; height: number }; 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 } = { const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
@@ -15,17 +16,18 @@ const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
'search': SearchWidget '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 Wdgt = WidgetMap[widget.type]
const component = <Wdgt.widget widget={widget} updateWidget={updateWidget}></Wdgt.widget> const component = <Wdgt.widget widget={widget} updateWidget={updateWidget}></Wdgt.widget>
const settings = Wdgt.settings({ widget: widget, updateWidget }) const settings = Wdgt.settings({ widget: widget, updateWidget })
return ( return (
<Widget <Widget
widget={widget} widgetData={widget}
gridSize={gridSize} gridSize={gridSize}
settingsContent={settings} settingsContent={settings}
updateWidget={updateWidget} updateWidget={updateWidget}
removeWidget={removeWidget}
> >
{component} {component}
</Widget> </Widget>
+24 -11
View File
@@ -1,35 +1,48 @@
import { GlobalSettings, SearchWidgetData, ClockWidgetData, WidgetTypes, WidgetData } from './types'; 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 => { export const intitialWidgetData = (type: WidgetTypes): WidgetData => {
switch (type) { switch (type) {
case 'clock': case 'clock':
return { return {
gridHeight: 2, gridHeight: 3,
gridWidth: 6, gridWidth: 6,
gridX: 15, gridX: 15,
gridY: 6, gridY: 6,
timezone: 'UTC', timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showSeconds: false, showSeconds: false,
use24Hour: true, use24Hour: true,
type: 'clock', type: 'clock',
background: true,
id: `${type}-${Date.now()}` id: `${type}-${Date.now()}`
} as ClockWidgetData } as ClockWidgetData
case 'search': case 'search':
return { return {
gridHeight: 2, gridHeight: 1,
gridWidth: 10, gridWidth: 10,
gridX: 13, gridX: 13,
gridY: 6, gridY: 6,
id: `${type}-${Date.now()}`, id: `${type}-${Date.now()}`,
type: 'search', type: 'search',
engine: 'Google' engines: ['Google'],
background: true,
} as SearchWidgetData } 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 { createRoot } from 'react-dom/client';
import { App } from './App'; 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]> => { getSetting: async <K extends keyof GlobalSettings>(key: K): Promise<GlobalSettings[K]> => {
const loaded = await chrome.storage.sync.get([key]) 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 { globalSettingsAtom } from "./globalSettings.atom";
import { settingsService } from "../services/settingsService";
import { from } from 'rxjs';
const backgroundImageState = atomWithObservable(() => export const backgroundImageState = globalSettingsAtom('backgroundUrl')
from(settingsService.getSetting('backgroundUrl'))
);
export const loadableBackgroundImageState = loadable(backgroundImageState)
+2 -3
View File
@@ -1,4 +1,3 @@
import { atom } from "jotai"; import { globalSettingsAtom } from "./globalSettings.atom";
import { DEFAULT_SETTINGS } from "../defaults";
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 { globalSettingsAtom } from "./globalSettings.atom";
import { DEFAULT_SETTINGS } from "../defaults";
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 { globalSettingsAtom } from "./globalSettings.atom";
import { settingsService } from "../services/settingsService";
import { from } from 'rxjs';
const widgetsState = atomWithObservable(() => export const widgetState = globalSettingsAtom('widgets')
from(settingsService.getSetting('widgets'))
);
export const loadableWidgetsState = loadable(widgetsState)
+3 -1
View File
@@ -5,12 +5,13 @@ export interface WidgetData {
gridY: number; gridY: number;
gridWidth: number; gridWidth: number;
gridHeight: number; gridHeight: number;
background: boolean;
[key: string]: any; [key: string]: any;
} }
export interface SearchWidgetData extends WidgetData { export interface SearchWidgetData extends WidgetData {
type: 'search'; type: 'search';
engine: 'Google' | 'Bing' | 'DuckDuckGo' | 'Yandex'; engines: Array<string>;
} }
export interface ClockWidgetData extends WidgetData { export interface ClockWidgetData extends WidgetData {
@@ -27,6 +28,7 @@ export interface GlobalSettings {
widgets: WidgetData[]; widgets: WidgetData[];
editEnabled: boolean; editEnabled: boolean;
showGrid: boolean; showGrid: boolean;
searchEngines: { [key in string]: string}
} }
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = { export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {