add quote widget

This commit is contained in:
Your Name
2025-04-18 19:49:42 -04:00
parent 066bb11814
commit 4975dea12a
30 changed files with 468 additions and 248 deletions
+11 -4
View File
@@ -7,14 +7,21 @@
"newtab": "frontend/utab.html" "newtab": "frontend/utab.html"
}, },
"permissions": [ "permissions": [
"storage" "storage",
"unlimitedStorage"
],
"optional_permissions": [],
"host_permissions": [
"https://zenquotes.io/*"
], ],
"icons": { "icons": {
"128": "icon.png" "128": "icon.png"
}, },
"incognito": "split", "incognito": "split",
"background": { "browser_specific_settings": {
"service_worker": "worker/main.js", "gecko": {
"type": "module" "id": "addon@example.com",
"strict_min_version": "58.0"
}
} }
} }
+2 -1
View File
@@ -9,7 +9,8 @@
"copy-static": "mkdir dist/frontend && cp -r static/* dist/frontend/", "copy-static": "mkdir dist/frontend && cp -r static/* dist/frontend/",
"build-frontend": "esbuild src/frontend/index.tsx --bundle --outfile=dist/frontend/index.js --minify --sourcemap", "build-frontend": "esbuild src/frontend/index.tsx --bundle --outfile=dist/frontend/index.js --minify --sourcemap",
"build-worker": "esbuild src/worker/main.ts --bundle --outfile=dist/worker/main.js --minify --sourcemap", "build-worker": "esbuild src/worker/main.ts --bundle --outfile=dist/worker/main.js --minify --sourcemap",
"clean": "rm -rf ./dist" "clean": "rm -rf ./dist",
"type-check": "tsc --noEmit"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
-3
View File
@@ -2,13 +2,10 @@ import React, { useEffect } from 'react';
import { NewTab } from './components/NewTab'; import { NewTab } from './components/NewTab';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { backgroundImageState } from './state/backgroundImage.state'; import { backgroundImageState } from './state/backgroundImage.state';
import { IpcService } from './services/ipcService';
export const App: React.FC = () => { export const App: React.FC = () => {
const [backgroundImage] = useAtom(backgroundImageState) const [backgroundImage] = useAtom(backgroundImageState)
useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]); useEffect(() => { document.body.style.backgroundImage = `url(${backgroundImage})` }, [backgroundImage]);
IpcService.writeFile({ path: 'testpath', fileContent: 'testcontent' }).then(console.log)
return <NewTab />; return <NewTab />;
}; };
+1 -1
View File
@@ -3,7 +3,7 @@ import { useAtom } from 'jotai'
import { WidgetFactory } from './WidgetFactory'; import { WidgetFactory } from './WidgetFactory';
import { gridState } from '../state/grid.state'; import { gridState } from '../state/grid.state';
import { widgetState } from '../state/widget.state'; import { widgetState } from '../state/widget.state';
import { GlobalSettingsPanel } from './global-settings/GlobalSettingsPanel'; import { GlobalSettingsPanel } from './widgets/globalsettings/GlobalSettingsPanel';
import { isIncognitoState } from '../state/incogntio.state'; import { isIncognitoState } from '../state/incogntio.state';
import { GridOverlay } from './grid-overlay/GridOverlay'; import { GridOverlay } from './grid-overlay/GridOverlay';
+14 -15
View File
@@ -1,24 +1,23 @@
import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react'; import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react';
import { WidgetData } from '../types'; import { WidgetData } from '../types';
import { CogwheelIcon } from './components/CogwheelIcon'; import { CogwheelIcon } from './reusable-components/CogwheelIcon';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { editState } from '../state/edit.state'; import { editState } from '../state/edit.state';
import { CloseButton } from './components/Closebutton'; import { CloseButton } from './reusable-components/Closebutton';
import { ModalOverlay } from './components/ModalOverlay'; import { ModalOverlay } from './reusable-components/ModalOverlay';
import { SettingsComponent } from './components/SettingsComponent'; import { SettingsComponent } from './reusable-components/SettingsComponent';
import MovieEditIcon from '@mui/icons-material/MovieEdit';
interface WidgetProps { interface WidgetProps {
widgetData: 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; removeWidget: (id: string) => void;
disableRemove?: boolean;
disableEdit?: boolean;
} }
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget, disableEdit, disableRemove }) => { 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);
@@ -62,11 +61,11 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
const rect = widgetRef.current.getBoundingClientRect(); const rect = widgetRef.current.getBoundingClientRect();
const gridX = snapToGrid(rect.left, gridSize.width); const gridX = snapToGrid(rect.left, gridSize.width);
const gridY = snapToGrid(rect.top, gridSize.height); const gridY = snapToGrid(rect.top, gridSize.height);
if (gridX !== widgetData.y || gridY !== widgetData.x) { if (gridX !== widgetData.x || gridY !== widgetData.Y) {
// overwrite the values so prevent a visual "bounce" // overwrite the values so prevent a visual "bounce"
widgetData.y = gridX; widgetData.x = gridX;
widgetData.x = gridY; widgetData.y = gridY;
updateWidget(widgetData.id, { y: gridX, x: gridY }); updateWidget(widgetData.id, { y: gridY, x: gridX });
} }
} }
}; };
@@ -101,8 +100,8 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
useEffect(() => { useEffect(() => {
if (widgetRef.current && !isDragging) { if (widgetRef.current && !isDragging) {
widgetRef.current.style.left = `${widgetData.y * gridSize.width}px`; widgetRef.current.style.left = `${widgetData.x * gridSize.width}px`;
widgetRef.current.style.top = `${widgetData.x * gridSize.height}px`; widgetRef.current.style.top = `${widgetData.y * gridSize.height}px`;
widgetRef.current.style.width = `${(widgetData.width) * gridSize.width}px`; widgetRef.current.style.width = `${(widgetData.width) * gridSize.width}px`;
widgetRef.current.style.height = `${(widgetData.height) * gridSize.height}px`; widgetRef.current.style.height = `${(widgetData.height) * gridSize.height}px`;
} }
@@ -135,7 +134,7 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
className="settings-button" className="settings-button"
onClick={() => setSettingsOpen(true)} onClick={() => setSettingsOpen(true)}
> >
<CogwheelIcon size={24} /> <MovieEditIcon sx={{ fontSize: 16 }} />
</button> </button>
} }
{children} {children}
+8 -5
View File
@@ -1,13 +1,15 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { GlobalSettings, WidgetData, WidgetExport, WidgetTypeValues } from '../types'; import { GlobalSettings, WidgetData, WidgetExport, WidgetTypeValues } from '../types';
import { Widget } from './Widget'; 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 { useAtom } from 'jotai';
import { widgetState } from '../state/widget.state'; import { widgetState } from '../state/widget.state';
import { settingsService } from '../services/settingsService'; import { settingsService } from '../services/settingsService';
import { QuoteWidget } from './widgets/quote/QuotesWidget';
import { SearchWidget } from './widgets/search/SearchWidget';
import { ClockWidget } from './widgets/clock/ClockWidget';
import { GlobalSettingsWidget } from './widgets/globalsettings/GlobalSettingsWidget';
export interface WidgetFactoryProps { export interface WidgetFactoryProps {
widget: WidgetData; widget: WidgetData;
grid: GlobalSettings['grid']; grid: GlobalSettings['grid'];
@@ -16,7 +18,8 @@ export interface WidgetFactoryProps {
const WidgetMap: { [key in WidgetTypeValues]: WidgetExport } = { const WidgetMap: { [key in WidgetTypeValues]: WidgetExport } = {
'clock': ClockWidget, 'clock': ClockWidget,
'search': SearchWidget, 'search': SearchWidget,
'globalsettings': GlobalSettingsWidget 'globalsettings': GlobalSettingsWidget,
'quote': QuoteWidget
} }
export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, grid }) => { export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, grid }) => {
@@ -56,7 +59,7 @@ export const WidgetFactory: React.FC<WidgetFactoryProps> = ({ widget, grid }) =>
const Wdgt = WidgetMap[widget.type] const Wdgt = WidgetMap[widget.type]
const component = <Wdgt.widget widget={widget} updateWidget={updateWidgets}></Wdgt.widget> const component = <Wdgt.widget widget={widget} updateWidget={updateWidgets}></Wdgt.widget>
const settings = Wdgt.settings({ widget, updateWidget: updateWidgets }) const settings = Wdgt.settings ? Wdgt.settings({ widget, updateWidget: updateWidgets }) : undefined
return ( return (
<Widget <Widget
@@ -1,69 +0,0 @@
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>
}
</>
}
@@ -1,17 +0,0 @@
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"
/>
</>)
}
@@ -1,46 +0,0 @@
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>
</>)
}
@@ -1,4 +1,5 @@
import React, { MouseEvent } from "react" import React, { MouseEvent } from "react"
import CloseIcon from '@mui/icons-material/Close';
type ButtonProps = { type ButtonProps = {
onClick: React.MouseEventHandler<HTMLButtonElement> onClick: React.MouseEventHandler<HTMLButtonElement>
@@ -6,6 +7,6 @@ type ButtonProps = {
export function CloseButton({ onClick }: ButtonProps) { export function CloseButton({ onClick }: ButtonProps) {
return ( return (
<button className="close-button" onClick={onClick}>×</button> <button className="close-button" onClick={onClick}><CloseIcon sx={{ fontSize: 16 }} /></button>
) )
} }
@@ -7,7 +7,6 @@ interface ModalOverlayProps {
} }
export function ModalOverlay({visible, setVisible, children}: ModalOverlayProps) { export function ModalOverlay({visible, setVisible, children}: ModalOverlayProps) {
const modalRef = useRef<HTMLDivElement>(null); const modalRef = useRef<HTMLDivElement>(null);
return visible && <div className="settings-modal" ref={modalRef}> return visible && <div className="settings-modal" ref={modalRef}>
@@ -0,0 +1,136 @@
import { Dispatch, ReactElement, useState } from "react"
import { WidgetData } from "../../types"
import { SetStateAction, useAtom } from "jotai"
import { incognitoAllowedState } from "../../state/incogntio.state"
import { Box, List, ListItem, ListItemIcon, ListItemText, MenuItem, Select, Switch, Tab, Tabs, TextField, Typography } from "@mui/material"
import BorderHorizontalIcon from '@mui/icons-material/BorderHorizontal';
import BorderVerticalIcon from '@mui/icons-material/BorderVertical';
import BorderOuterIcon from '@mui/icons-material/BorderOuter';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { TabPanel } from "./TabPanel"
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)
const [tab, setTab] = useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setTab(newValue);
};
return <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' }}
>
<Tab label="Display" />
{children != undefined && <Tab label="This Widget" />}
</Tabs>
<TabPanel value={tab} index={0}>
<List
sx={{ width: '100%', minWidth: 500, bgcolor: 'background.paper' }}
>
<ListItem>
<ListItemIcon>
<BorderHorizontalIcon />
</ListItemIcon>
<ListItemText id="input-width" primary="Width" secondary="(Grid cells)" />
<TextField
value={widgetData.width}
onChange={e => updateWidget(widgetData.id, { width: Math.max(1, parseInt(e.target.value)) })}
id="width-number"
type="number"
variant="standard"
slotProps={{
inputLabel: {
shrink: true,
},
}}
/>
</ListItem>
<ListItem>
<ListItemIcon>
<BorderVerticalIcon />
</ListItemIcon>
<ListItemText primary="Height" secondary="(Grid cells)" />
<TextField
value={widgetData.height}
onChange={e => updateWidget(widgetData.id, { height: Math.max(1, parseInt(e.target.value)) })}
type="number"
variant="standard"
slotProps={{
inputLabel: {
shrink: true,
},
}}
/>
</ListItem>
<ListItem>
<ListItemIcon>
<BorderOuterIcon />
</ListItemIcon>
<ListItemText primary="Background / Border" />
<Switch
checked={widgetData.background}
onChange={_ => updateWidget(widgetData.id, { background: !widgetData.background })}
/>
</ListItem>
<ListItem>
<ListItemIcon>
<VisibilityOffIcon />
</ListItemIcon>
<ListItemText primary="Visible in tab" />
{isIncognitoAllowed ? (
<Select
size="small"
value={widgetData.showIn}
onChange={e => updateWidget(
widgetData.id,
{ showIn: e.target.value as WidgetData['showIn'] }
)}
>
{(['standard', 'incognito', 'both'] as Array<WidgetData['showIn']>)
.map(showInOption => (
<MenuItem key={showInOption} value={showInOption}>
{showInOption}
</MenuItem>
))
}
</Select>
):(
<Typography>
Enable extension in Incognito mode to edit
</Typography>
)}
</ListItem>
</List>
</TabPanel>
{children != undefined &&
<TabPanel value={tab} index={1}>
{children}
</TabPanel>
}
</Box>
}
@@ -0,0 +1,28 @@
import { Box, Typography } from "@mui/material";
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
export 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}`}
style={{width: '100%'}}
{...other}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
}
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { ClockWidgetData, WidgetExport } from '../../types'; import { ClockWidgetData, WidgetExport } from '../../../types';
interface ClockWidgetProps { interface ClockWidgetProps {
widget: ClockWidgetData; widget: ClockWidgetData;
@@ -1,16 +1,17 @@
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { ModalOverlay } from "../components/ModalOverlay"; import { ModalOverlay } from "../../reusable-components/ModalOverlay";
import { useState } from "react"; import { useState } from "react";
import { globalSettingsOpenState } from "../../state/globalSettingsOpen.state"; import { globalSettingsOpenState } from "../../../state/globalSettingsOpen.state";
import { Box, Tab, Tabs, Typography } from "@mui/material"; import { Box, Tab, Tabs } from "@mui/material";
import { UtabSettings } from "./tab-contents/UtabSettings"; import { UtabSettings } from "./tab-contents/UtabSettings";
import { BackgroundSettings } from "./tab-contents/BackgroundSettings"; import { BackgroundSettings } from "./tab-contents/BackgroundSettings";
import { SearchEngineSettings } from "./tab-contents/SearchEngineSettings"; import { SearchEngineSettings } from "./tab-contents/SearchEngineSettings";
import { TabPanel } from "../../reusable-components/TabPanel";
const settingsComponents = { const settingsComponents = {
'uTab': UtabSettings, 'uTab': UtabSettings,
'Background': BackgroundSettings, 'Background': BackgroundSettings,
'Search': SearchEngineSettings 'Search': SearchEngineSettings,
} }
export function GlobalSettingsPanel() { export function GlobalSettingsPanel() {
@@ -29,7 +30,7 @@ export function GlobalSettingsPanel() {
variant="scrollable" variant="scrollable"
value={tab} value={tab}
onChange={handleChange} onChange={handleChange}
aria-label="Vertical tabs example" aria-label="Vertical tabs"
sx={{ borderRight: 1, borderColor: 'divider', textAlign: 'end' }} sx={{ borderRight: 1, borderColor: 'divider', textAlign: 'end' }}
> >
{Object.keys(settingsComponents).map(key => <Tab label={key} />)} {Object.keys(settingsComponents).map(key => <Tab label={key} />)}
@@ -46,28 +47,3 @@ export function GlobalSettingsPanel() {
) )
} }
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>
);
}
@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { CogwheelIcon } from '../components/CogwheelIcon'; import { CogwheelIcon } from '../../reusable-components/CogwheelIcon';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { WidgetData, WidgetExport } from '../../types'; import { WidgetData, WidgetExport } from '../../../types';
import { globalSettingsOpenState } from '../../state/globalSettingsOpen.state'; import { globalSettingsOpenState } from '../../../state/globalSettingsOpen.state';
interface GlobalSettingsProps { interface GlobalSettingsProps {
addWidget: (widget: WidgetData) => void, addWidget: (widget: WidgetData) => void,
@@ -17,6 +17,5 @@ const GlobalSettingsComponent: React.FC<GlobalSettingsProps> = ({ addWidget }) =
}; };
export const GlobalSettingsWidget: WidgetExport = { export const GlobalSettingsWidget: WidgetExport = {
settings: () => <></>,
widget: GlobalSettingsComponent widget: GlobalSettingsComponent
} }
@@ -0,0 +1,19 @@
import { Box, TextField } from "@mui/material";
import { useAtom } from "jotai";
import { backgroundImageState } from "../../../../state/backgroundImage.state";
export function BackgroundSettings() {
const [backgroundImage, setBackgroundImage] = useAtom(backgroundImageState)
return (
<Box>
<TextField
type="text"
value={backgroundImage}
onChange={(e) => setBackgroundImage(e.target.value)}
placeholder="Enter image URL"
/>
</Box>
)
}
@@ -1,6 +1,6 @@
import { Box, Button, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material"; import { Box, Button, IconButton, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from "@mui/material";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { availableEnginesState } from "../../../state/searchEngines.state"; import { availableEnginesState } from "../../../../state/searchEngines.state";
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
@@ -19,8 +19,6 @@ export function SearchEngineSettings() {
[aliasInput]: searchStringInput [aliasInput]: searchStringInput
} }
console.log(newEngines)
setAvailableSearchEngines(newEngines) setAvailableSearchEngines(newEngines)
setAliasInput('') setAliasInput('')
setSearchStringInput('') setSearchStringInput('')
@@ -38,7 +36,7 @@ export function SearchEngineSettings() {
return ( return (
<Box> <Box>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table"> <Table aria-label="simple table">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell align="left" style={{ fontWeight: 'bold' }}>Alias</TableCell> <TableCell align="left" style={{ fontWeight: 'bold' }}>Alias</TableCell>
@@ -78,6 +76,5 @@ export function SearchEngineSettings() {
</Table> </Table>
</TableContainer> </TableContainer>
</Box> </Box>
) )
} }
@@ -0,0 +1,58 @@
import { Box, List, ListItem, ListItemIcon, ListItemText, 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 DesignServicesIcon from '@mui/icons-material/DesignServices';
import GridOnIcon from '@mui/icons-material/GridOn';
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]
);
return (
<Box>
<List
sx={{ bgcolor: 'background.paper', width: 1 }}
>
<ListItem>
<ListItemIcon>
<DesignServicesIcon />
</ListItemIcon>
<ListItemText primary="Editmode" />
<Switch
checked={editEnabled}
onChange={() => setEditEnabled(!editEnabled)}
/>
</ListItem>
<ListItem>
<ListItemIcon>
<GridOnIcon />
</ListItemIcon>
<ListItemText primary="Show grid" />
<Switch
checked={grid.show}
onChange={() => setGrid({ ...grid, show: !grid.show })}
/>
</ListItem>
<button onClick={() => createWidget('clock')}>Clock</button>
<button onClick={() => createWidget('search')}>Search</button>
<button onClick={() => createWidget('quote')}>Quotes</button>
</List>
</Box>
)
}
@@ -0,0 +1,64 @@
import React, { useCallback, useEffect, useState } from 'react';
import { QuoteWidgetData, WidgetExport } from '../../../types';
import { Box } from '@mui/material';
type Quote = {
quote: string,
author: string,
}
interface QuoteWidgetProps {
widget: QuoteWidgetData;
updateWidget: (id: string, update: Partial<QuoteWidgetData>) => void
}
const getQuote = async (): Promise<Quote> => {
try {
const response = await fetch("https://zenquotes.io/?api=random", { method: "GET", mode: "cors" });
var data = await response.json();
return {
quote: data[0].q,
author: data[0].a
};
}catch(e){
return {
quote: String(e),
author: "https://zenquotes.io/?api=random"
}
}
}
export const QuoteWidgetComponent: React.FC<QuoteWidgetProps> = ({ widget, updateWidget }) => {
const [quote, setQuote] = useState<Quote>()
const updateQuote = useCallback(async () => {
const quote = await getQuote()
console.log("QUOTE", quote)
setQuote(quote)
updateWidget(widget.id, { lastRefresh: Date.now() })
}, [])
if (!quote || (Date.now() - widget.lastRefresh > widget.refreshInterval)) {
updateQuote()
}
return <>
<div className="quote-text">{quote?.quote}</div>
<div className="author">{quote?.author}</div>
</>;
};
export const QuoteWidgetSettings = ({ widget, updateWidget }: QuoteWidgetProps) => {
return (
<Box>
</Box>
);
};
export const QuoteWidget: WidgetExport = {
widget: QuoteWidgetComponent,
settings: QuoteWidgetSettings
}
@@ -1,8 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect } from 'react';
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../../types'; import { SearchWidgetData, WidgetExport } from '../../../types';
import { availableEnginesState } from '../../state/searchEngines.state'; import { availableEnginesState } from '../../../state/searchEngines.state';
import { useAtom, useAtomValue } from 'jotai'; import { useAtom } from 'jotai';
import { TextField } from '@mui/material';
interface SearchWidgetProps { interface SearchWidgetProps {
widget: SearchWidgetData; widget: SearchWidgetData;
-1
View File
@@ -1,4 +1,3 @@
import { FrontendBindings } from '../../shared/RPC' import { FrontendBindings } from '../../shared/RPC'
export const IpcService = FrontendBindings export const IpcService = FrontendBindings
+39 -16
View File
@@ -4,41 +4,63 @@ export const intitialWidgetData = (type: WidgetTypeValues): WidgetTypeMap[typeof
switch (type) { switch (type) {
case 'clock': case 'clock':
return { return {
type,
id: `${type}-${Date.now()}`,
showIn: 'both',
height: 6, height: 6,
width: 12, width: 12,
y: 30, x: 30,
x: 12, y: 3,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
showSeconds: false, showSeconds: false,
use24Hour: true, use24Hour: true,
type: 'clock',
background: true, background: true,
showIn: 'both',
id: `${type}-${Date.now()}`,
} }
case 'search': case 'search':
return { return {
type,
id: `${type}-${Date.now()}`,
showIn: 'both',
height: 2, height: 2,
width: 20, width: 20,
y: 26, x: 26,
x: 12, y: 17,
id: `${type}-${Date.now()}`,
type: 'search',
engines: ['Google'],
background: true, background: true,
showIn: 'both',
engines: ['Google'],
} }
case 'globalsettings': case 'globalsettings':
return { return {
type,
id: `${type}-${Date.now()}`,
showIn: 'both',
background: false, background: false,
height: 2, height: 2,
width: 2, width: 2,
y: 2, y: 2,
x: 2, x: 2,
id: 'globalsettings',
type: 'globalsettings',
permanent: true, permanent: true,
showIn: 'both' }
case 'quote':
return {
type,
id: `${type}-${Date.now()}`,
showIn: 'both',
background: false,
height: 6,
width: 72,
y: 30,
x: 0,
showAuthor: true,
refreshInterval: 86400000, // 1 day
lastRefresh: 0
} }
} }
} }
@@ -51,9 +73,10 @@ 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', backgroundUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Cat_August_2010-4.jpg/2880px-Cat_August_2010-4.jpg',
widgets: [ widgets: [
intitialWidgetData('search'), intitialWidgetData('search'),
intitialWidgetData('clock'), intitialWidgetData('clock'),
intitialWidgetData('globalsettings') intitialWidgetData('globalsettings'),
intitialWidgetData('quote')
], ],
editEnabled: true, editEnabled: true,
searchEngines: { searchEngines: {
+14 -7
View File
@@ -21,25 +21,32 @@ export type ClockWidgetData = WidgetData<'clock'> & {
timezone: string; timezone: string;
} }
export type QuoteWidgetData = WidgetData<'quote'> & {
showAuthor: boolean;
refreshInterval: number;
lastRefresh: number;
}
export type GlobalSettingsData = WidgetData<'globalsettings'> & { export type GlobalSettingsData = WidgetData<'globalsettings'> & {
permanent: true; permanent: true;
showIn: 'both' showIn: 'both'
} }
export type WidgetTypeValues = ( export type WidgetTypeValues = (
SearchWidgetData | | SearchWidgetData
ClockWidgetData | | ClockWidgetData
GlobalSettingsData | GlobalSettingsData
| QuoteWidgetData
)['type'] )['type']
export type WidgetTypeMap<WidgetType extends WidgetTypeValues = WidgetTypeValues> = { export type WidgetTypeMap = {
[Key in WidgetType]: WidgetData<Key>
} & {
'clock': ClockWidgetData, 'clock': ClockWidgetData,
'search': SearchWidgetData, 'search': SearchWidgetData,
'globalsettings': GlobalSettingsData, 'globalsettings': GlobalSettingsData,
'quote': QuoteWidgetData
} }
export interface GlobalSettings { export interface GlobalSettings {
grid: { grid: {
show: boolean; show: boolean;
@@ -54,5 +61,5 @@ export interface GlobalSettings {
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = { export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
widget: W, widget: W,
settings: S settings?: S
} }
+17 -11
View File
@@ -1,31 +1,37 @@
import { ExposedEndpoints } from "../worker/Endpoints" import { ExposedEndpoints } from "../worker/Endpoints"
type NamedFunction<ArgT, RetT, NameT extends string> = { name: NameT } & ((a: ArgT) => RetT) type NamedFunction<ArgT extends any[], RetT, NameT extends string> = { name: NameT } & ((...args: ArgT) => RetT);
type Promised<T> = T extends Promise<any> ? T : Promise<T> 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 wrapIpc = <ArgT extends any[], RetT, NameT extends string>(
const func = function (message: A) { fn: NamedFunction<ArgT, RetT, NameT>
): NamedFunction<ArgT, Promised<RetT>, NameT> => {
const func = function (...args: ArgT): Promise<RetT> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ {
endpoint: fn.name, endpoint: fn.name,
args: message args,
}, },
(response) => { res(response) } (response) => { res(response); }
) )
}) })
} }
Object.defineProperty(fn, "name", { value: fn.name }); Object.defineProperty(func, "name", { value: fn.name });
return func as NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N> return func as NamedFunction<ArgT, Promised<RetT>, NameT>;
} };
export type RPCs = typeof ExposedEndpoints export type RPCs = typeof ExposedEndpoints
export type FrontendIpcService = { export type FrontendIpcService = {
[endpoint in keyof RPCs]: NamedFunction<Parameters<RPCs[endpoint]>[0], Promised<ReturnType<RPCs[endpoint]>>, string> [endpoint in keyof RPCs]: NamedFunction<
Parameters<RPCs[endpoint]>,
Promised<ReturnType<RPCs[endpoint]>>,
endpoint
>
} }
export const FrontendBindings: FrontendIpcService = Object.entries(ExposedEndpoints) export const FrontendBindings: FrontendIpcService = Object.entries(ExposedEndpoints)
.map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn) })) .map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn as any) }))
.reduce((prev, curr) => Object.assign(prev, curr), {}) as FrontendIpcService .reduce((prev, curr) => Object.assign(prev, curr), {}) as FrontendIpcService
+5 -2
View File
@@ -1,6 +1,9 @@
export const ExposedEndpoints = { export const ExposedEndpoints = {
writeFile: (message: { path: string, fileContent: string }) => { writeFile: (filename: string, fileContent: string) => {
console.log("writing", message.fileContent, "to", message.path)
return "OK" return "OK"
},
writeFileAsync: async (path: string, fileContent: string) => {
return "async OK"
} }
} as const; } as const;
+7 -1
View File
@@ -1,7 +1,13 @@
import { ExposedEndpoints } from "./Endpoints"; import { ExposedEndpoints } from "./Endpoints";
chrome.runtime.onMessage.addListener((param: { endpoint: keyof typeof ExposedEndpoints, args: any }, sender, sendReponse) => { chrome.runtime.onMessage.addListener((param: { endpoint: keyof typeof ExposedEndpoints, args: any }, sender, sendReponse) => {
const returnValue = ExposedEndpoints[param.endpoint].apply({}, param.args) const returnValue = (ExposedEndpoints[param.endpoint] as Function).apply(undefined, param.args)
if(returnValue instanceof Promise){
returnValue.then(value => {
sendReponse(value)
})
return true;
}
if(returnValue !== undefined){ if(returnValue !== undefined){
sendReponse(returnValue) sendReponse(returnValue)
return true; return true;
+6 -2
View File
@@ -26,6 +26,11 @@ body {
border-radius: 5px; border-radius: 5px;
} }
.globalsettings {
overflow: visible !important;
padding: 0px !important;
}
.aero { .aero {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
@@ -138,8 +143,7 @@ body {
padding: 20px; padding: 20px;
border-radius: 10px; border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
min-width: 400px; width: 800px;
max-width: 60vw;
text-align: center; text-align: center;
} }
+21
View File
@@ -5,6 +5,12 @@
font-style: normal; font-style: normal;
} }
.clock {
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* Styles for ClockWidget time display */ /* Styles for ClockWidget time display */
.clock>.time { .clock>.time {
color: #ffffff; color: #ffffff;
@@ -21,6 +27,21 @@
font-size: 15cqmin; font-size: 15cqmin;
} }
/* Styles for ClockWidget time display */
.quote>.quote-text {
color: #AEAEAE;
font-style: italic;
text-align: center;
font-size: 25cqmin;
}
.quote>.author {
font-size: small;
text-align: center;
color: #AEAEAE;
font-size: 15cqh;
}
/* Styles for SearchWidget */ /* Styles for SearchWidget */
.search form { .search form {
display: flex; display: flex;