Appearance setting and gzip/etc support

This commit is contained in:
Gregory Schier
2024-01-12 13:39:08 -08:00
parent 202e272e90
commit 905bce0322
15 changed files with 180 additions and 75 deletions

View File

@@ -1,12 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n follow_redirects,\n validate_certificates,\n theme\n ) = (?, ?, ?) WHERE id = 'default';\n ",
"query": "\n UPDATE settings SET (\n follow_redirects,\n validate_certificates,\n theme,\n appearance\n ) = (?, ?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
"Right": 4
},
"nullable": []
},
"hash": "daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127"
"hash": "09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n follow_redirects,\n validate_certificates,\n theme\n FROM settings\n WHERE id = 'default'\n ",
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n follow_redirects,\n validate_certificates,\n request_timeout,\n theme,\n appearance\n FROM settings\n WHERE id = 'default'\n ",
"describe": {
"columns": [
{
@@ -34,8 +34,18 @@
"type_info": "Bool"
},
{
"name": "theme",
"name": "request_timeout",
"ordinal": 6,
"type_info": "Int64"
},
{
"name": "theme",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "appearance",
"ordinal": 8,
"type_info": "Text"
}
],
@@ -49,8 +59,10 @@
false,
false,
false,
false,
false,
false
]
},
"hash": "a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067"
"hash": "f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c"
}

15
src-tauri/Cargo.lock generated
View File

@@ -81,6 +81,20 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "async-compression"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
dependencies = [
"brotli",
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "atk"
version = "0.15.1"
@@ -3337,6 +3351,7 @@ version = "0.11.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
dependencies = [
"async-compression",
"base64 0.21.5",
"bytes",
"encoding_rs",

View File

@@ -25,7 +25,7 @@ chrono = { version = "0.4.23", features = ["serde"] }
futures = "0.3.26"
http = "0.2.8"
rand = "0.8.5"
reqwest = { version = "0.11.14", features = ["json", "multipart"] }
reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }

View File

@@ -7,5 +7,7 @@ CREATE TABLE settings
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
follow_redirects BOOLEAN DEFAULT TRUE NOT NULL,
validate_certificates BOOLEAN DEFAULT TRUE NOT NULL,
theme TEXT DEFAULT 'system' NOT NULL
request_timeout INTEGER DEFAULT 0 NOT NULL,
theme TEXT DEFAULT 'default' NOT NULL,
appearance TEXT DEFAULT 'system' NOT NULL
);

View File

@@ -20,6 +20,8 @@ pub struct Settings {
pub validate_certificates: bool,
pub follow_redirects: bool,
pub theme: String,
pub appearance: String,
pub request_timeout: i64,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -312,7 +314,9 @@ async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
updated_at,
follow_redirects,
validate_certificates,
theme
request_timeout,
theme,
appearance
FROM settings
WHERE id = 'default'
"#,
@@ -347,12 +351,14 @@ pub async fn update_settings(
UPDATE settings SET (
follow_redirects,
validate_certificates,
theme
) = (?, ?, ?) WHERE id = 'default';
theme,
appearance
) = (?, ?, ?, ?) WHERE id = 'default';
"#,
settings.follow_redirects,
settings.validate_certificates,
settings.theme,
settings.appearance,
)
.execute(pool)
.await?;

View File

