add quote widget
This commit is contained in:
+11
-4
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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 />;
|
||||||
};
|
};
|
||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>)
|
|
||||||
}
|
|
||||||
+2
-1
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
-1
@@ -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
-1
@@ -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;
|
||||||
+6
-30
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+3
-4
@@ -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>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
+2
-5
@@ -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
|
||||||
|
}
|
||||||
+4
-5
@@ -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,4 +1,3 @@
|
|||||||
import { FrontendBindings } from '../../shared/RPC'
|
import { FrontendBindings } from '../../shared/RPC'
|
||||||
|
|
||||||
export const IpcService = FrontendBindings
|
export const IpcService = FrontendBindings
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,7 +75,8 @@ export const DEFAULT_SETTINGS: GlobalSettings = {
|
|||||||
widgets: [
|
widgets: [
|
||||||
intitialWidgetData('search'),
|
intitialWidgetData('search'),
|
||||||
intitialWidgetData('clock'),
|
intitialWidgetData('clock'),
|
||||||
intitialWidgetData('globalsettings')
|
intitialWidgetData('globalsettings'),
|
||||||
|
intitialWidgetData('quote')
|
||||||
],
|
],
|
||||||
editEnabled: true,
|
editEnabled: true,
|
||||||
searchEngines: {
|
searchEngines: {
|
||||||
|
|||||||
+14
-7
@@ -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
|
||||||
}
|
}
|
||||||
+16
-10
@@ -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
|
||||||
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user