feat(vpn-app-desktop): add main screen (#4206)
This commit is contained in:
@@ -45,5 +45,6 @@ module.exports = {
|
||||
groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
|
||||
},
|
||||
],
|
||||
'import/extensions': ['error', 'never', { json: 'always', svg: 'always' }],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="text-[12px]">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
@@ -8,7 +8,7 @@
|
||||
</head>
|
||||
|
||||
<body class="h-screen">
|
||||
<div id="root" class="w-[390px] h-[820px] font-sans"></div>
|
||||
<div id="root" class="h-full font-sans"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@headlessui/tailwindcss": "^0.2.0",
|
||||
"@mui/base": "^5.0.0-beta.24",
|
||||
"@tauri-apps/api": "^1.5.0",
|
||||
"clsx": "^2.0.0",
|
||||
|
||||
@@ -3,11 +3,14 @@ use std::time::Duration;
|
||||
use tauri::{Manager, State};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::time::sleep;
|
||||
use tracing::{debug, instrument, trace};
|
||||
use tracing::{debug, error, instrument, trace};
|
||||
|
||||
use crate::{
|
||||
error::{CmdError, CmdErrorSource},
|
||||
states::{app::ConnectionState, SharedAppState},
|
||||
states::{
|
||||
app::{ConnectionState, VpnMode},
|
||||
SharedAppData, SharedAppState,
|
||||
},
|
||||
};
|
||||
|
||||
const EVENT_CONNECTION_STATE: &str = "connection-state";
|
||||
@@ -178,3 +181,37 @@ pub async fn get_connection_start_time(
|
||||
let app_state = state.lock().await;
|
||||
Ok(app_state.connection_start_time.map(|t| t.unix_timestamp()))
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
#[tauri::command]
|
||||
pub async fn set_vpn_mode(
|
||||
app_state: State<'_, SharedAppState>,
|
||||
data_state: State<'_, SharedAppData>,
|
||||
mode: VpnMode,
|
||||
) -> Result<(), CmdError> {
|
||||
debug!("set_vpn_mode");
|
||||
|
||||
let mut state = app_state.lock().await;
|
||||
|
||||
if let ConnectionState::Disconnected = state.state {
|
||||
} else {
|
||||
let err_message = format!("cannot change vpn mode from state {:?}", state.state);
|
||||
error!(err_message);
|
||||
return Err(CmdError::new(CmdErrorSource::CallerError, err_message));
|
||||
}
|
||||
state.vpn_mode = mode.clone();
|
||||
|
||||
// save the selected mode to disk
|
||||
let mut app_data_store = data_state.lock().await;
|
||||
let mut app_data = app_data_store
|
||||
.read()
|
||||
.await
|
||||
.map_err(|e| CmdError::new(CmdErrorSource::InternalError, e.to_string()))?;
|
||||
app_data.vpn_mode = Some(mode);
|
||||
app_data_store.data = app_data;
|
||||
app_data_store
|
||||
.write()
|
||||
.await
|
||||
.map_err(|e| CmdError::new(CmdErrorSource::InternalError, e.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -25,7 +25,10 @@ pub struct CmdError {
|
||||
|
||||
impl CmdError {
|
||||
pub fn new(error: CmdErrorSource, message: String) -> Self {
|
||||
Self { message, source: error }
|
||||
Self {
|
||||
message,
|
||||
source: error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::states::app::{NodeConfig, PrivacyMode};
|
||||
use crate::states::app::{NodeConfig, VpnMode};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug, Clone, TS)]
|
||||
#[ts(export)]
|
||||
pub enum UiMode {
|
||||
pub enum UiTheme {
|
||||
Dark,
|
||||
#[default]
|
||||
Light,
|
||||
@@ -17,8 +17,8 @@ pub struct AppData {
|
||||
pub monitoring: Option<bool>,
|
||||
pub autoconnect: Option<bool>,
|
||||
pub killswitch: Option<bool>,
|
||||
pub ui_mode: Option<UiMode>,
|
||||
pub privacy_mode: Option<PrivacyMode>,
|
||||
pub ui_theme: Option<UiTheme>,
|
||||
pub vpn_mode: Option<VpnMode>,
|
||||
pub entry_node: Option<NodeConfig>,
|
||||
pub exit_node: Option<NodeConfig>,
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
connection::set_vpn_mode,
|
||||
connection::get_connection_state,
|
||||
connection::connect,
|
||||
connection::disconnect,
|
||||
|
||||
@@ -22,11 +22,10 @@ pub enum ConnectionState {
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, TS, Clone)]
|
||||
#[ts(export)]
|
||||
pub enum PrivacyMode {
|
||||
High,
|
||||
Medium,
|
||||
pub enum VpnMode {
|
||||
Mixnet,
|
||||
#[default]
|
||||
Low,
|
||||
TwoHop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, TS)]
|
||||
@@ -40,7 +39,7 @@ pub struct TunnelConfig {
|
||||
pub struct AppState {
|
||||
pub state: ConnectionState,
|
||||
pub error: Option<String>,
|
||||
pub privacy_mode: PrivacyMode,
|
||||
pub vpn_mode: VpnMode,
|
||||
pub entry_node: Option<NodeConfig>,
|
||||
pub exit_node: Option<NodeConfig>,
|
||||
pub tunnel: Option<TunnelConfig>,
|
||||
|
||||
@@ -44,10 +44,10 @@
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"resizable": false,
|
||||
"resizable": true,
|
||||
"title": "NymVPN",
|
||||
"width": 390,
|
||||
"height": 820
|
||||
"width": 440,
|
||||
"height": 920
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@ export function mockTauriIPC() {
|
||||
monitoring: false,
|
||||
autoconnect: false,
|
||||
killswitch: false,
|
||||
ui_mode: 'Dark',
|
||||
privacy_mode: 'High',
|
||||
ui_theme: 'Dark',
|
||||
vpn_mode: 'TwoHop',
|
||||
entry_node: null,
|
||||
exit_node: null,
|
||||
}),
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
},
|
||||
"connecting-message": "Establishing connection",
|
||||
"connection-time": "Connection time",
|
||||
"select-network-title": "Select network",
|
||||
"select-network-label": "Select network",
|
||||
"select-node-title": "Connect to",
|
||||
"mixnet-mode": {
|
||||
"title": "5-hop mixnet",
|
||||
"desc": "Best for payments, emails, messages"
|
||||
},
|
||||
"wg-mode": {
|
||||
"twohop-mode": {
|
||||
"title": "2-hop WireGuard",
|
||||
"desc": "Best for browsing, streaming, sharing"
|
||||
},
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import clsx from 'clsx';
|
||||
import { useMainDispatch, useMainState } from '../contexts';
|
||||
import { ConnectionState, StateDispatch } from '../types';
|
||||
import { ConnectionTimer } from '../ui';
|
||||
|
||||
function Home() {
|
||||
const state = useMainState();
|
||||
const dispatch = useMainDispatch() as StateDispatch;
|
||||
|
||||
const { t } = useTranslation('home');
|
||||
|
||||
const handleClick = async () => {
|
||||
if (state.state === 'Connected') {
|
||||
dispatch({ type: 'disconnect' });
|
||||
invoke('disconnect').then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
} else if (state.state === 'Disconnected') {
|
||||
dispatch({ type: 'connect' });
|
||||
invoke('connect').then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const statusBadgeDynStyles = {
|
||||
Connected: [
|
||||
'bg-blanc-nacre-icicle',
|
||||
'text-vert-menthe',
|
||||
'dark:bg-baltic-sea-quartzite',
|
||||
],
|
||||
Disconnected: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-coal-mine-light',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-coal-mine-dark',
|
||||
],
|
||||
Connecting: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-baltic-sea',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-white',
|
||||
],
|
||||
Disconnecting: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-baltic-sea',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-white',
|
||||
],
|
||||
Unknown: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-coal-mine-light',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-coal-mine-dark',
|
||||
],
|
||||
};
|
||||
|
||||
const getStatusText = (state: ConnectionState) => {
|
||||
switch (state) {
|
||||
case 'Connected':
|
||||
return t('status.connected');
|
||||
case 'Disconnected':
|
||||
return t('status.disconnected');
|
||||
case 'Connecting':
|
||||
return t('status.connecting');
|
||||
case 'Disconnecting':
|
||||
return t('status.disconnecting');
|
||||
case 'Unknown':
|
||||
return t('status.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="h-80 flex flex-col justify-center items-center gap-y-2">
|
||||
<div className="flex flex-1 items-end">
|
||||
<div
|
||||
className={clsx([
|
||||
...statusBadgeDynStyles[state.state],
|
||||
'font-bold py-4 px-6 rounded-full text-lg',
|
||||
])}
|
||||
>
|
||||
{getStatusText(state.state)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1">
|
||||
{state.loading && state.progressMessages.length > 0 && (
|
||||
<p className="text-dim-gray dark:text-mercury-mist font-bold">
|
||||
{state.progressMessages[state.progressMessages.length - 1]}
|
||||
</p>
|
||||
)}
|
||||
{state.state === 'Connected' && <ConnectionTimer />}
|
||||
{state.error && (
|
||||
<p className="text-teaberry font-bold">{state.error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center gap-y-2">
|
||||
<button className="justify-self-end" onClick={handleClick}>
|
||||
{state.state === 'Disconnected' ? t('connect') : t('disconnect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -3,7 +3,7 @@ import { TopBar } from '../ui';
|
||||
|
||||
function NavLayout() {
|
||||
return (
|
||||
<div className="h-full bg-blanc-nacre dark:bg-baltic-sea text-baltic-sea dark:text-white">
|
||||
<div className="h-full flex flex-col bg-blanc-nacre dark:bg-baltic-sea text-baltic-sea dark:text-white">
|
||||
<TopBar />
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConnectionState } from '../../types';
|
||||
import { useMainState } from '../../contexts';
|
||||
import ConnectionTimer from './ConnectionTimer';
|
||||
|
||||
function ConnectionStatus() {
|
||||
const state = useMainState();
|
||||
|
||||
const { t } = useTranslation('home');
|
||||
const statusBadgeDynStyles = {
|
||||
Connected: [
|
||||
'bg-blanc-nacre-icicle',
|
||||
'text-vert-menthe',
|
||||
'dark:bg-baltic-sea-quartzite',
|
||||
],
|
||||
Disconnected: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-coal-mine-light',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-coal-mine-dark',
|
||||
],
|
||||
Connecting: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-baltic-sea',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-white',
|
||||
],
|
||||
Disconnecting: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-baltic-sea',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-white',
|
||||
],
|
||||
Unknown: [
|
||||
'bg-blanc-nacre-platinum',
|
||||
'text-coal-mine-light',
|
||||
'dark:bg-baltic-sea-oil',
|
||||
'dark:text-coal-mine-dark',
|
||||
],
|
||||
};
|
||||
|
||||
const getStatusText = (state: ConnectionState) => {
|
||||
switch (state) {
|
||||
case 'Connected':
|
||||
return t('status.connected');
|
||||
case 'Disconnected':
|
||||
return t('status.disconnected');
|
||||
case 'Connecting':
|
||||
return t('status.connecting');
|
||||
case 'Disconnecting':
|
||||
return t('status.disconnecting');
|
||||
case 'Unknown':
|
||||
return t('status.unknown');
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="h-64 flex flex-col justify-center items-center gap-y-2">
|
||||
<div className="flex flex-1 items-end">
|
||||
<div
|
||||
className={clsx([
|
||||
...statusBadgeDynStyles[state.state],
|
||||
'font-bold py-4 px-6 rounded-full text-lg',
|
||||
])}
|
||||
>
|
||||
{getStatusText(state.state)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1">
|
||||
{state.loading && state.progressMessages.length > 0 && (
|
||||
<p className="text-dim-gray dark:text-mercury-mist font-bold">
|
||||
{state.progressMessages[state.progressMessages.length - 1]}
|
||||
</p>
|
||||
)}
|
||||
{state.state === 'Connected' && <ConnectionTimer />}
|
||||
{state.error && (
|
||||
<p className="text-teaberry font-bold">{state.error}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectionStatus;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import { useMainState } from '../contexts';
|
||||
import { useMainState } from '../../contexts';
|
||||
|
||||
function ConnectionTimer() {
|
||||
const { sessionStartDate } = useMainState();
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@mui/base';
|
||||
import { useMainDispatch, useMainState } from '../../contexts';
|
||||
import { ConnectionState, StateDispatch } from '../../types';
|
||||
import NetworkModeSelect from './NetworkModeSelect';
|
||||
import ConnectionStatus from './ConnectionStatus';
|
||||
|
||||
function Home() {
|
||||
const state = useMainState();
|
||||
const dispatch = useMainDispatch() as StateDispatch;
|
||||
|
||||
const { t } = useTranslation('home');
|
||||
|
||||
const handleClick = async () => {
|
||||
if (state.state === 'Connected') {
|
||||
dispatch({ type: 'disconnect' });
|
||||
invoke('disconnect').then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
} else if (state.state === 'Disconnected') {
|
||||
dispatch({ type: 'connect' });
|
||||
invoke('connect').then((result) => {
|
||||
console.log(result);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getButtonText = (state: ConnectionState) => {
|
||||
switch (state) {
|
||||
case 'Connected':
|
||||
return t('disconnect');
|
||||
case 'Disconnected':
|
||||
return t('connect');
|
||||
case 'Connecting':
|
||||
return (
|
||||
<div className="flex justify-center items-center animate-spin">
|
||||
<span className="font-icon text-2xl font-medium">autorenew</span>
|
||||
</div>
|
||||
);
|
||||
case 'Disconnecting':
|
||||
return (
|
||||
<div className="flex justify-center items-center animate-spin">
|
||||
<span className="font-icon text-2xl font-medium">autorenew</span>
|
||||
</div>
|
||||
);
|
||||
case 'Unknown':
|
||||
return t('status.unknown');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col p-4">
|
||||
<ConnectionStatus />
|
||||
<div className="flex grow flex-col justify-between gap-y-2">
|
||||
<div className="flex flex-col justify-between">
|
||||
<NetworkModeSelect />
|
||||
<div></div>
|
||||
</div>
|
||||
<Button
|
||||
className={clsx([
|
||||
'rounded-lg text-lg font-bold py-4 px-6 h-16 focus:outline-none focus:ring-4 focus:ring-black focus:dark:ring-white shadow',
|
||||
(state.state === 'Disconnected' || state.state === 'Connecting') &&
|
||||
'bg-melon text-white dark:text-baltic-sea',
|
||||
(state.state === 'Connected' || state.state === 'Disconnecting') &&
|
||||
'bg-cornflower text-white dark:text-baltic-sea',
|
||||
])}
|
||||
onClick={handleClick}
|
||||
disabled={state.loading}
|
||||
>
|
||||
{getButtonText(state.state)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useState } from 'react';
|
||||
import { RadioGroup } from '@headlessui/react';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMainDispatch, useMainState } from '../../contexts';
|
||||
import { StateDispatch, VpnMode } from '../../types';
|
||||
|
||||
type VpnModeOption = { name: VpnMode; title: string; desc: string };
|
||||
|
||||
function NetworkModeSelect() {
|
||||
const state = useMainState();
|
||||
const dispatch = useMainDispatch() as StateDispatch;
|
||||
const [selected, setSelected] = useState(state.vpnMode);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const { t } = useTranslation('home');
|
||||
|
||||
const handleNetworkModeChange = async (value: VpnMode) => {
|
||||
if (state.state === 'Disconnected' && value !== state.vpnMode) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await invoke<void>('set_vpn_mode', { mode: value });
|
||||
dispatch({ type: 'set-vpn-mode', mode: value });
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const vpnModes: VpnModeOption[] = [
|
||||
{
|
||||
name: 'Mixnet',
|
||||
title: t('mixnet-mode.title'),
|
||||
desc: t('mixnet-mode.desc'),
|
||||
},
|
||||
{
|
||||
name: 'TwoHop',
|
||||
title: t('twohop-mode.title'),
|
||||
desc: t('twohop-mode.desc'),
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (value: VpnMode) => {
|
||||
setSelected(value);
|
||||
handleNetworkModeChange(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<RadioGroup value={selected} onChange={handleSelect}>
|
||||
<RadioGroup.Label
|
||||
as="div"
|
||||
className="font-semibold text-lg text-baltic-sea dark:text-white mb-4"
|
||||
>
|
||||
{t('select-network-label')}
|
||||
</RadioGroup.Label>
|
||||
<div className="space-y-4">
|
||||
{vpnModes.map((mode) => (
|
||||
<RadioGroup.Option
|
||||
key={mode.name}
|
||||
value={mode.name}
|
||||
className={({ checked }) =>
|
||||
clsx([
|
||||
'bg-white dark:bg-baltic-sea-jaguar relative flex rounded-lg px-5 py-3 shadow-md focus:outline-none',
|
||||
(state.state !== 'Disconnected' || loading) &&
|
||||
'cursor-not-allowed',
|
||||
checked &&
|
||||
'ring-0 ring-melon ring-offset-2 ring-offset-melon',
|
||||
state.state === 'Disconnected' && 'cursor-pointer',
|
||||
])
|
||||
}
|
||||
disabled={state.state !== 'Disconnected' || loading}
|
||||
>
|
||||
{({ checked }) => {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-between gap-4">
|
||||
{checked ? (
|
||||
<span className="font-icon text-2xl text-melon">
|
||||
radio_button_checked
|
||||
</span>
|
||||
) : (
|
||||
<span className="font-icon text-2xl text-mercury-pinkish">
|
||||
radio_button_unchecked
|
||||
</span>
|
||||
)}
|
||||
<div className="flex flex-1 items-center">
|
||||
<div className="text-sm">
|
||||
<RadioGroup.Label
|
||||
as="p"
|
||||
className="font-semibold text-lg text-baltic-sea dark:text-mercury-pinkish"
|
||||
>
|
||||
{mode.title}
|
||||
</RadioGroup.Label>
|
||||
<RadioGroup.Description
|
||||
as="span"
|
||||
className="text-base text-ciment-feet dark:text-mercury-mist"
|
||||
>
|
||||
<span>{mode.desc}</span>
|
||||
</RadioGroup.Description>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</RadioGroup.Option>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NetworkModeSelect;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Home } from './Home';
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './home';
|
||||
export { default as NavLayout } from './NavLayout';
|
||||
export { default as Home } from './Home';
|
||||
export { default as Settings } from './Settings';
|
||||
export { default as NodeLocation } from './NodeLocation';
|
||||
export { default as Error } from './Error';
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { AppData, AppState, ConnectionState } from '../types';
|
||||
import { AppData, AppState, ConnectionState, VpnMode } from '../types';
|
||||
|
||||
export type StateAction =
|
||||
| { type: 'set-partial-state'; partialState: Partial<AppState> }
|
||||
| { type: 'change-connection-state'; state: ConnectionState }
|
||||
| { type: 'set-vpn-mode'; mode: VpnMode }
|
||||
| { type: 'set-error'; error: string }
|
||||
| { type: 'reset-error' }
|
||||
| { type: 'new-progress-message'; message: string }
|
||||
@@ -18,16 +19,16 @@ export type StateAction =
|
||||
export const initialState: AppState = {
|
||||
state: 'Disconnected',
|
||||
loading: false,
|
||||
privacyMode: 'High',
|
||||
vpnMode: 'TwoHop',
|
||||
tunnel: { name: 'nym', id: 'nym' },
|
||||
uiMode: 'Light',
|
||||
uiTheme: 'Light',
|
||||
progressMessages: [],
|
||||
localAppData: {
|
||||
monitoring: false,
|
||||
autoconnect: false,
|
||||
killswitch: false,
|
||||
uiMode: 'Light',
|
||||
privacyMode: 'High',
|
||||
uiTheme: 'Light',
|
||||
vpnMode: 'TwoHop',
|
||||
entryNode: null,
|
||||
exitNode: null,
|
||||
},
|
||||
@@ -35,6 +36,12 @@ export const initialState: AppState = {
|
||||
|
||||
export function reducer(state: AppState, action: StateAction): AppState {
|
||||
switch (action.type) {
|
||||
case 'set-vpn-mode':
|
||||
return {
|
||||
...state,
|
||||
vpnMode: action.mode,
|
||||
localAppData: { ...state.localAppData, vpnMode: action.mode },
|
||||
};
|
||||
case 'set-partial-state': {
|
||||
return { ...state, ...action.partialState };
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@ export function MainStateProvider({ children }: Props) {
|
||||
autoconnect: data.autoconnect || false,
|
||||
monitoring: data.monitoring || false,
|
||||
killswitch: data.killswitch || false,
|
||||
uiMode: data.ui_mode || 'Light',
|
||||
privacyMode: data.privacy_mode || 'High',
|
||||
uiTheme: data.ui_theme || 'Light',
|
||||
vpnMode: data.vpn_mode || 'TwoHop',
|
||||
entryNode: data.entry_node,
|
||||
exitNode: data.exit_node,
|
||||
},
|
||||
@@ -57,8 +57,8 @@ export function MainStateProvider({ children }: Props) {
|
||||
dispatch({
|
||||
type: 'set-partial-state',
|
||||
partialState: {
|
||||
uiMode: data.ui_mode || 'Light',
|
||||
privacyMode: data.privacy_mode || 'High',
|
||||
uiTheme: data.ui_theme || 'Light',
|
||||
vpnMode: data.vpn_mode || 'TwoHop',
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PrivacyMode } from './app-state';
|
||||
import { VpnMode } from './app-state';
|
||||
|
||||
export type UiMode = 'Dark' | 'Light';
|
||||
export type UiTheme = 'Dark' | 'Light';
|
||||
|
||||
export interface NodeConfig {
|
||||
id: string;
|
||||
@@ -11,8 +11,8 @@ export interface AppData {
|
||||
monitoring: boolean;
|
||||
autoconnect: boolean;
|
||||
killswitch: boolean;
|
||||
uiMode: UiMode;
|
||||
privacyMode: PrivacyMode;
|
||||
uiTheme: UiTheme;
|
||||
vpnMode: VpnMode;
|
||||
entryNode?: NodeConfig | null;
|
||||
exitNode?: NodeConfig | null;
|
||||
}
|
||||
@@ -22,8 +22,8 @@ export interface AppDataFromBackend {
|
||||
monitoring: boolean | null;
|
||||
autoconnect: boolean | null;
|
||||
killswitch: boolean | null;
|
||||
ui_mode: UiMode | null;
|
||||
privacy_mode: PrivacyMode;
|
||||
ui_theme: UiTheme | null;
|
||||
vpn_mode: VpnMode;
|
||||
entry_node: NodeConfig | null;
|
||||
exit_node: NodeConfig | null;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ConnectionState =
|
||||
| 'Disconnecting'
|
||||
| 'Unknown';
|
||||
|
||||
export type PrivacyMode = 'High' | 'Medium' | 'Low';
|
||||
export type VpnMode = 'TwoHop' | 'Mixnet';
|
||||
|
||||
export interface TunnelConfig {
|
||||
id: string;
|
||||
@@ -23,9 +23,9 @@ export type AppState = {
|
||||
error?: string | null;
|
||||
progressMessages: string[];
|
||||
sessionStartDate?: Dayjs | null;
|
||||
privacyMode: PrivacyMode;
|
||||
vpnMode: VpnMode;
|
||||
tunnel: TunnelConfig;
|
||||
uiMode: 'Light' | 'Dark';
|
||||
uiTheme: 'Light' | 'Dark';
|
||||
localAppData: AppData;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { routes } from '../constants.ts';
|
||||
import { routes } from '../constants';
|
||||
|
||||
export type Routes = (typeof routes)[keyof typeof routes];
|
||||
|
||||
@@ -7,10 +7,10 @@ export default function ThemeSetter({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { uiMode } = useMainState();
|
||||
const { uiTheme } = useMainState();
|
||||
|
||||
return (
|
||||
<div className={clsx([uiMode === 'Dark' && 'dark', 'h-full'])}>
|
||||
<div className={clsx([uiTheme === 'Dark' && 'dark', 'h-full'])}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function TopBar() {
|
||||
}, [location.pathname, navBarData]);
|
||||
|
||||
return (
|
||||
<nav className="flex flex-row flex-nowrap justify-between items-center bg-white text-baltic-sea dark:bg-baltic-sea-jaguar dark:text-mercury-pinkish h-16 text-xl">
|
||||
<nav className="flex flex-row flex-nowrap justify-between items-center shrink-0 bg-white text-baltic-sea dark:bg-baltic-sea-jaguar dark:text-mercury-pinkish h-16 text-xl">
|
||||
{currentNavLocation?.leftIcon ? (
|
||||
<button className="w-6 mx-4" onClick={currentNavLocation.handleLeftNav}>
|
||||
<span className="font-icon dark:text-laughing-jack text-2xl">
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as ThemeSetter } from './ThemeSetter';
|
||||
export { default as TopBar } from './TopBar';
|
||||
export { default as ConnectionTimer } from './ConnectionTimer';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Config } from 'tailwindcss';
|
||||
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||
import headlessui from '@headlessui/tailwindcss';
|
||||
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
@@ -66,6 +67,8 @@ export default {
|
||||
'gun-powder': '#49454F',
|
||||
// [D] top-bar icon
|
||||
'laughing-jack': '#CAC4D0',
|
||||
// [L] button bg in disabled state
|
||||
'wind-chime': '#DEDEE1',
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
@@ -79,7 +82,7 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [headlessui],
|
||||
// Toggling dark mode manually
|
||||
darkMode: 'class',
|
||||
} satisfies Config;
|
||||
|
||||
@@ -188,6 +188,18 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
||||
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
||||
|
||||
"@headlessui/react@^1.7.17":
|
||||
version "1.7.17"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.17.tgz#a0ec23af21b527c030967245fd99776aa7352bc6"
|
||||
integrity sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==
|
||||
dependencies:
|
||||
client-only "^0.0.1"
|
||||
|
||||
"@headlessui/tailwindcss@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@headlessui/tailwindcss/-/tailwindcss-0.2.0.tgz#2c55c98fd8eee4b4f21ec6eb35a014b840059eec"
|
||||
integrity sha512-fpL830Fln1SykOCboExsWr3JIVeQKieLJ3XytLe/tt1A0XzqUthOftDmjcCYLW62w7mQI7wXcoPXr3tZ9QfGxw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.11.13":
|
||||
version "0.11.13"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297"
|
||||
@@ -932,6 +944,11 @@ chokidar@^3.5.3:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
clsx@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b"
|
||||
|
||||
Reference in New Issue
Block a user