added license, readme and theme management
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Xyroscar
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
68
README.md
68
README.md
@@ -1,7 +1,67 @@
|
|||||||
# Tauri + SvelteKit + TypeScript
|
# Resona
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, SvelteKit and TypeScript in Vite.
|
Resona is a desktop API client for testing and debugging HTTP APIs. Built with Tauri, SvelteKit, and Rust.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Features
|
||||||
|
|
||||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
- **Workspaces**: Organize your API requests into workspaces with tags for easy filtering
|
||||||
|
- **Collections**: Group related requests into collections within workspaces
|
||||||
|
- **Variables**: Define variables at global, workspace, collection, or request scope with automatic interpolation
|
||||||
|
- **HTTP Client**: Send HTTP requests with support for various body types (JSON, form-data, URL-encoded, etc.)
|
||||||
|
- **Sync Groups**: Sync variables across multiple workspaces
|
||||||
|
- **Themes**: Multiple built-in themes including light, dark, and Catppuccin variants (Latte, Frappe, Macchiato, Mocha)
|
||||||
|
- **Persistent Storage**: All data is stored locally using an embedded database (redb)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: SvelteKit, TypeScript, TailwindCSS, shadcn-svelte
|
||||||
|
- **Backend**: Rust, Tauri
|
||||||
|
- **Database**: redb (embedded key-value store)
|
||||||
|
- **HTTP**: reqwest
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Node.js (v18+)
|
||||||
|
- Rust (latest stable)
|
||||||
|
- Bun (or npm/yarn/pnpm)
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# Run in development mode
|
||||||
|
bun run tauri dev
|
||||||
|
|
||||||
|
# Build for production
|
||||||
|
bun run tauri build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
resona/
|
||||||
|
├── src/ # Frontend (SvelteKit)
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── components/ # UI components
|
||||||
|
│ │ ├── services/ # API service layer
|
||||||
|
│ │ └── types/ # TypeScript types
|
||||||
|
│ └── routes/ # SvelteKit routes
|
||||||
|
├── src-tauri/ # Backend (Rust/Tauri)
|
||||||
|
│ └── src/
|
||||||
|
│ ├── collections/ # Collections module
|
||||||
|
│ ├── db/ # Database layer
|
||||||
|
│ ├── http/ # HTTP client
|
||||||
|
│ ├── requests/ # Requests module
|
||||||
|
│ ├── settings/ # App settings
|
||||||
|
│ ├── variables/ # Variables module
|
||||||
|
│ └── workspaces/ # Workspaces module
|
||||||
|
└── static/ # Static assets
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ pub enum DbError {
|
|||||||
#[error("Serialization error: {0}")]
|
#[error("Serialization error: {0}")]
|
||||||
Serialization(String),
|
Serialization(String),
|
||||||
|
|
||||||
|
#[error("JSON error: {0}")]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
#[error("IO error: {0}")]
|
#[error("IO error: {0}")]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ mod collections;
|
|||||||
mod db;
|
mod db;
|
||||||
mod http;
|
mod http;
|
||||||
mod requests;
|
mod requests;
|
||||||
|
mod settings;
|
||||||
mod variables;
|
mod variables;
|
||||||
mod workspaces;
|
mod workspaces;
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ use requests::{
|
|||||||
create_request, delete_request, get_all_requests_by_workspace, get_request,
|
create_request, delete_request, get_all_requests_by_workspace, get_request,
|
||||||
get_requests_by_collection, get_standalone_requests_by_workspace, update_request,
|
get_requests_by_collection, get_standalone_requests_by_workspace, update_request,
|
||||||
};
|
};
|
||||||
|
use settings::{get_settings, reset_settings, update_settings};
|
||||||
use variables::{
|
use variables::{
|
||||||
create_variable, delete_variable, get_collection_variables, get_global_variables,
|
create_variable, delete_variable, get_collection_variables, get_global_variables,
|
||||||
get_request_variables, get_resolved_variables, get_variable, get_workspace_variables,
|
get_request_variables, get_resolved_variables, get_variable, get_workspace_variables,
|
||||||
@@ -86,6 +88,10 @@ pub fn run() {
|
|||||||
delete_variable,
|
delete_variable,
|
||||||
// HTTP client
|
// HTTP client
|
||||||
send_http_request,
|
send_http_request,
|
||||||
|
// Settings commands
|
||||||
|
get_settings,
|
||||||
|
update_settings,
|
||||||
|
reset_settings,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
27
src-tauri/src/settings/commands.rs
Normal file
27
src-tauri/src/settings/commands.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::db::Database;
|
||||||
|
|
||||||
|
use super::service::SettingsService;
|
||||||
|
use super::types::{AppSettings, UpdateSettingsInput};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_settings(db: State<Database>) -> Result<AppSettings, String> {
|
||||||
|
let service = SettingsService::new(db.inner().clone());
|
||||||
|
service.get().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn update_settings(
|
||||||
|
db: State<Database>,
|
||||||
|
input: UpdateSettingsInput,
|
||||||
|
) -> Result<AppSettings, String> {
|
||||||
|
let service = SettingsService::new(db.inner().clone());
|
||||||
|
service.update(input).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn reset_settings(db: State<Database>) -> Result<AppSettings, String> {
|
||||||
|
let service = SettingsService::new(db.inner().clone());
|
||||||
|
service.reset().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
7
src-tauri/src/settings/mod.rs
Normal file
7
src-tauri/src/settings/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mod commands;
|
||||||
|
mod service;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
pub use commands::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use types::{AppSettings, CustomTheme, Theme, ThemeColors, UpdateSettingsInput};
|
||||||
78
src-tauri/src/settings/service.rs
Normal file
78
src-tauri/src/settings/service.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use crate::db::{Database, DbResult, APP_SETTINGS};
|
||||||
|
|
||||||
|
use super::types::{AppSettings, UpdateSettingsInput};
|
||||||
|
|
||||||
|
const SETTINGS_KEY: &str = "settings";
|
||||||
|
|
||||||
|
pub struct SettingsService {
|
||||||
|
db: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsService {
|
||||||
|
pub fn new(db: Database) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> DbResult<AppSettings> {
|
||||||
|
let read_txn = self.db.begin_read()?;
|
||||||
|
let table = read_txn.open_table(APP_SETTINGS)?;
|
||||||
|
|
||||||
|
match table.get(SETTINGS_KEY)? {
|
||||||
|
Some(value) => {
|
||||||
|
let settings: AppSettings = serde_json::from_str(value.value())?;
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
None => Ok(AppSettings::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self, input: UpdateSettingsInput) -> DbResult<AppSettings> {
|
||||||
|
let mut settings = self.get()?;
|
||||||
|
|
||||||
|
if let Some(theme) = input.theme {
|
||||||
|
settings.theme = theme;
|
||||||
|
}
|
||||||
|
if let Some(custom_themes) = input.custom_themes {
|
||||||
|
settings.custom_themes = custom_themes;
|
||||||
|
}
|
||||||
|
if let Some(default_timeout) = input.default_timeout {
|
||||||
|
settings.default_timeout = default_timeout;
|
||||||
|
}
|
||||||
|
if let Some(follow_redirects) = input.follow_redirects {
|
||||||
|
settings.follow_redirects = follow_redirects;
|
||||||
|
}
|
||||||
|
if let Some(validate_ssl) = input.validate_ssl {
|
||||||
|
settings.validate_ssl = validate_ssl;
|
||||||
|
}
|
||||||
|
if let Some(max_history_items) = input.max_history_items {
|
||||||
|
settings.max_history_items = max_history_items;
|
||||||
|
}
|
||||||
|
if let Some(auto_save_requests) = input.auto_save_requests {
|
||||||
|
settings.auto_save_requests = auto_save_requests;
|
||||||
|
}
|
||||||
|
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut table = write_txn.open_table(APP_SETTINGS)?;
|
||||||
|
let json = serde_json::to_string(&settings)?;
|
||||||
|
table.insert(SETTINGS_KEY, json.as_str())?;
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&self) -> DbResult<AppSettings> {
|
||||||
|
let settings = AppSettings::default();
|
||||||
|
|
||||||
|
let write_txn = self.db.begin_write()?;
|
||||||
|
{
|
||||||
|
let mut table = write_txn.open_table(APP_SETTINGS)?;
|
||||||
|
let json = serde_json::to_string(&settings)?;
|
||||||
|
table.insert(SETTINGS_KEY, json.as_str())?;
|
||||||
|
}
|
||||||
|
write_txn.commit()?;
|
||||||
|
|
||||||
|
Ok(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src-tauri/src/settings/types.rs
Normal file
95
src-tauri/src/settings/types.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum Theme {
|
||||||
|
System,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
Latte,
|
||||||
|
Frappe,
|
||||||
|
Macchiato,
|
||||||
|
Mocha,
|
||||||
|
Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Theme {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::System
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CustomTheme {
|
||||||
|
pub name: String,
|
||||||
|
pub colors: ThemeColors,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ThemeColors {
|
||||||
|
pub background: String,
|
||||||
|
pub foreground: String,
|
||||||
|
pub primary: String,
|
||||||
|
pub secondary: String,
|
||||||
|
pub accent: String,
|
||||||
|
pub muted: String,
|
||||||
|
pub border: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppSettings {
|
||||||
|
pub theme: Theme,
|
||||||
|
#[serde(default)]
|
||||||
|
pub custom_themes: Vec<CustomTheme>,
|
||||||
|
#[serde(default = "default_timeout")]
|
||||||
|
pub default_timeout: u32,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub follow_redirects: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub validate_ssl: bool,
|
||||||
|
#[serde(default = "default_max_history")]
|
||||||
|
pub max_history_items: u32,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub auto_save_requests: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_timeout() -> u32 {
|
||||||
|
30000
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_max_history() -> u32 {
|
||||||
|
100
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AppSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
theme: Theme::System,
|
||||||
|
custom_themes: Vec::new(),
|
||||||
|
default_timeout: 30000,
|
||||||
|
follow_redirects: true,
|
||||||
|
validate_ssl: true,
|
||||||
|
max_history_items: 100,
|
||||||
|
auto_save_requests: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct UpdateSettingsInput {
|
||||||
|
pub theme: Option<Theme>,
|
||||||
|
pub custom_themes: Option<Vec<CustomTheme>>,
|
||||||
|
pub default_timeout: Option<u32>,
|
||||||
|
pub follow_redirects: Option<bool>,
|
||||||
|
pub validate_ssl: Option<bool>,
|
||||||
|
pub max_history_items: Option<u32>,
|
||||||
|
pub auto_save_requests: Option<bool>,
|
||||||
|
}
|
||||||
@@ -7,12 +7,18 @@
|
|||||||
import { Label } from "$lib/components/ui/label/index.js";
|
import { Label } from "$lib/components/ui/label/index.js";
|
||||||
import { Separator } from "$lib/components/ui/separator/index.js";
|
import { Separator } from "$lib/components/ui/separator/index.js";
|
||||||
import SettingsIcon from "@lucide/svelte/icons/settings";
|
import SettingsIcon from "@lucide/svelte/icons/settings";
|
||||||
import type { AppSettings } from "$lib/types/workspace";
|
import CheckIcon from "@lucide/svelte/icons/check";
|
||||||
|
import type { AppSettings, Theme } from "$lib/types/workspace";
|
||||||
import {
|
import {
|
||||||
get_settings,
|
get_settings,
|
||||||
update_settings,
|
update_settings,
|
||||||
reset_settings,
|
reset_settings,
|
||||||
} from "$lib/services/settings";
|
} from "$lib/services/settings";
|
||||||
|
import {
|
||||||
|
theme as themeStore,
|
||||||
|
themes,
|
||||||
|
themeLabels,
|
||||||
|
} from "$lib/theme-switcher";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -24,9 +30,10 @@
|
|||||||
|
|
||||||
let settings = $state<AppSettings>({
|
let settings = $state<AppSettings>({
|
||||||
theme: "system",
|
theme: "system",
|
||||||
|
customThemes: [],
|
||||||
defaultTimeout: 30000,
|
defaultTimeout: 30000,
|
||||||
followRedirects: true,
|
followRedirects: true,
|
||||||
validateSSL: true,
|
validateSsl: true,
|
||||||
maxHistoryItems: 100,
|
maxHistoryItems: 100,
|
||||||
autoSaveRequests: true,
|
autoSaveRequests: true,
|
||||||
});
|
});
|
||||||
@@ -56,10 +63,12 @@
|
|||||||
|
|
||||||
async function handleReset() {
|
async function handleReset() {
|
||||||
settings = await reset_settings();
|
settings = await reset_settings();
|
||||||
|
themeStore.applyTheme(settings.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleThemeChange(value: string) {
|
async function handleThemeChange(newTheme: Theme) {
|
||||||
settings.theme = value as "light" | "dark" | "system";
|
settings.theme = newTheme;
|
||||||
|
await themeStore.applyTheme(newTheme);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -77,42 +86,69 @@
|
|||||||
</Dialog.Description>
|
</Dialog.Description>
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<Tabs.Root value="general" class="flex-1 overflow-hidden">
|
<Tabs.Root value="appearance" class="flex-1 overflow-hidden">
|
||||||
<Tabs.List class="grid w-full grid-cols-3">
|
<Tabs.List class="grid w-full grid-cols-4">
|
||||||
|
<Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="requests">Requests</Tabs.Trigger>
|
<Tabs.Trigger value="requests">Requests</Tabs.Trigger>
|
||||||
<Tabs.Trigger value="advanced">Advanced</Tabs.Trigger>
|
<Tabs.Trigger value="advanced">Advanced</Tabs.Trigger>
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
|
|
||||||
<div class="mt-4 overflow-auto max-h-[400px]">
|
<div class="mt-4 overflow-auto max-h-[400px]">
|
||||||
<Tabs.Content value="general" class="space-y-4">
|
<Tabs.Content value="appearance" class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-3">
|
||||||
<Label for="theme">Theme</Label>
|
<Label>Theme</Label>
|
||||||
<Select.Root
|
|
||||||
type="single"
|
|
||||||
value={settings.theme}
|
|
||||||
onValueChange={handleThemeChange}
|
|
||||||
>
|
|
||||||
<Select.Trigger id="theme" class="w-full">
|
|
||||||
{settings.theme === "light"
|
|
||||||
? "Light"
|
|
||||||
: settings.theme === "dark"
|
|
||||||
? "Dark"
|
|
||||||
: "System"}
|
|
||||||
</Select.Trigger>
|
|
||||||
<Select.Content>
|
|
||||||
<Select.Item value="system">System</Select.Item>
|
|
||||||
<Select.Item value="light">Light</Select.Item>
|
|
||||||
<Select.Item value="dark">Dark</Select.Item>
|
|
||||||
</Select.Content>
|
|
||||||
</Select.Root>
|
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
Choose the application color theme.
|
Select a color theme for the application.
|
||||||
</p>
|
</p>
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
{#each themes as t}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex items-center justify-between px-3 py-2 rounded-md border text-sm transition-colors {settings.theme ===
|
||||||
|
t
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-border hover:bg-muted'}"
|
||||||
|
onclick={() => handleThemeChange(t)}
|
||||||
|
>
|
||||||
|
<span>{themeLabels[t]}</span>
|
||||||
|
{#if settings.theme === t}
|
||||||
|
<CheckIcon class="size-4 text-primary" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
|
<div class="rounded-lg border p-4 bg-muted/30">
|
||||||
|
<h4 class="font-medium mb-2">Theme Preview</h4>
|
||||||
|
<p class="text-sm text-muted-foreground mb-3">
|
||||||
|
Preview of the current theme colors.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div
|
||||||
|
class="size-8 rounded bg-background border"
|
||||||
|
title="Background"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="size-8 rounded bg-foreground"
|
||||||
|
title="Foreground"
|
||||||
|
></div>
|
||||||
|
<div class="size-8 rounded bg-primary" title="Primary"></div>
|
||||||
|
<div class="size-8 rounded bg-secondary" title="Secondary"></div>
|
||||||
|
<div class="size-8 rounded bg-muted" title="Muted"></div>
|
||||||
|
<div class="size-8 rounded bg-accent" title="Accent"></div>
|
||||||
|
<div
|
||||||
|
class="size-8 rounded bg-destructive"
|
||||||
|
title="Destructive"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="general" class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="maxHistory">History Items</Label>
|
<Label for="maxHistory">History Items</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -127,6 +163,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="space-y-0.5">
|
<div class="space-y-0.5">
|
||||||
<Label>Auto-save Requests</Label>
|
<Label>Auto-save Requests</Label>
|
||||||
@@ -208,15 +246,15 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={settings.validateSSL}
|
aria-checked={settings.validateSsl}
|
||||||
aria-label="Toggle SSL validation"
|
aria-label="Toggle SSL validation"
|
||||||
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {settings.validateSSL
|
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 {settings.validateSsl
|
||||||
? 'bg-primary'
|
? 'bg-primary'
|
||||||
: 'bg-input'}"
|
: 'bg-input'}"
|
||||||
onclick={() => (settings.validateSSL = !settings.validateSSL)}
|
onclick={() => (settings.validateSsl = !settings.validateSsl)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform {settings.validateSSL
|
class="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform {settings.validateSsl
|
||||||
? 'translate-x-5'
|
? 'translate-x-5'
|
||||||
: 'translate-x-0'}"
|
: 'translate-x-0'}"
|
||||||
></span>
|
></span>
|
||||||
|
|||||||
@@ -1,33 +1,16 @@
|
|||||||
import type { AppSettings } from "$lib/types/workspace";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import type { AppSettings, UpdateSettingsInput } from "$lib/types/workspace";
|
||||||
let settings: AppSettings = {
|
|
||||||
theme: "system",
|
|
||||||
defaultTimeout: 30000,
|
|
||||||
followRedirects: true,
|
|
||||||
validateSSL: true,
|
|
||||||
maxHistoryItems: 100,
|
|
||||||
autoSaveRequests: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function get_settings(): Promise<AppSettings> {
|
export async function get_settings(): Promise<AppSettings> {
|
||||||
return { ...settings };
|
return invoke<AppSettings>("get_settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update_settings(
|
export async function update_settings(
|
||||||
updates: Partial<AppSettings>
|
input: UpdateSettingsInput
|
||||||
): Promise<AppSettings> {
|
): Promise<AppSettings> {
|
||||||
settings = { ...settings, ...updates };
|
return invoke<AppSettings>("update_settings", { input });
|
||||||
return { ...settings };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reset_settings(): Promise<AppSettings> {
|
export async function reset_settings(): Promise<AppSettings> {
|
||||||
settings = {
|
return invoke<AppSettings>("reset_settings");
|
||||||
theme: "system",
|
|
||||||
defaultTimeout: 30000,
|
|
||||||
followRedirects: true,
|
|
||||||
validateSSL: true,
|
|
||||||
maxHistoryItems: 100,
|
|
||||||
autoSaveRequests: true,
|
|
||||||
};
|
|
||||||
return { ...settings };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
|
import { get_settings, update_settings } from "$lib/services/settings";
|
||||||
|
import type { Theme } from "$lib/types/workspace";
|
||||||
|
|
||||||
export const themes = [
|
export const themes = [
|
||||||
|
"system",
|
||||||
"light",
|
"light",
|
||||||
"dark",
|
"dark",
|
||||||
"latte",
|
"latte",
|
||||||
@@ -9,46 +12,105 @@ export const themes = [
|
|||||||
"macchiato",
|
"macchiato",
|
||||||
"mocha",
|
"mocha",
|
||||||
] as const;
|
] as const;
|
||||||
type Theme = (typeof themes)[number];
|
|
||||||
|
export const themeLabels: Record<Theme, string> = {
|
||||||
|
system: "System",
|
||||||
|
light: "Light",
|
||||||
|
dark: "Dark",
|
||||||
|
latte: "Latte (Catppuccin)",
|
||||||
|
frappe: "Frappe (Catppuccin)",
|
||||||
|
macchiato: "Macchiato (Catppuccin)",
|
||||||
|
mocha: "Mocha (Catppuccin)",
|
||||||
|
custom: "Custom",
|
||||||
|
};
|
||||||
|
|
||||||
const createThemeStore = () => {
|
const createThemeStore = () => {
|
||||||
const { subscribe, set } = writable<Theme>("light");
|
const { subscribe, set } = writable<Theme>("system");
|
||||||
|
|
||||||
function applyTheme(theme: Theme) {
|
function getSystemTheme(): "light" | "dark" {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
document.documentElement.classList.remove(...themes);
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
document.documentElement.classList.add(theme);
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
set(theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeTheme() {
|
|
||||||
if (browser) {
|
|
||||||
const storedTheme = localStorage.getItem("theme") as Theme | null;
|
|
||||||
const systemTheme: Theme = window.matchMedia(
|
|
||||||
"(prefers-color-scheme: dark)"
|
|
||||||
).matches
|
|
||||||
? "dark"
|
? "dark"
|
||||||
: "light";
|
: "light";
|
||||||
const initialTheme = storedTheme ?? systemTheme;
|
|
||||||
applyTheme(initialTheme);
|
|
||||||
}
|
}
|
||||||
|
return "light";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetToSystem() {
|
function applyThemeToDOM(theme: Theme) {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
localStorage.removeItem("theme");
|
const allThemes = [
|
||||||
initializeTheme();
|
"light",
|
||||||
|
"dark",
|
||||||
|
"latte",
|
||||||
|
"frappe",
|
||||||
|
"macchiato",
|
||||||
|
"mocha",
|
||||||
|
];
|
||||||
|
document.documentElement.classList.remove(...allThemes);
|
||||||
|
|
||||||
|
const effectiveTheme = theme === "system" ? getSystemTheme() : theme;
|
||||||
|
if (effectiveTheme !== "custom") {
|
||||||
|
document.documentElement.classList.add(effectiveTheme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
async function applyTheme(theme: Theme) {
|
||||||
|
applyThemeToDOM(theme);
|
||||||
|
set(theme);
|
||||||
|
try {
|
||||||
|
await update_settings({ theme });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save theme setting:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeTheme() {
|
||||||
|
if (browser) {
|
||||||
|
try {
|
||||||
|
const settings = await get_settings();
|
||||||
|
const theme = settings.theme;
|
||||||
|
applyThemeToDOM(theme);
|
||||||
|
set(theme);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load theme setting:", e);
|
||||||
|
applyThemeToDOM("system");
|
||||||
|
set("system");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for system theme changes
|
||||||
|
window
|
||||||
|
.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.addEventListener("change", () => {
|
||||||
|
// Re-apply if using system theme
|
||||||
|
const currentTheme = get(themeStore);
|
||||||
|
if (currentTheme === "system") {
|
||||||
|
applyThemeToDOM("system");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetToSystem() {
|
||||||
|
await applyTheme("system");
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeStore = {
|
||||||
subscribe,
|
subscribe,
|
||||||
applyTheme,
|
applyTheme,
|
||||||
initializeTheme,
|
initializeTheme,
|
||||||
resetToSystem,
|
resetToSystem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return themeStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function get<T>(store: {
|
||||||
|
subscribe: (fn: (value: T) => void) => () => void;
|
||||||
|
}): T {
|
||||||
|
let value: T;
|
||||||
|
store.subscribe((v) => (value = v))();
|
||||||
|
return value!;
|
||||||
|
}
|
||||||
|
|
||||||
export const theme = createThemeStore();
|
export const theme = createThemeStore();
|
||||||
|
|||||||
@@ -46,11 +46,47 @@ export type UpdateSyncGroupInput = {
|
|||||||
sync_secrets?: boolean;
|
sync_secrets?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Theme =
|
||||||
|
| "system"
|
||||||
|
| "light"
|
||||||
|
| "dark"
|
||||||
|
| "latte"
|
||||||
|
| "frappe"
|
||||||
|
| "macchiato"
|
||||||
|
| "mocha"
|
||||||
|
| "custom";
|
||||||
|
|
||||||
|
export type ThemeColors = {
|
||||||
|
background: string;
|
||||||
|
foreground: string;
|
||||||
|
primary: string;
|
||||||
|
secondary: string;
|
||||||
|
accent: string;
|
||||||
|
muted: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomTheme = {
|
||||||
|
name: string;
|
||||||
|
colors: ThemeColors;
|
||||||
|
};
|
||||||
|
|
||||||
export type AppSettings = {
|
export type AppSettings = {
|
||||||
theme: "light" | "dark" | "system";
|
theme: Theme;
|
||||||
|
customThemes: CustomTheme[];
|
||||||
defaultTimeout: number;
|
defaultTimeout: number;
|
||||||
followRedirects: boolean;
|
followRedirects: boolean;
|
||||||
validateSSL: boolean;
|
validateSsl: boolean;
|
||||||
maxHistoryItems: number;
|
maxHistoryItems: number;
|
||||||
autoSaveRequests: boolean;
|
autoSaveRequests: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateSettingsInput = {
|
||||||
|
theme?: Theme;
|
||||||
|
customThemes?: CustomTheme[];
|
||||||
|
defaultTimeout?: number;
|
||||||
|
followRedirects?: boolean;
|
||||||
|
validateSsl?: boolean;
|
||||||
|
maxHistoryItems?: number;
|
||||||
|
autoSaveRequests?: boolean;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user