@@ -1,15 +1,16 @@
use std::fs;
use std::fs::{create_dir_all, File};
use std::io::Write;
use std::time::Duration;
use base64::Engine;
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use log::warn;
use reqwest::multipart;
use reqwest::redirect::Policy;
use sqlx::{Pool, Sqlite};
use sqlx::types::Json;
use sqlx::{Pool, Sqlite};
use tauri::{AppHandle, Wry};
use crate::{emit_side_effect, models, render, response_err};
@@ -38,17 +39,26 @@ pub async fn actually_send_request(
.await
.expect("Failed to get settings");
let client = reqwest::Client::builder()
let mut client_builder = reqwest::Client::builder()
.redirect(match settings.follow_redirects {
true => Policy::limited(10), // TODO: Handle redirects natively
false => Policy::none(),
})
.gzip(true)
.brotli(true)
.deflate(true)
.referer(false)
.danger_accept_invalid_certs(!settings.validate_certificates)
.connection_verbose(true) // TODO: Capture this log somehow
.tls_info(true)
// .use_rustls_tls() // TODO: Make this configurable (maybe)
.build()
.expect("Failed to build client");
.tls_info(true);
if settings.request_timeout > 0 {
client_builder =
client_builder.timeout(Duration::from_millis(settings.request_timeout.unsigned_abs()));
}
// .use_rustls_tls() // TODO: Make this configurable (maybe)
let client = client_builder.build().expect("Failed to build client");
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
.expect("Failed to create method");
@@ -258,6 +268,7 @@ pub async fn actually_send_request(
response.url = v.url().to_string();
let body_bytes = v.bytes().await.expect("Failed to get body").to_vec();
response.content_length = Some(body_bytes.len() as i64);
println!("Response: {:?}", body_bytes.len());
{
// Write body to FS

View File

@@ -10,6 +10,7 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { requestsQueryKey } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { responsesQueryKey } from '../hooks/useResponses';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
@@ -49,6 +50,8 @@ export function GlobalHooks() {
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: payload.model === 'settings'
? settingsQueryKey()
: null;
if (queryKey === null) {
@@ -74,6 +77,8 @@ export function GlobalHooks() {
? workspacesQueryKey(payload)
: payload.model === 'key_value'
? keyValueQueryKey(payload)
: payload.model === 'settings'
? settingsQueryKey()
: null;
if (queryKey === null) {
@@ -107,6 +112,8 @@ export function GlobalHooks() {
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
} else if (payload.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
} else if (payload.model === 'settings') {
queryClient.setQueryData(settingsQueryKey(), undefined);
}
});
useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {

View File

@@ -1,33 +1,83 @@
import classNames from 'classnames';
import { useSettings } from '../hooks/useSettings';
import { useTheme } from '../hooks/useTheme';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import type { Appearance } from '../lib/theme/window';
import { Checkbox } from './core/Checkbox';
import { Input } from './core/Input';
import { VStack } from './core/Stacks';
export const SettingsDialog = () => {
const { appearance, toggleAppearance } = useTheme();
const { appearance, setAppearance } = useTheme();
const settings = useSettings();
const updateSettings = useUpdateSettings();
if (settings == null) {
return null;
}
console.log('SETTINGS', settings);
return (
<VStack space={2}>
<Checkbox
checked={settings.validateCertificates}
title="Validate TLS Certificates"
onChange={(validateCertificates) =>
updateSettings.mutateAsync({ ...settings, validateCertificates })
}
/>
<Checkbox
checked={settings.followRedirects}
title="Follow Redirects"
onChange={(followRedirects) => updateSettings.mutateAsync({ ...settings, followRedirects })}
/>
<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />
<div className="w-full gap-2 grid grid-cols-[auto_1fr] gap-x-6 auto-rows-[2rem] items-center">
<Checkbox
className="col-span-full"
checked={settings.validateCertificates}
title="Validate TLS Certificates"
onChange={(validateCertificates) =>
updateSettings.mutateAsync({ ...settings, validateCertificates })
}
/>
<Checkbox
className="col-span-full"
checked={settings.followRedirects}
title="Follow Redirects"
onChange={(followRedirects) =>
updateSettings.mutateAsync({ ...settings, followRedirects })
}
/>
<div>Request Timeout (ms)</div>
<div>
<Input
size="sm"
name="requestTimeout"
label="Request Timeout (ms)"
containerClassName="col-span-2"
hideLabel
defaultValue={`${settings.requestTimeout}`}
validate={(value) => parseInt(value) >= 0}
onChange={(v) =>
updateSettings.mutateAsync({ ...settings, requestTimeout: parseInt(v) || 0 })
}
/>
</div>
<div>Appearance</div>
<select
value={settings.appearance}
style={selectBackgroundStyles}
onChange={(e) => updateSettings.mutateAsync({ ...settings, appearance: e.target.value })}
className={classNames(
'border w-full px-2 outline-none bg-transparent',
'border-highlight focus:border-focus',
'h-sm',
)}
>
<option value="system">Match System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
{/*<Checkbox checked={appearance === 'dark'} title="Dark Mode" onChange={toggleAppearance} />*/}
</VStack>
);
};
const selectBackgroundStyles = {
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
backgroundPosition: 'right 0.5rem center',
backgroundRepeat: 'no-repeat',
backgroundSize: '1.5em 1.5em',
};

View File

@@ -1,30 +1,35 @@
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import type { Appearance } from '../lib/theme/window';
import {
getAppearance,
setAppearance,
setAppearanceOnDocument,
getPreferredAppearance,
subscribeToPreferredAppearanceChange,
} from '../lib/theme/window';
import { useKeyValue } from './useKeyValue';
import { useSettings } from './useSettings';
export function useTheme() {
const appearanceKv = useKeyValue<Appearance>({
key: 'appearance',
defaultValue: getAppearance(),
});
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
getPreferredAppearance(),
);
const handleToggleAppearance = async () => {
appearanceKv.set(appearanceKv.value === 'dark' ? 'light' : 'dark');
};
const settings = useSettings();
// Set appearance when preferred theme changes
useEffect(() => subscribeToPreferredAppearanceChange(appearanceKv.set), [appearanceKv.set]);
useEffect(() => {
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
}, []);
// Sync appearance when k/v changes
useEffect(() => setAppearance(appearanceKv.value), [appearanceKv.value]);
const appearance =
settings == null || settings?.appearance === 'system'
? preferredAppearance
: settings.appearance;
return {
appearance: appearanceKv.value,
toggleAppearance: handleToggleAppearance,
};
useEffect(() => {
if (settings == null) {
return;
}
setAppearanceOnDocument(settings.appearance as Appearance);
}, [appearance, settings]);
return { appearance };
}

View File

@@ -21,7 +21,9 @@ export interface Settings extends BaseModel {
readonly model: 'settings';
validateCertificates: boolean;
followRedirects: boolean;
requestTimeout: number;
theme: string;
appearance: string;
}
export interface Workspace extends BaseModel {

View File

@@ -1,7 +1,9 @@
import type { AppTheme, AppThemeColors } from './theme';
import { generateCSS, toTailwindVariable } from './theme';
export type Appearance = 'dark' | 'light';
export type Appearance = 'dark' | 'light' | 'system';
const DEFAULT_APPEARANCE: Appearance = 'system';
enum Theme {
yaak = 'yaak',
@@ -61,19 +63,11 @@ const lightTheme: AppTheme = {
},
};
export function getAppearance(): Appearance {
const docAppearance = document.documentElement.getAttribute('data-appearance');
if (docAppearance === 'dark' || docAppearance === 'light') {
return docAppearance;
}
return getPreferredAppearance();
}
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
const theme = resolvedAppearance === 'dark' ? darkTheme : lightTheme;
export function setAppearance(a?: Appearance) {
const appearance = a ?? getPreferredAppearance();
const theme = appearance === 'dark' ? darkTheme : lightTheme;
document.documentElement.setAttribute('data-appearance', appearance);
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
document.documentElement.setAttribute('data-theme', theme.name);
let existingStyleEl = document.head.querySelector(`style[data-theme-definition]`);
@@ -85,11 +79,11 @@ export function setAppearance(a?: Appearance) {
existingStyleEl.textContent = [
`/* ${darkTheme.name} */`,
`[data-appearance="dark"] {`,
`[data-resolved-appearance="dark"] {`,
...generateCSS(darkTheme).map(toTailwindVariable),
'}',
`/* ${lightTheme.name} */`,
`[data-appearance="light"] {`,
`[data-resolved-appearance="light"] {`,
...generateCSS(lightTheme).map(toTailwindVariable),
'}',
].join('\n');

View File

@@ -68,4 +68,14 @@
--color-white: 255 100% 100%;
--color-black: 255 0% 0%;
}
select {
@apply appearance-none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
-webkit-print-color-adjust: exact;
}
}

View File

@@ -2,9 +2,7 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { attachConsole } from 'tauri-plugin-log-api';
import { App } from './components/App';
import { getKeyValue } from './lib/keyValueStore';
import { maybeRestorePathname } from './lib/persistPathname';
import { getPreferredAppearance, setAppearance } from './lib/theme/window';
import './main.css';
await attachConsole();
@@ -15,13 +13,6 @@ document.addEventListener('keydown', (e) => {
if (e.key === 'Backspace') e.preventDefault();
});
setAppearance(
await getKeyValue({
key: 'appearance',
fallback: getPreferredAppearance(),
}),
);
createRoot(document.getElementById('root') as HTMLElement).render(
<StrictMode>
<App />

View File

@@ -8,7 +8,7 @@ const height = {
/** @type {import("tailwindcss").Config} */
module.exports = {
darkMode: ['class', '[data-appearance="dark"]'],
darkMode: ['class', '[data-resolved-appearance="dark"]'],
content: ['./index.html', './src-web/**/*.{html,js,jsx,ts,tsx}'],
theme: {
extend: {