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": {
|
"icons": {
|
||||||
"128": "icon.png"
|
"128": "icon.png"
|
||||||
}
|
},
|
||||||
|
"incognito": "spanning"
|
||||||
}
|
}
|
||||||
+22
-27
@@ -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;
|
|
||||||
}
|
|
||||||
+12
-14
@@ -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">
|
||||||
@@ -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(
|
||||||
|
<T extends WidgetData>(id: string, updates: Partial<T>) => {
|
||||||
|
console.log(id, updates)
|
||||||
|
|
||||||
const updatedWidgets = widgets.map(w =>
|
const updatedWidgets = widgets.map(w =>
|
||||||
w.id === id ? { ...w, ...updates } : w
|
w.id === id ? { ...w, ...updates } : w
|
||||||
);
|
);
|
||||||
setWidgets(updatedWidgets);
|
setWidgets(updatedWidgets)
|
||||||
settingsService.saveSetting('widgets', updatedWidgets)
|
},
|
||||||
};
|
[widgets, setWidgets, settingsService]
|
||||||
|
);
|
||||||
|
|
||||||
const addWidget = (data: WidgetData) => {
|
const addWidget = useCallback(
|
||||||
|
(data: WidgetData) => {
|
||||||
const updatedWidgets = [...widgets, data]
|
const updatedWidgets = [...widgets, data]
|
||||||
setWidgets([...widgets, data])
|
setWidgets(updatedWidgets)
|
||||||
settingsService.saveSetting('widgets', 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -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>) => {
|
|
||||||
|
const [availableEngines] = useAtom(searchEngineState)
|
||||||
|
|
||||||
|
const handleSearch = useCallback(
|
||||||
|
(e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value;
|
const query = (e.currentTarget.elements.namedItem('query') as HTMLInputElement).value;
|
||||||
|
|
||||||
if (query) {
|
if (query) {
|
||||||
setTimeout(() => {
|
widget.engines.forEach(engine => {
|
||||||
window.open(`${searchEngines[widget.engine]}${encodeURIComponent(query)}`, '_blank')
|
if (!(engine in availableEngines)) {
|
||||||
}, 200)
|
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>
|
||||||
|
</>
|
||||||
|
}
|
||||||
@@ -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 (<>
|
||||||
|
|
||||||
<div
|
|
||||||
ref={widgetRef}
|
|
||||||
className="container"
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
style={{ cursor: isDraggable && !isSettingsOpen ? 'grab' : 'default' }}
|
|
||||||
>
|
|
||||||
<ModalOverlay
|
<ModalOverlay
|
||||||
setVisible={setSettingsOpen}
|
setVisible={setSettingsOpen}
|
||||||
visible={isSettingsOpen}
|
visible={isSettingsOpen}
|
||||||
>
|
>
|
||||||
<SettingsComponent
|
<SettingsComponent
|
||||||
heightInput={heightInput}
|
widgetData={widgetData}
|
||||||
setHeightInput={setHeightInput}
|
updateWidget={updateWidget}
|
||||||
setWidthInput={setWidthInput}
|
|
||||||
widthInput={widthInput}
|
|
||||||
>
|
>
|
||||||
{settingsContent}
|
{settingsContent}
|
||||||
</SettingsComponent>
|
</SettingsComponent>
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
{isDraggable && <>
|
<div
|
||||||
<CloseButton onClick={() => undefined /*removeWidget(widget.id)*/} />
|
ref={widgetRef}
|
||||||
|
className={`container ${widgetData.background ? 'aero' : ''}`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
style={{ cursor: isEditable && !isSettingsOpen ? 'grab' : 'default' }}
|
||||||
|
>
|
||||||
|
{isEditable && <>
|
||||||
|
<CloseButton onClick={() => removeWidget(widgetData.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>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
+23
-10
@@ -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,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';
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -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)
|
|
||||||
@@ -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")
|
||||||
@@ -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 { globalSettingsAtom } from "./globalSettings.atom";
|
||||||
import { DEFAULT_SETTINGS } from "../defaults";
|
|
||||||
|
|
||||||
export const showGridState = atom<boolean>(DEFAULT_SETTINGS.showGrid)
|
export const showGridState = globalSettingsAtom('showGrid')
|
||||||
@@ -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)
|
|
||||||
@@ -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 > = {
|
||||||
|
|||||||
Reference in New Issue
Block a user