diff --git a/src-tauri/.sqlx/query-daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127.json b/src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json similarity index 54% rename from src-tauri/.sqlx/query-daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127.json rename to src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json index 4301ff18..fca2fa85 100644 --- a/src-tauri/.sqlx/query-daf3fc4a8c620af81be9e2ef25a4fd352d9498f9b6c7c567635edcbbe9ac5127.json +++ b/src-tauri/.sqlx/query-09a8074f7ef8d734607f95391819e38f822488933905dcc7a7788bd184bc7796.json @@ -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" } diff --git a/src-tauri/.sqlx/query-a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067.json b/src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json similarity index 69% rename from src-tauri/.sqlx/query-a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067.json rename to src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json index f06da07c..44576266 100644 --- a/src-tauri/.sqlx/query-a25253024aec650aff34b38684de49b94514f676863acf02e0411da1161af067.json +++ b/src-tauri/.sqlx/query-f27d45f7ea2b04fc203e46a85be96a591a6495794dc042e1e2f3460c9ed65a5c.json @@ -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" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 967b582b..c15960f2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d4cb9fcf..5850192e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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"] } diff --git a/src-tauri/migrations/20240111221224_settings.sql b/src-tauri/migrations/20240111221224_settings.sql index b9a150b2..4044bef4 100644 --- a/src-tauri/migrations/20240111221224_settings.sql +++ b/src-tauri/migrations/20240111221224_settings.sql @@ -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 ); diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 512c199e..fd3905b2 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -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) -> Result { 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?; diff --git a/src-tauri/src/send.rs b/src-tauri/src/send.rs index d570bc75..9ad7bb7c 100644 --- a/src-tauri/src/send.rs +++ b/src-tauri/src/send.rs @@ -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 diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index 348d7a7f..ab758dbe 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -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(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('zoom', ({ payload: zoomDelta, windowLabel }) => { diff --git a/src-web/components/SettingsDialog.tsx b/src-web/components/SettingsDialog.tsx index 1db97c51..bf6b6ed0 100644 --- a/src-web/components/SettingsDialog.tsx +++ b/src-web/components/SettingsDialog.tsx @@ -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 ( - - updateSettings.mutateAsync({ ...settings, validateCertificates }) - } - /> - updateSettings.mutateAsync({ ...settings, followRedirects })} - /> - +
+ + updateSettings.mutateAsync({ ...settings, validateCertificates }) + } + /> + + + updateSettings.mutateAsync({ ...settings, followRedirects }) + } + /> + +
Request Timeout (ms)
+
+ parseInt(value) >= 0} + onChange={(v) => + updateSettings.mutateAsync({ ...settings, requestTimeout: parseInt(v) || 0 }) + } + /> +
+ +
Appearance
+ +
+ {/**/}
); }; + +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', +}; diff --git a/src-web/hooks/useTheme.ts b/src-web/hooks/useTheme.ts index d049caf7..78be5d69 100644 --- a/src-web/hooks/useTheme.ts +++ b/src-web/hooks/useTheme.ts @@ -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({ - key: 'appearance', - defaultValue: getAppearance(), - }); + const [preferredAppearance, setPreferredAppearance] = useState( + 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 }; } diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 4429f16b..bec463e5 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -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 { diff --git a/src-web/lib/theme/window.ts b/src-web/lib/theme/window.ts index a14e31d6..d055d710 100644 --- a/src-web/lib/theme/window.ts +++ b/src-web/lib/theme/window.ts @@ -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'); diff --git a/src-web/main.css b/src-web/main.css index d64a736d..c3a49846 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -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; + } } diff --git a/src-web/main.tsx b/src-web/main.tsx index 088cb07f..e3209990 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -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( diff --git a/tailwind.config.cjs b/tailwind.config.cjs index a9054c5c..8830f323 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -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: {