mirror of
https://github.com/responsively-org/responsively-app
synced 2024-11-10 14:54:12 +00:00
Merge origin/main
This commit is contained in:
commit
c1e29ed8cd
10 changed files with 251 additions and 112 deletions
|
@ -13,3 +13,13 @@ export const PREVIEW_LAYOUTS = {
|
|||
|
||||
export type PreviewLayout =
|
||||
typeof PREVIEW_LAYOUTS[keyof typeof PREVIEW_LAYOUTS];
|
||||
|
||||
export const IPC_MAIN_CHANNELS = {
|
||||
APP_META: 'app-meta',
|
||||
PERMISSION_REQUEST: 'permission-request',
|
||||
PERMISSION_RESPONSE: 'permission-response',
|
||||
AUTH_REQUEST: 'auth-request',
|
||||
AUTH_RESPONSE: 'auth-response',
|
||||
} as const;
|
||||
|
||||
export type Channels = typeof IPC_MAIN_CHANNELS[keyof typeof IPC_MAIN_CHANNELS];
|
||||
|
|
44
desktop-app/src/main/http-basic-auth/index.ts
Normal file
44
desktop-app/src/main/http-basic-auth/index.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { AuthInfo, app, BrowserWindow, ipcMain } from 'electron';
|
||||
import { IPC_MAIN_CHANNELS } from '../../common/constants';
|
||||
|
||||
export type AuthRequestArgs = AuthInfo;
|
||||
|
||||
export interface AuthResponseArgs {
|
||||
username: string;
|
||||
password: string;
|
||||
authInfo: AuthInfo;
|
||||
}
|
||||
|
||||
type Callback = (username: string, password: string) => void;
|
||||
|
||||
const inProgressAuthentications: { [key: string]: Callback[] } = {};
|
||||
|
||||
const handleLogin = async (
|
||||
authInfo: AuthInfo,
|
||||
mainWindow: BrowserWindow,
|
||||
callback: (username: string, password: string) => void
|
||||
) => {
|
||||
if (inProgressAuthentications[authInfo.host]) {
|
||||
inProgressAuthentications[authInfo.host].push(callback);
|
||||
return;
|
||||
}
|
||||
inProgressAuthentications[authInfo.host] = [callback];
|
||||
|
||||
mainWindow.webContents.send(IPC_MAIN_CHANNELS.AUTH_REQUEST, authInfo);
|
||||
ipcMain.once(
|
||||
IPC_MAIN_CHANNELS.AUTH_RESPONSE,
|
||||
(_, { authInfo: respAuthInfo, username, password }: AuthResponseArgs) => {
|
||||
inProgressAuthentications[respAuthInfo.host].forEach((cb) =>
|
||||
cb(username, password)
|
||||
);
|
||||
delete inProgressAuthentications[respAuthInfo.host];
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const initHttpBasicAuthHandlers = (mainWindow: BrowserWindow) => {
|
||||
app.on('login', (event, _webContents, _request, authInfo, callback) => {
|
||||
event.preventDefault();
|
||||
handleLogin(authInfo, mainWindow, callback);
|
||||
});
|
||||
};
|
|
@ -12,6 +12,7 @@ import path from 'path';
|
|||
import { app, BrowserWindow, shell, ipcMain, screen } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import log from 'electron-log';
|
||||
import { IPC_MAIN_CHANNELS } from 'common/constants';
|
||||
import MenuBuilder from './menu';
|
||||
import { resolveHtmlPath } from './util';
|
||||
import { BROWSER_SYNC_HOST, initInstance } from './browser-sync';
|
||||
|
@ -22,6 +23,7 @@ import { initDevtoolsHandlers } from './devtools';
|
|||
import { initWebviewStorageManagerHandlers } from './webview-storage-manager';
|
||||
import { initNativeFunctionHandlers } from './native-functions';
|
||||
import { WebPermissionHandlers } from './web-permissions';
|
||||
import { initHttpBasicAuthHandlers } from './http-basic-auth';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
|
@ -33,13 +35,7 @@ export default class AppUpdater {
|
|||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
ipcMain.on('ipc-example', async (event, arg) => {
|
||||
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
|
||||
console.log(msgTemplate(arg));
|
||||
event.reply('ipc-example', msgTemplate('pong'));
|
||||
});
|
||||
|
||||
ipcMain.handle('app-meta', async () => {
|
||||
ipcMain.handle(IPC_MAIN_CHANNELS.APP_META, async () => {
|
||||
return {
|
||||
webviewPreloadPath: app.isPackaged
|
||||
? path.join(__dirname, 'preload-webview.js')
|
||||
|
@ -112,6 +108,7 @@ const createWindow = async () => {
|
|||
},
|
||||
});
|
||||
initDevtoolsHandlers(mainWindow);
|
||||
initHttpBasicAuthHandlers(mainWindow);
|
||||
const webPermissionHandlers = WebPermissionHandlers(mainWindow);
|
||||
|
||||
mainWindow.loadURL(resolveHtmlPath('index.html'));
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export type Channels = 'ipc-example' | 'app-meta';
|
||||
|
||||
console.log('Preload main');
|
||||
|
||||
const documentBodyInit = () => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Channels } from 'common/constants';
|
||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
|
||||
export type Channels = 'ipc-example' | 'app-meta';
|
||||
|
||||
contextBridge.exposeInMainWorld('electron', {
|
||||
ipcRenderer: {
|
||||
sendMessage<T>(channel: Channels, args: T[]) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import { IPC_MAIN_CHANNELS } from '../../common/constants';
|
||||
import store from '../../store';
|
||||
|
||||
export interface PermissionRequestArg {
|
||||
|
@ -102,7 +103,7 @@ class PermissionsManager {
|
|||
this.callbacks[key] = [];
|
||||
}
|
||||
};
|
||||
const PERMISSION_RESPONSE_CHANNEL = 'permission-response';
|
||||
const PERMISSION_RESPONSE_CHANNEL = IPC_MAIN_CHANNELS.PERMISSION_RESPONSE;
|
||||
if (ipcMain.listeners(PERMISSION_RESPONSE_CHANNEL).length === 0) {
|
||||
ipcMain.handle(PERMISSION_RESPONSE_CHANNEL, handler);
|
||||
}
|
||||
|
@ -153,7 +154,7 @@ class PermissionsManager {
|
|||
if (previousState === PERMISSION_STATE.PROMPT) {
|
||||
return;
|
||||
}
|
||||
this.mainWindow.webContents.send('permission-request', {
|
||||
this.mainWindow.webContents.send(IPC_MAIN_CHANNELS.PERMISSION_REQUEST, {
|
||||
permission: type,
|
||||
requestingOrigin: origin,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import { IPC_MAIN_CHANNELS } from 'common/constants';
|
||||
import { AuthInfo } from 'electron';
|
||||
import { AuthResponseArgs } from 'main/http-basic-auth';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Button from 'renderer/components/Button';
|
||||
import Input from 'renderer/components/Input';
|
||||
import Modal from 'renderer/components/Modal';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
authInfo: AuthInfo | null;
|
||||
}
|
||||
|
||||
const AuthModal = ({ isOpen, onClose, authInfo }: Props) => {
|
||||
const [username, setUsername] = useState<string>('');
|
||||
const [password, setPassword] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setUsername('');
|
||||
setPassword('');
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const onSubmit = (proceed: boolean) => {
|
||||
if (authInfo == null) {
|
||||
return;
|
||||
}
|
||||
window.electron.ipcRenderer.sendMessage<AuthResponseArgs>(
|
||||
IPC_MAIN_CHANNELS.AUTH_RESPONSE,
|
||||
{
|
||||
authInfo,
|
||||
username: proceed ? username : '',
|
||||
password: proceed ? password : '',
|
||||
}
|
||||
);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Http Authentication">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p>
|
||||
Authentication request for{' '}
|
||||
<span className="font-bold">{authInfo?.host}</span>
|
||||
</p>
|
||||
<div className="flex w-[420px] flex-col gap-2">
|
||||
<Input
|
||||
label="Username"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row justify-end gap-2">
|
||||
<Button className="px-2" onClick={() => onSubmit(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="px-2" onClick={() => onSubmit(true)} isActive>
|
||||
Proceed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthModal;
|
|
@ -1,11 +1,14 @@
|
|||
import { Icon } from '@iconify/react';
|
||||
import cx from 'classnames';
|
||||
import { IPC_MAIN_CHANNELS } from 'common/constants';
|
||||
import { AuthRequestArgs } from 'main/http-basic-auth';
|
||||
import { PermissionRequestArg } from 'main/web-permissions/PermissionsManager';
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Button from 'renderer/components/Button';
|
||||
import { webViewPubSub } from 'renderer/lib/pubsub';
|
||||
import { selectAddress, setAddress } from 'renderer/store/features/renderer';
|
||||
import AuthModal from './AuthModal';
|
||||
import SuggestionList from './SuggestionList';
|
||||
|
||||
export const ADDRESS_BAR_EVENTS = {
|
||||
|
@ -28,6 +31,7 @@ const AddressBar = () => {
|
|||
const [deleteCacheLoading, setDeleteCacheLoading] = useState<boolean>(false);
|
||||
const [permissionRequest, setPermissionRequest] =
|
||||
useState<PermissionRequestArg | null>(null);
|
||||
const [authRequest, setAuthRequest] = useState<AuthRequestArgs | null>(null);
|
||||
const address = useSelector(selectAddress);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -41,13 +45,22 @@ const AddressBar = () => {
|
|||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on<PermissionRequestArg>(
|
||||
'permission-request',
|
||||
IPC_MAIN_CHANNELS.PERMISSION_REQUEST,
|
||||
(args) => {
|
||||
setPermissionRequest(args);
|
||||
}
|
||||
);
|
||||
|
||||
window.electron.ipcRenderer.on<AuthRequestArgs>(
|
||||
IPC_MAIN_CHANNELS.AUTH_REQUEST,
|
||||
(args) => {
|
||||
setAuthRequest(args);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners('permission-request');
|
||||
window.electron.ipcRenderer.removeAllListeners(
|
||||
IPC_MAIN_CHANNELS.PERMISSION_REQUEST
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -61,7 +74,7 @@ const AddressBar = () => {
|
|||
if (!permissionRequest) {
|
||||
return;
|
||||
}
|
||||
window.electron.ipcRenderer.invoke('permission-response', {
|
||||
window.electron.ipcRenderer.invoke(IPC_MAIN_CHANNELS.PERMISSION_RESPONSE, {
|
||||
permissionRequest,
|
||||
allow,
|
||||
});
|
||||
|
@ -116,99 +129,106 @@ const AddressBar = () => {
|
|||
const isHomepage = address === homepage;
|
||||
|
||||
return (
|
||||
<div className="relative z-10 w-full flex-grow">
|
||||
<div className="absolute top-2 left-2 mr-2 flex flex-col items-start">
|
||||
<Icon icon="mdi:web" className="text-gray-500" />
|
||||
{permissionRequest != null ? (
|
||||
<div className="z-40 mt-4 flex w-96 flex-col gap-8 rounded bg-white p-6 shadow-lg ring-1 ring-slate-500 !ring-opacity-40 focus:outline-none dark:bg-slate-900 dark:ring-white dark:!ring-opacity-40">
|
||||
<span>
|
||||
{permissionRequest.requestingOrigin} requests permission for:{' '}
|
||||
<br />
|
||||
<span className="flex justify-center font-bold capitalize">
|
||||
{permissionRequest.permission}
|
||||
<>
|
||||
<div className="relative z-10 w-full flex-grow">
|
||||
<div className="absolute top-2 left-2 mr-2 flex flex-col items-start">
|
||||
<Icon icon="mdi:web" className="text-gray-500" />
|
||||
{permissionRequest != null ? (
|
||||
<div className="z-40 mt-4 flex w-96 flex-col gap-8 rounded bg-white p-6 shadow-lg ring-1 ring-slate-500 !ring-opacity-40 focus:outline-none dark:bg-slate-900 dark:ring-white dark:!ring-opacity-40">
|
||||
<span>
|
||||
{permissionRequest.requestingOrigin} requests permission for:{' '}
|
||||
<br />
|
||||
<span className="flex justify-center font-bold capitalize">
|
||||
{permissionRequest.permission}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex w-1/2 justify-around">
|
||||
<Button
|
||||
onClick={() => {
|
||||
permissionReqClickHandler(false);
|
||||
}}
|
||||
isActionButton
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
permissionReqClickHandler(true);
|
||||
}}
|
||||
isActionButton
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
<div className="flex justify-end">
|
||||
<div className="flex w-1/2 justify-around">
|
||||
<Button
|
||||
onClick={() => {
|
||||
permissionReqClickHandler(false);
|
||||
}}
|
||||
isActionButton
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
permissionReqClickHandler(true);
|
||||
}}
|
||||
isActionButton
|
||||
>
|
||||
Allow
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className={cx(
|
||||
'w-full text-ellipsis rounded-full px-2 py-1 pl-8 pr-20 dark:bg-slate-900',
|
||||
{
|
||||
'rounded-tl-lg rounded-tr-lg rounded-bl-none rounded-br-none outline-none':
|
||||
isSuggesting,
|
||||
}
|
||||
)}
|
||||
value={typedAddress}
|
||||
onChange={(e) => setTypedAddress(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
setIsSuggesting(false);
|
||||
}, 100);
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 mr-2 flex items-center">
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteStorage}
|
||||
isLoading={deleteStorageLoading}
|
||||
title="Delete Storage"
|
||||
>
|
||||
<Icon icon="mdi:database-remove-outline" />
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteCookies}
|
||||
isLoading={deleteCookiesLoading}
|
||||
title="Delete Cookies"
|
||||
>
|
||||
<Icon icon="mdi:cookie-remove-outline" />
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteCache}
|
||||
isLoading={deleteCacheLoading}
|
||||
title="Clear Cache"
|
||||
>
|
||||
<Icon icon="mdi:wifi-remove" />
|
||||
</Button>
|
||||
<Button
|
||||
className={cx('rounded-full', {
|
||||
'text-blue-500': isHomepage,
|
||||
})}
|
||||
onClick={() => setHomepage(address)}
|
||||
title="Homepage"
|
||||
>
|
||||
<Icon icon={isHomepage ? 'mdi:home' : 'mdi:home-outline'} />
|
||||
</Button>
|
||||
</div>
|
||||
{isSuggesting ? (
|
||||
<SuggestionList match={typedAddress} onEnter={onEnter} />
|
||||
) : null}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className={cx(
|
||||
'w-full text-ellipsis rounded-full px-2 py-1 pl-8 pr-20 dark:bg-slate-900',
|
||||
{
|
||||
'rounded-tl-lg rounded-tr-lg rounded-bl-none rounded-br-none outline-none':
|
||||
isSuggesting,
|
||||
}
|
||||
)}
|
||||
value={typedAddress}
|
||||
onChange={(e) => setTypedAddress(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={() => {
|
||||
setTimeout(() => {
|
||||
setIsSuggesting(false);
|
||||
}, 100);
|
||||
}}
|
||||
<AuthModal
|
||||
isOpen={authRequest != null}
|
||||
onClose={() => setAuthRequest(null)}
|
||||
authInfo={authRequest}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 mr-2 flex items-center">
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteStorage}
|
||||
isLoading={deleteStorageLoading}
|
||||
title="Delete Storage"
|
||||
>
|
||||
<Icon icon="mdi:database-remove-outline" />
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteCookies}
|
||||
isLoading={deleteCookiesLoading}
|
||||
title="Delete Cookies"
|
||||
>
|
||||
<Icon icon="mdi:cookie-remove-outline" />
|
||||
</Button>
|
||||
<Button
|
||||
className="rounded-full"
|
||||
onClick={deleteCache}
|
||||
isLoading={deleteCacheLoading}
|
||||
title="Clear Cache"
|
||||
>
|
||||
<Icon icon="mdi:wifi-remove" />
|
||||
</Button>
|
||||
<Button
|
||||
className={cx('rounded-full', {
|
||||
'text-blue-500': isHomepage,
|
||||
})}
|
||||
onClick={() => setHomepage(address)}
|
||||
title="Homepage"
|
||||
>
|
||||
<Icon icon={isHomepage ? 'mdi:home' : 'mdi:home-outline'} />
|
||||
</Button>
|
||||
</div>
|
||||
{isSuggesting ? (
|
||||
<SuggestionList match={typedAddress} onEnter={onEnter} />
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +1,12 @@
|
|||
import { IPC_MAIN_CHANNELS } from 'common/constants';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './AppContent';
|
||||
|
||||
const container = document.getElementById('root')!;
|
||||
const root = createRoot(container);
|
||||
|
||||
// calling IPC exposed from preload script
|
||||
window.electron.ipcRenderer.once('ipc-example', (arg) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(arg);
|
||||
});
|
||||
window.electron.ipcRenderer.sendMessage('ipc-example', ['ping']);
|
||||
window.electron.ipcRenderer
|
||||
.invoke('app-meta', [])
|
||||
.invoke(IPC_MAIN_CHANNELS.APP_META, [])
|
||||
.then((arg: any) => {
|
||||
window.responsively = { webviewPreloadPath: arg.webviewPreloadPath };
|
||||
return root.render(<App />);
|
||||
|
|
4
desktop-app/src/renderer/preload.d.ts
vendored
4
desktop-app/src/renderer/preload.d.ts
vendored
|
@ -1,10 +1,10 @@
|
|||
import { Channels } from 'main/preload';
|
||||
import type { Channels } from 'common/constants';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
ipcRenderer: {
|
||||
sendMessage<T>(channel: Channels, args: T[]): void;
|
||||
sendMessage<T>(channel: Channels, ...args: T[]): void;
|
||||
on<T>(
|
||||
channel: string,
|
||||
func: (...args: T[]) => void
|
||||
|
|
Loading…
Reference in a new issue