Fix all the general issues in sizing, placement, dragging, architecture and so on
This commit is contained in:
+2
-1
@@ -11,5 +11,6 @@
|
||||
],
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
}
|
||||
},
|
||||
"incognito": "spanning"
|
||||
}
|
||||
+22
-27
@@ -16,17 +16,23 @@ body {
|
||||
/* Base styles for widget containers */
|
||||
.container {
|
||||
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;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
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 */
|
||||
@@ -45,15 +51,6 @@ body {
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.close-button:hover {
|
||||
background: #ff4444;
|
||||
}
|
||||
|
||||
.close-button:active {
|
||||
background: #cc0000;
|
||||
}
|
||||
|
||||
/* Styles for the settings button */
|
||||
@@ -72,7 +69,6 @@ body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
@@ -89,6 +85,14 @@ body {
|
||||
font-size: 48px;
|
||||
font-family: 'IBM Plex Mono', monospace;
|
||||
text-align: center;
|
||||
font-size: 50cqmin;
|
||||
}
|
||||
|
||||
.timezone-display{
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
font-size: 15cqmin;
|
||||
}
|
||||
|
||||
/* SearchWidget form */
|
||||
@@ -145,7 +149,6 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.settings-toggle:hover {
|
||||
@@ -154,9 +157,7 @@ body {
|
||||
|
||||
/* Main settings modal */
|
||||
.settings-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -259,9 +260,3 @@ body {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.timezone-display{
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
}
|
||||
+12
-14
@@ -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">
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
+23
-10
@@ -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,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
|
||||
@@ -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]
|
||||
},
|
||||
|
||||
};
|
||||
@@ -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')
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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')
|
||||
@@ -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 > = {
|
||||
|
||||
Reference in New Issue
Block a user