add IPC
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { NewTab } from './components/NewTab';
|
||||
import { useAtom } from 'jotai';
|
||||
import { backgroundImageState } from './state/backgroundImage.state';
|
||||
import { IpcService } from './services/ipcService';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const [backgroundImage] = useAtom(backgroundImageState)
|
||||
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
|
||||
|
||||
IpcService.writeFile({ path: 'testpath', fileContent: 'testcontent' }).then(console.log)
|
||||
|
||||
return <NewTab />;
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { Suspense } from 'react';
|
||||
import { useAtom } from 'jotai'
|
||||
import { WidgetFactory } from './WidgetFactory';
|
||||
import { gridState } from '../state/grid.state';
|
||||
import { widgetState } from '../state/widget.state';
|
||||
import { GlobalSettingsPanel } from './global-settings/GlobalSettingsPanel';
|
||||
import { isIncognitoState } from '../state/incogntio.state';
|
||||
import { GridOverlay } from './grid-overlay/GridOverlay';
|
||||
|
||||
export const NewTab: React.FC = () => {
|
||||
|
||||
const [widgets] = useAtom(widgetState)
|
||||
const [grid] = useAtom(gridState)
|
||||
const [isIncognitoOn] = useAtom(isIncognitoState)
|
||||
|
||||
const widgetsToShow = widgets.filter(widget =>
|
||||
widget.showIn === 'both' ||
|
||||
(!isIncognitoOn && widget.showIn === 'standard') ||
|
||||
(isIncognitoOn && widget.showIn === 'incognito')
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense fallback={<></>}>
|
||||
<GlobalSettingsPanel />
|
||||
<GridOverlay grid={grid} />
|
||||
|
||||
{widgetsToShow.map(widget => (
|
||||
<WidgetFactory
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
grid={grid}
|
||||
/>
|
||||
))}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react';
|
||||
import { WidgetData } from '../types';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
import { CogwheelIcon } from './components/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';
|
||||
import { CloseButton } from './components/Closebutton';
|
||||
import { ModalOverlay } from './components/ModalOverlay';
|
||||
import { SettingsComponent } from './components/SettingsComponent';
|
||||
|
||||
interface WidgetProps {
|
||||
widgetData: WidgetData;
|
||||
@@ -15,9 +14,11 @@ interface WidgetProps {
|
||||
settingsContent: ReactElement;
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void;
|
||||
removeWidget: (id: string) => void;
|
||||
disableRemove?: boolean;
|
||||
disableEdit?: boolean;
|
||||
}
|
||||
|
||||
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget }) => {
|
||||
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget, disableEdit, disableRemove }) => {
|
||||
const widgetRef = useRef<HTMLDivElement>(null);
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -61,11 +62,11 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
|
||||
const rect = widgetRef.current.getBoundingClientRect();
|
||||
const gridX = snapToGrid(rect.left, gridSize.width);
|
||||
const gridY = snapToGrid(rect.top, gridSize.height);
|
||||
if(gridX !== widgetData.gridX || gridY !== widgetData.gridY){
|
||||
if (gridX !== widgetData.y || gridY !== widgetData.x) {
|
||||
// overwrite the values so prevent a visual "bounce"
|
||||
widgetData.gridX = gridX;
|
||||
widgetData.gridY = gridY;
|
||||
updateWidget(widgetData.id, { gridX, gridY });
|
||||
widgetData.y = gridX;
|
||||
widgetData.x = gridY;
|
||||
updateWidget(widgetData.id, { y: gridX, x: gridY });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -100,10 +101,10 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
|
||||
|
||||
useEffect(() => {
|
||||
if (widgetRef.current && !isDragging) {
|
||||
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`;
|
||||
widgetRef.current.style.left = `${widgetData.y * gridSize.width}px`;
|
||||
widgetRef.current.style.top = `${widgetData.x * gridSize.height}px`;
|
||||
widgetRef.current.style.width = `${(widgetData.width) * gridSize.width}px`;
|
||||
widgetRef.current.style.height = `${(widgetData.height) * gridSize.height}px`;
|
||||
}
|
||||
}, [widgetData, gridSize, isDragging]);
|
||||
|
||||
@@ -115,25 +116,28 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
|
||||
<SettingsComponent
|
||||
widgetData={widgetData}
|
||||
updateWidget={updateWidget}
|
||||
settingsOpen={[isSettingsOpen, setSettingsOpen]}
|
||||
>
|
||||
{settingsContent}
|
||||
</SettingsComponent>
|
||||
</ModalOverlay>
|
||||
<div
|
||||
ref={widgetRef}
|
||||
className={`container ${widgetData.background ? 'aero' : ''}`}
|
||||
className={`container ${widgetData.type} ${widgetData.background ? 'aero' : ''}`}
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isEditable && !isSettingsOpen ? 'grab' : 'default' }}
|
||||
>
|
||||
{isEditable && <>
|
||||
{(isEditable && !widgetData.permanent) && <>
|
||||
<CloseButton onClick={() => removeWidget(widgetData.id)} />
|
||||
</>}
|
||||
{(isEditable) &&
|
||||
<button
|
||||
className="settings-button"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
</>}
|
||||
}
|
||||
{children}
|
||||
</div>
|
||||
</>);
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { GlobalSettings, WidgetData, WidgetExport, WidgetTypeValues } from '../types';
|
||||
import { Widget } from './Widget';
|
||||
import { SearchWidget } from './search-widget/SearchWidget';
|
||||
import { ClockWidget } from './clock-widget/ClockWidget';
|
||||
import { GlobalSettingsWidget } from './global-settings/GlobalSettingsWidget';
|
||||
import { useAtom } from 'jotai';
|
||||
import { widgetState } from '../state/widget.state';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
|
||||
export interface WidgetFactoryProps {
|
||||
widget: WidgetData;
|
||||
grid: GlobalSettings['grid'];
|
||||
}
|
||||
|
||||
const WidgetMap: { [key in WidgetTypeValues]: WidgetExport } = {
|
||||
'clock': ClockWidget,
|
||||
'search': SearchWidget,
|
||||
'globalsettings': GlobalSettingsWidget
|
||||
}
|
||||
|
||||
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, grid }) => {
|
||||
const [widgets, setWidgets] = useAtom(widgetState)
|
||||
const [gridSize, setGridSize] = useState({ width: window.innerWidth / grid.cols, height: window.innerHeight / grid.rows });
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setGridSize({
|
||||
width: window.innerWidth / grid.cols,
|
||||
height: window.innerHeight / grid.rows
|
||||
});
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [window.innerWidth, window.innerHeight, grid])
|
||||
|
||||
const updateWidgets = useCallback(
|
||||
<T extends WidgetData>(id: string, updates: Partial<T>) => {
|
||||
console.log(id, updates)
|
||||
|
||||
const updatedWidgets = widgets.map(w =>
|
||||
w.id === id ? { ...w, ...updates } : w
|
||||
);
|
||||
setWidgets(updatedWidgets)
|
||||
},
|
||||
[widgets, setWidgets, settingsService]
|
||||
);
|
||||
|
||||
const removeWidget = useCallback(
|
||||
(widgetId: string) => {
|
||||
const updatedWidgets = widgets.filter(widget => widget.id !== widgetId)
|
||||
setWidgets(updatedWidgets)
|
||||
},
|
||||
[widgets, setWidgets, settingsService]
|
||||
);
|
||||
|
||||
const Wdgt = WidgetMap[widget.type]
|
||||
const component = <Wdgt.widget widget={widget} updateWidget={updateWidgets}></Wdgt.widget>
|
||||
const settings = Wdgt.settings({ widget, updateWidget: updateWidgets })
|
||||
|
||||
return (
|
||||
<Widget
|
||||
widgetData={widget}
|
||||
gridSize={gridSize}
|
||||
settingsContent={settings}
|
||||
updateWidget={updateWidgets}
|
||||
removeWidget={removeWidget}
|
||||
>
|
||||
{component}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ClockWidgetData, WidgetExport } from '../types';
|
||||
import { ClockWidgetData, WidgetExport } from '../../types';
|
||||
|
||||
interface ClockWidgetProps {
|
||||
widget: ClockWidgetData;
|
||||
@@ -28,8 +28,8 @@ export const ClockWidgetComponent: React.FC<ClockWidgetProps> = ({ widget, updat
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="clock-time">{formatTime()}</div>
|
||||
<div className="timezone-display">{widget.timezone}</div>
|
||||
<div className="time">{formatTime()}</div>
|
||||
<div className="timezone">{widget.timezone}</div>
|
||||
</>;
|
||||
};
|
||||
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
type CogwheelIconProps = {
|
||||
size: number;
|
||||
onClick?: () => void;
|
||||
className?: any
|
||||
}
|
||||
|
||||
export function CogwheelIcon({size}: CogwheelIconProps) {
|
||||
export function CogwheelIcon({size, className, onClick}: CogwheelIconProps) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className={className} onClick={onClick} width={size} height={size} viewBox={`0 0 ${size} ${size}`} fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l-.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l-.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v-.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l-.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Dispatch, ReactElement } from "react"
|
||||
import { WidgetData } from "../../types"
|
||||
import { SetStateAction, useAtom } from "jotai"
|
||||
import { incognitoAllowedState, isIncognitoState } from "../../state/incogntio.state"
|
||||
|
||||
type GenericSettingsProps = {
|
||||
widgetData: WidgetData
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void
|
||||
children: ReactElement | ReactElement[]
|
||||
settingsOpen?: [boolean, Dispatch<SetStateAction<boolean>>]
|
||||
}
|
||||
|
||||
export function SettingsComponent({
|
||||
widgetData,
|
||||
updateWidget,
|
||||
children
|
||||
}: GenericSettingsProps) {
|
||||
const [isIncognitoAllowed] = useAtom(incognitoAllowedState)
|
||||
|
||||
return <>
|
||||
{children}
|
||||
<label>
|
||||
Width (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={widgetData.width}
|
||||
onChange={e => updateWidget(widgetData.id, { width: parseInt(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Height (grid cells):
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={widgetData.height}
|
||||
onChange={e => updateWidget(widgetData.id, { height: parseInt(e.target.value) })}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Background:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={widgetData.background}
|
||||
onChange={_ => updateWidget(widgetData.id, { background: !widgetData.background })}
|
||||
/>
|
||||
</label>
|
||||
{isIncognitoAllowed &&
|
||||
<label>
|
||||
Show in standard or incognito tabs?
|
||||
<select
|
||||
value={widgetData.showIn}
|
||||
onChange={e => updateWidget(
|
||||
widgetData.id,
|
||||
{ showIn: e.target.value as WidgetData['showIn'] }
|
||||
)}>
|
||||
{(['standard', 'incognito', 'both'] as Array<WidgetData['showIn']>)
|
||||
.map(showInOption => (
|
||||
<option key={showInOption} value={showInOption}>
|
||||
{showInOption}
|
||||
</option>
|
||||
))
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
}
|
||||
|
||||
</>
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { ModalOverlay } from "../components/ModalOverlay";
|
||||
import { useState } from "react";
|
||||
import { globalSettingsOpenState } from "../../state/globalSettingsOpen.state";
|
||||
import { Box, Tab, Tabs, Typography } from "@mui/material";
|
||||
import { UtabSettings } from "./tab-contents/UtabSettings";
|
||||
import { BackgroundSettings } from "./tab-contents/BackgroundSettings";
|
||||
import { SearchEngineSettings } from "./tab-contents/SearchEngineSettings";
|
||||
|
||||
const settingsComponents = {
|
||||
'uTab': UtabSettings,
|
||||
'Background': BackgroundSettings,
|
||||
'Search': SearchEngineSettings
|
||||
}
|
||||
|
||||
export function GlobalSettingsPanel() {
|
||||
const [isOpen, setOpen] = useAtom(globalSettingsOpenState)
|
||||
const [tab, setTab] = useState(0);
|
||||
|
||||
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTab(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalOverlay visible={isOpen} setVisible={setOpen}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', height: 600 }}>
|
||||
<Tabs
|
||||
orientation="vertical"
|
||||
variant="scrollable"
|
||||
value={tab}
|
||||
onChange={handleChange}
|
||||
aria-label="Vertical tabs example"
|
||||
sx={{ borderRight: 1, borderColor: 'divider', textAlign: 'end' }}
|
||||
>
|
||||
{Object.keys(settingsComponents).map(key => <Tab label={key} />)}
|
||||
</Tabs>
|
||||
|
||||
{Object.values(settingsComponents).map(
|
||||
(Component, i) =>
|
||||
<TabPanel value={tab} index={i}>
|
||||
<Component></Component>
|
||||
</TabPanel>
|
||||
)}
|
||||
</Box>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
role="tabpanel"
|
||||
hidden={value !== index}
|
||||
id={`vertical-tabpanel-${index}`}
|
||||
aria-labelledby={`vertical-tab-${index}`}
|
||||
{...other}
|
||||
>
|
||||
{value === index && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography>{children}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { CogwheelIcon } from '../components/CogwheelIcon';
|
||||
import { useAtom } from 'jotai';
|
||||
import { WidgetData, WidgetExport } from '../../types';
|
||||
import { globalSettingsOpenState } from '../../state/globalSettingsOpen.state';
|
||||
|
||||
interface GlobalSettingsProps {
|
||||
addWidget: (widget: WidgetData) => void,
|
||||
}
|
||||
|
||||
const GlobalSettingsComponent: React.FC<GlobalSettingsProps> = ({ addWidget }) => {
|
||||
const [isGlobalSettingsOpen, setGlobalSettingsOpen] = useAtom(globalSettingsOpenState)
|
||||
|
||||
return (
|
||||
<CogwheelIcon className="settings-toggle" size={24} onClick={() => setGlobalSettingsOpen(!isGlobalSettingsOpen)} />
|
||||
);
|
||||
};
|
||||
|
||||
export const GlobalSettingsWidget: WidgetExport = {
|
||||
settings: () => <></>,
|
||||
widget: GlobalSettingsComponent
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TextField } from "@mui/material";
|
||||
import { useAtom } from "jotai";
|
||||
import { backgroundImageState } from "../../../state/backgroundImage.state";
|
||||
|
||||
export function BackgroundSettings() {
|
||||
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
|
||||
|
||||
return (<>
|
||||
<TextField
|
||||
type="text"
|
||||
value={backgroundImage}
|
||||
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</>)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Box, Button, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
|
||||
import { useAtom } from "jotai";
|
||||
import { availableEnginesState } from "../../../state/searchEngines.state";
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
|
||||
export function SearchEngineSettings() {
|
||||
const [availableSearchEngines, setAvailableSearchEngines] = useAtom(availableEnginesState)
|
||||
|
||||
const [aliasInput, setAliasInput] = useState('')
|
||||
const [searchStringInput, setSearchStringInput] = useState('')
|
||||
|
||||
const addEngine = useCallback(() => {
|
||||
const newEngines = {
|
||||
...availableSearchEngines,
|
||||
[aliasInput]: searchStringInput
|
||||
}
|
||||
|
||||
console.log(newEngines)
|
||||
|
||||
setAvailableSearchEngines(newEngines)
|
||||
setAliasInput('')
|
||||
setSearchStringInput('')
|
||||
}, [availableSearchEngines, aliasInput, searchStringInput, setAvailableSearchEngines, setAliasInput, setSearchStringInput])
|
||||
|
||||
const removeEngine = useCallback((alias:string) => {
|
||||
const engines = { ...availableSearchEngines }
|
||||
delete engines[alias]
|
||||
setAvailableSearchEngines(engines)
|
||||
setAliasInput('')
|
||||
setSearchStringInput('')
|
||||
}, [availableSearchEngines, setAvailableSearchEngines])
|
||||
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="left" style={{ fontWeight: 'bold' }}>Alias</TableCell>
|
||||
<TableCell align="left" style={{ fontWeight: 'bold' }}>Searchstring</TableCell>
|
||||
<TableCell></TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{Object.entries(availableSearchEngines).map(([name, searchString]) => (
|
||||
<TableRow
|
||||
key={name}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell align="left">{name}</TableCell>
|
||||
<TableCell align="left">{searchString}</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={_ => removeEngine(name)} variant="outlined" size="small" color="warning" startIcon={<DeleteIcon />}>
|
||||
Remove
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<TextField value={aliasInput} onChange={e => setAliasInput(e.target.value)} id="Alias-input" variant="standard" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextField value={searchStringInput} onChange={e => setSearchStringInput(e.target.value)} id="Searchstring-input" variant="standard" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button onClick={addEngine} variant="outlined" size="small" startIcon={<AddIcon />}>
|
||||
Add
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Box>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Switch } from "@mui/material";
|
||||
import { useAtom } from "jotai";
|
||||
import { editState } from "../../../state/edit.state";
|
||||
import { gridState } from "../../../state/grid.state";
|
||||
import { widgetState } from "../../../state/widget.state";
|
||||
import { WidgetTypeValues } from "../../../types";
|
||||
import { intitialWidgetData } from "../../../state/defaults";
|
||||
import { useCallback } from "react";
|
||||
import { incognitoAllowedState } from "../../../state/incogntio.state";
|
||||
|
||||
export function UtabSettings() {
|
||||
const [editEnabled, setEditEnabled] = useAtom(editState)
|
||||
const [grid, setGrid] = useAtom(gridState)
|
||||
const [widgets, setWidgets] = useAtom(widgetState)
|
||||
|
||||
const createWidget = useCallback(
|
||||
(type: WidgetTypeValues) => {
|
||||
const data = intitialWidgetData(type)
|
||||
const updatedWidgets = [...widgets, data]
|
||||
setWidgets(updatedWidgets)
|
||||
},
|
||||
[widgets, setWidgets]
|
||||
);
|
||||
|
||||
const [isIncognitoAllowed] = useAtom(incognitoAllowedState)
|
||||
|
||||
return (<>
|
||||
Incognito enabled: {isIncognitoAllowed?'yes':'no'}
|
||||
<label>
|
||||
Editmode:
|
||||
<Switch
|
||||
checked={editEnabled}
|
||||
onChange={() => setEditEnabled(!editEnabled)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Show Grid:
|
||||
<Switch
|
||||
checked={grid.show}
|
||||
onChange={() => setGrid({ ...grid, show: !grid.show})}
|
||||
/>
|
||||
</label>
|
||||
<button onClick={() => createWidget('clock')}>Add Clock</button>
|
||||
<button onClick={() => createWidget('search')}>Add Search</button>
|
||||
</>)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { GlobalSettings } from "../../types";
|
||||
|
||||
const Colors = {
|
||||
'red': '#FF3333',
|
||||
'blue': '#3333FF'
|
||||
} as const
|
||||
|
||||
const coloredGrindlines = (i:number, max: number) => {
|
||||
let color;
|
||||
|
||||
if(max%4 === 0 && i === max/4){
|
||||
color = Colors.blue
|
||||
}
|
||||
if(max%2 === 0 && i === max/2){
|
||||
color = Colors.red
|
||||
}
|
||||
if((max*3)%4 === 0 && i === max*3/4){
|
||||
color = Colors.blue
|
||||
}
|
||||
return { background: color }
|
||||
}
|
||||
|
||||
type GridOverlayProps = {
|
||||
grid: GlobalSettings['grid']
|
||||
}
|
||||
|
||||
export function GridOverlay({grid}:GridOverlayProps){
|
||||
return (<>
|
||||
{grid.show && (
|
||||
<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}%`,
|
||||
...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}%`,
|
||||
...coloredGrindlines(i, grid.rows),
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>)
|
||||
}
|
||||
+11
-8
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../types';
|
||||
import { searchEngineState } from '../state/searchEngines.state';
|
||||
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../../types';
|
||||
import { availableEnginesState } from '../../state/searchEngines.state';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { TextField } from '@mui/material';
|
||||
|
||||
interface SearchWidgetProps {
|
||||
widget: SearchWidgetData;
|
||||
@@ -12,7 +13,7 @@ const makeSearchString = (searchStringTemplate: string, query: string) => search
|
||||
|
||||
const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidget }) => {
|
||||
|
||||
const [availableEngines] = useAtom(searchEngineState)
|
||||
const [availableEngines] = useAtom(availableEnginesState)
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -21,9 +22,6 @@ const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidg
|
||||
|
||||
if (query) {
|
||||
widget.engines.forEach(engine => {
|
||||
if (!(engine in availableEngines)) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.open(makeSearchString(availableEngines[engine], query), '_blank')
|
||||
}, 200)
|
||||
@@ -34,14 +32,19 @@ const SearchWidgetComponent: React.FC<SearchWidgetProps> = ({ widget, updateWidg
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSearch}>
|
||||
<form onSubmit={handleSearch} style={{ width: '100%', height: '100%', fontSize: '50cqmin' }}>
|
||||
<input type="text" name="query" placeholder={`Search ${widget.engines}`} autoComplete="off" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchWidgetSettings = ({ widget, updateWidget }: SearchWidgetProps) => {
|
||||
const [availableEngines] = useAtom(searchEngineState)
|
||||
const [availableEngines] = useAtom(availableEnginesState)
|
||||
|
||||
useEffect(() => {
|
||||
const engines = widget.engines.filter(engine => Object.keys(availableEngines).includes(engine))
|
||||
updateWidget(widget.id, { engines })
|
||||
}, [availableEngines])
|
||||
|
||||
const handleEngineChange = useCallback(
|
||||
(e: any) => {
|
||||
@@ -0,0 +1,4 @@
|
||||
import { FrontendBindings } from '../../shared/RPC'
|
||||
|
||||
export const IpcService = FrontendBindings
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GlobalSettings, WidgetData } from '../types';
|
||||
import { DEFAULT_SETTINGS } from '../defaults';
|
||||
import { GlobalSettings } from '../types';
|
||||
import { DEFAULT_SETTINGS } from '../state/defaults';
|
||||
|
||||
export const settingsService = {
|
||||
saveSetting: async <K extends keyof GlobalSettings>(key: K, value: GlobalSettings[K]) => {
|
||||
@@ -13,4 +13,7 @@ export const settingsService = {
|
||||
return loaded[key] !== undefined ? loaded[key] as GlobalSettings[K] : DEFAULT_SETTINGS[key]
|
||||
},
|
||||
|
||||
exportSettings: async (): Promise<GlobalSettings> => {
|
||||
return await chrome.storage.sync.get(null) as GlobalSettings
|
||||
}
|
||||
};
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import { globalSettingsAtom } from "./globalSettings.atom";
|
||||
import { globalSettingsAtom } from "./util/globalSettings.atom";
|
||||
|
||||
export const backgroundImageState = globalSettingsAtom('backgroundUrl')
|
||||
@@ -1,48 +1,66 @@
|
||||
import { GlobalSettings, SearchWidgetData, ClockWidgetData, WidgetTypes, WidgetData } from './types';
|
||||
import { GlobalSettings, WidgetTypeValues, WidgetTypeMap } from '../types';
|
||||
|
||||
|
||||
|
||||
export const intitialWidgetData = (type: WidgetTypes): WidgetData => {
|
||||
export const intitialWidgetData = (type: WidgetTypeValues): WidgetTypeMap[typeof type] => {
|
||||
switch (type) {
|
||||
case 'clock':
|
||||
return {
|
||||
gridHeight: 3,
|
||||
gridWidth: 6,
|
||||
gridX: 15,
|
||||
gridY: 6,
|
||||
height: 6,
|
||||
width: 12,
|
||||
y: 30,
|
||||
x: 12,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
showSeconds: false,
|
||||
use24Hour: true,
|
||||
type: 'clock',
|
||||
background: true,
|
||||
id: `${type}-${Date.now()}`
|
||||
} as ClockWidgetData
|
||||
showIn: 'both',
|
||||
id: `${type}-${Date.now()}`,
|
||||
}
|
||||
case 'search':
|
||||
return {
|
||||
gridHeight: 1,
|
||||
gridWidth: 10,
|
||||
gridX: 13,
|
||||
gridY: 6,
|
||||
height: 2,
|
||||
width: 20,
|
||||
y: 26,
|
||||
x: 12,
|
||||
id: `${type}-${Date.now()}`,
|
||||
type: 'search',
|
||||
engines: ['Google'],
|
||||
background: true,
|
||||
} as SearchWidgetData
|
||||
showIn: 'both',
|
||||
}
|
||||
case 'globalsettings':
|
||||
return {
|
||||
background: false,
|
||||
height: 2,
|
||||
width: 2,
|
||||
y: 2,
|
||||
x: 2,
|
||||
id: 'globalsettings',
|
||||
type: 'globalsettings',
|
||||
permanent: true,
|
||||
showIn: 'both'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
grid: {
|
||||
show: true,
|
||||
cols: 72,
|
||||
rows: 36
|
||||
},
|
||||
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')
|
||||
intitialWidgetData('clock'),
|
||||
intitialWidgetData('globalsettings')
|
||||
],
|
||||
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}'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { globalSettingsAtom } from "./util/globalSettings.atom";
|
||||
|
||||
export const editState = globalSettingsAtom("editEnabled")
|
||||
@@ -0,0 +1,3 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const globalSettingsOpenState = atom(false)
|
||||
@@ -0,0 +1,3 @@
|
||||
import { globalSettingsAtom } from "./util/globalSettings.atom";
|
||||
|
||||
export const gridState = globalSettingsAtom('grid')
|
||||
@@ -0,0 +1,8 @@
|
||||
import { atom } from "jotai";
|
||||
import { asyncStorageProxyAtom } from "./util/asyncStorageProxy.atom";
|
||||
|
||||
export const incognitoAllowedState = asyncStorageProxyAtom(
|
||||
() => chrome.extension.isAllowedIncognitoAccess()
|
||||
)
|
||||
|
||||
export const isIncognitoState = atom(chrome.extension.inIncognitoContext)
|
||||
@@ -0,0 +1,3 @@
|
||||
import { globalSettingsAtom } from "./util/globalSettings.atom";
|
||||
|
||||
export const availableEnginesState = globalSettingsAtom('searchEngines')
|
||||
@@ -0,0 +1,24 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
export const asyncStorageProxyAtom = <T>(
|
||||
getter: () => Promise<T>,
|
||||
setter: (value: T) => Promise<void> = async () => { throw new Error('AsyncProxyAtom setter not defined') }
|
||||
) => {
|
||||
const storage = atom<T | undefined>(undefined)
|
||||
|
||||
return atom(
|
||||
async (get) => {
|
||||
const existing = get(storage)
|
||||
|
||||
if (existing === undefined) {
|
||||
const remoteValue = await getter()
|
||||
return remoteValue
|
||||
}
|
||||
return existing
|
||||
},
|
||||
async (get, set, update: T) => {
|
||||
await setter(update)
|
||||
set(storage, update)
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { settingsService } from "../../services/settingsService";
|
||||
import { GlobalSettings } from "../../types";
|
||||
import { asyncStorageProxyAtom } from "./asyncStorageProxy.atom";
|
||||
|
||||
export const globalSettingsAtom = <T extends keyof GlobalSettings>(key:T) =>
|
||||
asyncStorageProxyAtom(
|
||||
async () => {const value = await settingsService.getSetting(key); console.log('reading', key, value); return value;},
|
||||
(update) => {console.log('saving', key, update); return settingsService.saveSetting(key, update)}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { globalSettingsAtom } from "./util/globalSettings.atom";
|
||||
|
||||
export const widgetState = globalSettingsAtom('widgets')
|
||||
@@ -0,0 +1,58 @@
|
||||
export type WidgetData<T extends WidgetTypeValues = WidgetTypeValues> = {
|
||||
id: string;
|
||||
type: T;
|
||||
y: number;
|
||||
x: number;
|
||||
width: number;
|
||||
height: number;
|
||||
background: boolean;
|
||||
permanent?: true;
|
||||
showIn: 'standard' | 'incognito' | 'both'
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type SearchWidgetData = WidgetData<'search'> & {
|
||||
engines: Array<string>;
|
||||
}
|
||||
|
||||
export type ClockWidgetData = WidgetData<'clock'> & {
|
||||
showSeconds: boolean;
|
||||
use24Hour: boolean;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type GlobalSettingsData = WidgetData<'globalsettings'> & {
|
||||
permanent: true;
|
||||
showIn: 'both'
|
||||
}
|
||||
|
||||
export type WidgetTypeValues = (
|
||||
SearchWidgetData |
|
||||
ClockWidgetData |
|
||||
GlobalSettingsData
|
||||
)['type']
|
||||
|
||||
export type WidgetTypeMap<WidgetType extends WidgetTypeValues = WidgetTypeValues> = {
|
||||
[Key in WidgetType]: WidgetData<Key>
|
||||
} & {
|
||||
'clock': ClockWidgetData,
|
||||
'search': SearchWidgetData,
|
||||
'globalsettings': GlobalSettingsData,
|
||||
}
|
||||
|
||||
export interface GlobalSettings {
|
||||
grid: {
|
||||
show: boolean;
|
||||
rows: number;
|
||||
cols: number;
|
||||
}
|
||||
backgroundUrl: string;
|
||||
widgets: WidgetData[];
|
||||
editEnabled: boolean;
|
||||
searchEngines: { [key in string]: string}
|
||||
}
|
||||
|
||||
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
|
||||
widget: W,
|
||||
settings: S
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NewTab } from './components/NewTab';
|
||||
|
||||
export const App: React.FC = () => {
|
||||
return <NewTab />;
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { CogwheelIcon } from './CogwheelIcon';
|
||||
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 { ModalOverlay } from './ModalOverlay';
|
||||
import { backgroundImageState } from '../state/backgroundImage.state';
|
||||
|
||||
interface SettingsPanelProps {
|
||||
addWidget: (widget: WidgetData) => void,
|
||||
}
|
||||
|
||||
export const GlobalSettingsPanel: React.FC<SettingsPanelProps> = ({
|
||||
addWidget
|
||||
}) => {
|
||||
const [isModalShowing, setModalShowing] = useState(false);
|
||||
|
||||
const [editEnabled, setEditEnabled] = useAtom(editState);
|
||||
const [showGrid, setShowGrid] = useAtom(showGridState);
|
||||
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
|
||||
|
||||
const createWidget = (type: WidgetTypes) => addWidget(intitialWidgetData(type));
|
||||
|
||||
return (
|
||||
<>
|
||||
<button className="settings-toggle" onClick={() => setModalShowing(true)}>
|
||||
<CogwheelIcon size={24} />
|
||||
</button>
|
||||
<ModalOverlay
|
||||
setVisible={setModalShowing}
|
||||
visible={isModalShowing}
|
||||
>
|
||||
<h2>Settings</h2>
|
||||
<label>
|
||||
Background URL:
|
||||
<input
|
||||
type="text"
|
||||
value={backgroundImage}
|
||||
onChange={(e) => setBackgroundImage(e.target.value)}
|
||||
placeholder="Enter image URL"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Drag Enabled:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editEnabled}
|
||||
onChange={() => setEditEnabled(!editEnabled)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Show Grid:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showGrid}
|
||||
onChange={() => setShowGrid(!showGrid)}
|
||||
/>
|
||||
</label>
|
||||
<div className="widget-buttons">
|
||||
<button onClick={() => createWidget('clock')}>Add Clock</button>
|
||||
<button onClick={() => createWidget('search')}>Add Search</button>
|
||||
</div>
|
||||
</ModalOverlay >
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback, Suspense, startTransition } from 'react';
|
||||
import { useAtom, useAtomValue } from 'jotai'
|
||||
import { WidgetFactory } from './WidgetFactory';
|
||||
import { GlobalSettingsPanel } from './GlobalSettingsPanel';
|
||||
import { showGridState } from '../state/showGrid.state';
|
||||
import { WidgetData } from '../types';
|
||||
import { settingsService } from '../services/settingsService';
|
||||
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 [showGrid] = useAtom(showGridState)
|
||||
const [widgets, setWidgets] = useAtom(widgetState)
|
||||
const [backgroundImage] = useAtom(backgroundImageState)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setGridSize({
|
||||
width: window.innerWidth / GRID_COLS,
|
||||
height: window.innerHeight / GRID_ROWS
|
||||
});
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [window.innerWidth, window.innerHeight])
|
||||
|
||||
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
|
||||
|
||||
const updateWidget = useCallback(
|
||||
<T extends WidgetData>(id: string, updates: Partial<T>) => {
|
||||
console.log(id, updates)
|
||||
|
||||
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 (
|
||||
<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}%`,
|
||||
...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}%`,
|
||||
...coloredGrindlines(i, GRID_ROWS),
|
||||
}} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{widgets.map(widget => (
|
||||
<WidgetFactory
|
||||
key={widget.id}
|
||||
widget={widget}
|
||||
gridSize={gridSize}
|
||||
updateWidget={updateWidget}
|
||||
removeWidget={removeWidget}
|
||||
/>
|
||||
))}
|
||||
</ Suspense>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
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,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { WidgetData, WidgetExport, WidgetTypes } from '../types';
|
||||
import { Widget } from './Widget';
|
||||
import { SearchWidget } from './SearchWidget';
|
||||
import { ClockWidget } from './ClockWidget';
|
||||
|
||||
interface WidgetFactoryProps {
|
||||
widget: WidgetData;
|
||||
gridSize: { width: number; height: number };
|
||||
updateWidget: (id: string, data: Partial<WidgetData>) => void;
|
||||
removeWidget: (id:string) => void;
|
||||
}
|
||||
|
||||
const WidgetMap: { [key in WidgetTypes]: WidgetExport } = {
|
||||
'clock': ClockWidget,
|
||||
'search': SearchWidget
|
||||
}
|
||||
|
||||
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
|
||||
widgetData={widget}
|
||||
gridSize={gridSize}
|
||||
settingsContent={settings}
|
||||
updateWidget={updateWidget}
|
||||
removeWidget={removeWidget}
|
||||
>
|
||||
{component}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
import { globalSettingsAtom } from "./globalSettings.atom";
|
||||
|
||||
export const editState = globalSettingsAtom("editEnabled")
|
||||
@@ -1,28 +0,0 @@
|
||||
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,3 +0,0 @@
|
||||
import { globalSettingsAtom } from "./globalSettings.atom";
|
||||
|
||||
export const searchEngineState = globalSettingsAtom('searchEngines')
|
||||
@@ -1,3 +0,0 @@
|
||||
import { globalSettingsAtom } from "./globalSettings.atom";
|
||||
|
||||
export const showGridState = globalSettingsAtom('showGrid')
|
||||
@@ -1,3 +0,0 @@
|
||||
import { globalSettingsAtom } from "./globalSettings.atom";
|
||||
|
||||
export const widgetState = globalSettingsAtom('widgets')
|
||||
@@ -1,37 +0,0 @@
|
||||
export interface WidgetData {
|
||||
id: string;
|
||||
type: WidgetTypes;
|
||||
gridX: number;
|
||||
gridY: number;
|
||||
gridWidth: number;
|
||||
gridHeight: number;
|
||||
background: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface SearchWidgetData extends WidgetData {
|
||||
type: 'search';
|
||||
engines: Array<string>;
|
||||
}
|
||||
|
||||
export interface ClockWidgetData extends WidgetData {
|
||||
type: 'clock';
|
||||
showSeconds: boolean;
|
||||
use24Hour: boolean;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type WidgetTypes = (SearchWidgetData | ClockWidgetData)['type']
|
||||
|
||||
export interface GlobalSettings {
|
||||
backgroundUrl: string;
|
||||
widgets: WidgetData[];
|
||||
editEnabled: boolean;
|
||||
showGrid: boolean;
|
||||
searchEngines: { [key in string]: string}
|
||||
}
|
||||
|
||||
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
|
||||
widget: W,
|
||||
settings: S
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ExposedEndpoints } from "../worker/Endpoints"
|
||||
|
||||
type NamedFunction<ArgT, RetT, NameT extends string> = { name: NameT } & ((a: ArgT) => RetT)
|
||||
|
||||
type Promised<T> = T extends Promise<any> ? T : Promise<T>
|
||||
|
||||
const wrapIpc = <A, R, N extends string = any>(fn: NamedFunction<A, R, N>): NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N> => {
|
||||
const func = function (message: A) {
|
||||
return new Promise((res, rej) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
endpoint: fn.name,
|
||||
args: message
|
||||
},
|
||||
(response) => { res(response) }
|
||||
)
|
||||
})
|
||||
}
|
||||
Object.defineProperty(fn, "name", { value: fn.name });
|
||||
return func as NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N>
|
||||
}
|
||||
|
||||
export type RPCs = typeof ExposedEndpoints
|
||||
|
||||
export type FrontendIpcService = {
|
||||
[endpoint in keyof RPCs]: NamedFunction<Parameters<RPCs[endpoint]>[0], Promised<ReturnType<RPCs[endpoint]>>, string>
|
||||
}
|
||||
|
||||
export const FrontendBindings: FrontendIpcService = Object.entries(ExposedEndpoints)
|
||||
.map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn) }))
|
||||
.reduce((prev, curr) => Object.assign(prev, curr), {}) as FrontendIpcService
|
||||
@@ -0,0 +1,6 @@
|
||||
export const ExposedEndpoints = {
|
||||
writeFile: (message: { path: string, fileContent: string }) => {
|
||||
console.log("writing", message.fileContent, "to", message.path)
|
||||
return "OK"
|
||||
}
|
||||
} as const;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ExposedEndpoints } from "./Endpoints";
|
||||
|
||||
chrome.runtime.onMessage.addListener((param: { endpoint: keyof typeof ExposedEndpoints, args: any }, sender, sendReponse) => {
|
||||
const returnValue = ExposedEndpoints[param.endpoint].apply({}, param.args)
|
||||
if(returnValue !== undefined){
|
||||
sendReponse(returnValue)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user