add quote widget
This commit is contained in:
+11
-4
@@ -7,14 +7,21 @@
|
||||
"newtab": "frontend/utab.html"
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
"storage",
|
||||
"unlimitedStorage"
|
||||
],
|
||||
"optional_permissions": [],
|
||||
"host_permissions": [
|
||||
"https://zenquotes.io/*"
|
||||
],
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
},
|
||||
"incognito": "split",
|
||||
"background": {
|
||||
"service_worker": "worker/main.js",
|
||||
"type": "module"
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "addon@example.com",
|
||||
"strict_min_version": "58.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -9,7 +9,8 @@
|
||||
"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-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": "",
|
||||
"license": "ISC",
|
||||
|
||||
@@ -2,13 +2,10 @@ 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 />;
|
||||
};
|
||||
@@ -3,7 +3,7 @@ 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 { GlobalSettingsPanel } from './widgets/globalsettings/GlobalSettingsPanel';
|
||||
import { isIncognitoState } from '../state/incogntio.state';
|
||||
import { GridOverlay } from './grid-overlay/GridOverlay';
|
||||
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import React, { useRef, useState, useEffect, ReactElement, useCallback, startTransition } from 'react';
|
||||
import { WidgetData } from '../types';
|
||||
import { CogwheelIcon } from './components/CogwheelIcon';
|
||||
import { CogwheelIcon } from './reusable-components/CogwheelIcon';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { editState } from '../state/edit.state';
|
||||
import { CloseButton } from './components/Closebutton';
|
||||
import { ModalOverlay } from './components/ModalOverlay';
|
||||
import { SettingsComponent } from './components/SettingsComponent';
|
||||
import { CloseButton } from './reusable-components/Closebutton';
|
||||
import { ModalOverlay } from './reusable-components/ModalOverlay';
|
||||
import { SettingsComponent } from './reusable-components/SettingsComponent';
|
||||
import MovieEditIcon from '@mui/icons-material/MovieEdit';
|
||||
|
||||
interface WidgetProps {
|
||||
widgetData: WidgetData;
|
||||
gridSize: { width: number; height: number };
|
||||
children: React.ReactNode;
|
||||
settingsContent: ReactElement;
|
||||
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, disableEdit, disableRemove }) => {
|
||||
export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children, settingsContent, updateWidget, removeWidget }) => {
|
||||
const widgetRef = 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 gridX = snapToGrid(rect.left, gridSize.width);
|
||||
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"
|
||||
widgetData.y = gridX;
|
||||
widgetData.x = gridY;
|
||||
updateWidget(widgetData.id, { y: gridX, x: gridY });
|
||||
widgetData.x = gridX;
|
||||
widgetData.y = gridY;
|
||||
updateWidget(widgetData.id, { y: gridY, x: gridX });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -101,8 +100,8 @@ export const Widget: React.FC<WidgetProps> = ({ widgetData, gridSize, children,
|
||||
|
||||
useEffect(() => {
|
||||
if (widgetRef.current && !isDragging) {
|
||||
widgetRef.current.style.left = `${widgetData.y * gridSize.width}px`;
|
||||
widgetRef.current.style.top = `${widgetData.x * gridSize.height}px`;
|
||||
widgetRef.current.style.left = `${widgetData.x * gridSize.width}px`;
|
||||
widgetRef.current.style.top = `${widgetData.y * gridSize.height}px`;
|
||||
widgetRef.current.style.width = `${(widgetData.width) * gridSize.width}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"
|
||||
onClick={() => setSettingsOpen(true)}
|
||||
>
|
||||
<CogwheelIcon size={24} />
|
||||
<MovieEditIcon sx={{ fontSize: 16 }} />
|
||||
</button>
|
||||
}
|
||||
{children}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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';
|
||||
|
||||
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 {
|
||||
widget: WidgetData;
|
||||
grid: GlobalSettings['grid'];
|
||||
@@ -16,7 +18,8 @@ export interface WidgetFactoryProps {
|
||||
const WidgetMap: { [key in WidgetTypeValues]: WidgetExport } = {
|
||||
'clock': ClockWidget,
|
||||
'search': SearchWidget,
|
||||
'globalsettings': GlobalSettingsWidget
|
||||
'globalsettings': GlobalSettingsWidget,
|
||||
'quote': QuoteWidget
|
||||
}
|
||||
|
||||
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 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 (
|
||||
<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>
|
||||
</>)
|
||||
}
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
import React, { MouseEvent } from "react"
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
type ButtonProps = {
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>
|
||||
@@ -6,6 +7,6 @@ type ButtonProps = {
|
||||
|
||||
export function CloseButton({ onClick }: ButtonProps) {
|
||||
return (
|
||||
<button className="close-button" onClick={onClick}>×</button>
|
||||
<button className="close-button" onClick={onClick}><CloseIcon sx={{ fontSize: 16 }} /></button>
|
||||
)
|
||||
}
|
||||
-1
@@ -7,7 +7,6 @@ interface ModalOverlayProps {
|
||||
}
|
||||
|
||||
export function ModalOverlay({visible, setVisible, children}: ModalOverlayProps) {
|
||||
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
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
-1
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ClockWidgetData, WidgetExport } from '../../types';
|
||||
import { ClockWidgetData, WidgetExport } from '../../../types';
|
||||
|
||||
interface ClockWidgetProps {
|
||||
widget: ClockWidgetData;
|
||||
+6
-30
@@ -1,16 +1,17 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { ModalOverlay } from "../components/ModalOverlay";
|
||||
import { ModalOverlay } from "../../reusable-components/ModalOverlay";
|
||||
import { useState } from "react";
|
||||
import { globalSettingsOpenState } from "../../state/globalSettingsOpen.state";
|
||||
import { Box, Tab, Tabs, Typography } from "@mui/material";
|
||||
import { globalSettingsOpenState } from "../../../state/globalSettingsOpen.state";
|
||||
import { Box, Tab, Tabs } from "@mui/material";
|
||||
import { UtabSettings } from "./tab-contents/UtabSettings";
|
||||
import { BackgroundSettings } from "./tab-contents/BackgroundSettings";
|
||||
import { SearchEngineSettings } from "./tab-contents/SearchEngineSettings";
|
||||
import { TabPanel } from "../../reusable-components/TabPanel";
|
||||
|
||||
const settingsComponents = {
|
||||
'uTab': UtabSettings,
|
||||
'Background': BackgroundSettings,
|
||||
'Search': SearchEngineSettings
|
||||
'Search': SearchEngineSettings,
|
||||
}
|
||||
|
||||
export function GlobalSettingsPanel() {
|
||||
@@ -29,7 +30,7 @@ export function GlobalSettingsPanel() {
|
||||
variant="scrollable"
|
||||
value={tab}
|
||||
onChange={handleChange}
|
||||
aria-label="Vertical tabs example"
|
||||
aria-label="Vertical tabs"
|
||||
sx={{ borderRight: 1, borderColor: 'divider', textAlign: 'end' }}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
+3
-4
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { CogwheelIcon } from '../components/CogwheelIcon';
|
||||
import { CogwheelIcon } from '../../reusable-components/CogwheelIcon';
|
||||
import { useAtom } from 'jotai';
|
||||
import { WidgetData, WidgetExport } from '../../types';
|
||||
import { globalSettingsOpenState } from '../../state/globalSettingsOpen.state';
|
||||
import { WidgetData, WidgetExport } from '../../../types';
|
||||
import { globalSettingsOpenState } from '../../../state/globalSettingsOpen.state';
|
||||
|
||||
interface GlobalSettingsProps {
|
||||
addWidget: (widget: WidgetData) => void,
|
||||
@@ -17,6 +17,5 @@ const GlobalSettingsComponent: React.FC<GlobalSettingsProps> = ({ addWidget }) =
|
||||
};
|
||||
|
||||
export const GlobalSettingsWidget: WidgetExport = {
|
||||
settings: () => <></>,
|
||||
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>
|
||||
)
|
||||
|
||||
}
|
||||
+2
-5
@@ -1,6 +1,6 @@
|
||||
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 { 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';
|
||||
@@ -19,8 +19,6 @@ export function SearchEngineSettings() {
|
||||
[aliasInput]: searchStringInput
|
||||
}
|
||||
|
||||
console.log(newEngines)
|
||||
|
||||
setAvailableSearchEngines(newEngines)
|
||||
setAliasInput('')
|
||||
setSearchStringInput('')
|
||||
@@ -38,7 +36,7 @@ export function SearchEngineSettings() {
|
||||
return (
|
||||
<Box>
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell align="left" style={{ fontWeight: 'bold' }}>Alias</TableCell>
|
||||
@@ -78,6 +76,5 @@ export function SearchEngineSettings() {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</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
|
||||
}
|
||||
+4
-5
@@ -1,8 +1,7 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { GlobalSettings, SearchWidgetData, WidgetExport } from '../../types';
|
||||
import { availableEnginesState } from '../../state/searchEngines.state';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import { TextField } from '@mui/material';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { SearchWidgetData, WidgetExport } from '../../../types';
|
||||
import { availableEnginesState } from '../../../state/searchEngines.state';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
interface SearchWidgetProps {
|
||||
widget: SearchWidgetData;
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FrontendBindings } from '../../shared/RPC'
|
||||
|
||||
export const IpcService = FrontendBindings
|
||||
|
||||
|
||||
@@ -4,41 +4,63 @@ export const intitialWidgetData = (type: WidgetTypeValues): WidgetTypeMap[typeof
|
||||
switch (type) {
|
||||
case 'clock':
|
||||
return {
|
||||
type,
|
||||
id: `${type}-${Date.now()}`,
|
||||
showIn: 'both',
|
||||
|
||||
height: 6,
|
||||
width: 12,
|
||||
y: 30,
|
||||
x: 12,
|
||||
x: 30,
|
||||
y: 3,
|
||||
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
showSeconds: false,
|
||||
use24Hour: true,
|
||||
type: 'clock',
|
||||
background: true,
|
||||
showIn: 'both',
|
||||
id: `${type}-${Date.now()}`,
|
||||
}
|
||||
case 'search':
|
||||
return {
|
||||
type,
|
||||
id: `${type}-${Date.now()}`,
|
||||
showIn: 'both',
|
||||
|
||||
height: 2,
|
||||
width: 20,
|
||||
y: 26,
|
||||
x: 12,
|
||||
id: `${type}-${Date.now()}`,
|
||||
type: 'search',
|
||||
engines: ['Google'],
|
||||
x: 26,
|
||||
y: 17,
|
||||
background: true,
|
||||
showIn: 'both',
|
||||
|
||||
|
||||
engines: ['Google'],
|
||||
}
|
||||
case 'globalsettings':
|
||||
return {
|
||||
type,
|
||||
id: `${type}-${Date.now()}`,
|
||||
showIn: 'both',
|
||||
|
||||
background: false,
|
||||
height: 2,
|
||||
width: 2,
|
||||
y: 2,
|
||||
x: 2,
|
||||
id: 'globalsettings',
|
||||
type: 'globalsettings',
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +75,8 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
|
||||
widgets: [
|
||||
intitialWidgetData('search'),
|
||||
intitialWidgetData('clock'),
|
||||
intitialWidgetData('globalsettings')
|
||||
intitialWidgetData('globalsettings'),
|
||||
intitialWidgetData('quote')
|
||||
],
|
||||
editEnabled: true,
|
||||
searchEngines: {
|
||||
|
||||
+14
-7
@@ -21,25 +21,32 @@ export type ClockWidgetData = WidgetData<'clock'> & {
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export type QuoteWidgetData = WidgetData<'quote'> & {
|
||||
showAuthor: boolean;
|
||||
refreshInterval: number;
|
||||
lastRefresh: number;
|
||||
}
|
||||
|
||||
export type GlobalSettingsData = WidgetData<'globalsettings'> & {
|
||||
permanent: true;
|
||||
showIn: 'both'
|
||||
}
|
||||
|
||||
export type WidgetTypeValues = (
|
||||
SearchWidgetData |
|
||||
ClockWidgetData |
|
||||
GlobalSettingsData
|
||||
| SearchWidgetData
|
||||
| ClockWidgetData
|
||||
| GlobalSettingsData
|
||||
| QuoteWidgetData
|
||||
)['type']
|
||||
|
||||
export type WidgetTypeMap<WidgetType extends WidgetTypeValues = WidgetTypeValues> = {
|
||||
[Key in WidgetType]: WidgetData<Key>
|
||||
} & {
|
||||
export type WidgetTypeMap = {
|
||||
'clock': ClockWidgetData,
|
||||
'search': SearchWidgetData,
|
||||
'globalsettings': GlobalSettingsData,
|
||||
'quote': QuoteWidgetData
|
||||
}
|
||||
|
||||
|
||||
export interface GlobalSettings {
|
||||
grid: {
|
||||
show: boolean;
|
||||
@@ -54,5 +61,5 @@ export interface GlobalSettings {
|
||||
|
||||
export type WidgetExport<W = React.FC<any>, S = (...a:any[]) =>JSX.Element > = {
|
||||
widget: W,
|
||||
settings: S
|
||||
settings?: S
|
||||
}
|
||||
+16
-10
@@ -1,31 +1,37 @@
|
||||
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>
|
||||
|
||||
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) {
|
||||
const wrapIpc = <ArgT extends any[], RetT, NameT extends string>(
|
||||
fn: NamedFunction<ArgT, RetT, NameT>
|
||||
): NamedFunction<ArgT, Promised<RetT>, NameT> => {
|
||||
const func = function (...args: ArgT): Promise<RetT> {
|
||||
return new Promise((res, rej) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
endpoint: fn.name,
|
||||
args: message
|
||||
args,
|
||||
},
|
||||
(response) => { res(response) }
|
||||
(response) => { res(response); }
|
||||
)
|
||||
})
|
||||
}
|
||||
Object.defineProperty(fn, "name", { value: fn.name });
|
||||
return func as NamedFunction<A, R extends Promise<any> ? R : Promise<R>, N>
|
||||
}
|
||||
Object.defineProperty(func, "name", { value: fn.name });
|
||||
return func as NamedFunction<ArgT, Promised<RetT>, NameT>;
|
||||
};
|
||||
|
||||
export type RPCs = typeof ExposedEndpoints
|
||||
|
||||
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)
|
||||
.map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn) }))
|
||||
.map(([endpoint, fn]) => ({ [endpoint]: wrapIpc(fn as any) }))
|
||||
.reduce((prev, curr) => Object.assign(prev, curr), {}) as FrontendIpcService
|
||||
@@ -1,6 +1,9 @@
|
||||
export const ExposedEndpoints = {
|
||||
writeFile: (message: { path: string, fileContent: string }) => {
|
||||
console.log("writing", message.fileContent, "to", message.path)
|
||||
writeFile: (filename: string, fileContent: string) => {
|
||||
return "OK"
|
||||
},
|
||||
|
||||
writeFileAsync: async (path: string, fileContent: string) => {
|
||||
return "async OK"
|
||||
}
|
||||
} as const;
|
||||
+7
-1
@@ -1,7 +1,13 @@
|
||||
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)
|
||||
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){
|
||||
sendReponse(returnValue)
|
||||
return true;
|
||||
|
||||
+6
-2
@@ -26,6 +26,11 @@ body {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.globalsettings {
|
||||
overflow: visible !important;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.aero {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
@@ -138,8 +143,7 @@ body {
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
min-width: 400px;
|
||||
max-width: 60vw;
|
||||
width: 800px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,12 @@
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.clock {
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Styles for ClockWidget time display */
|
||||
.clock>.time {
|
||||
color: #ffffff;
|
||||
@@ -21,6 +27,21 @@
|
||||
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 */
|
||||
.search form {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user