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}")]
|
||||
Serialization(String),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ mod collections;
|
||||
mod db;
|
||||
mod http;
|
||||
mod requests;
|
||||
mod settings;
|
||||
mod variables;
|
||||
mod workspaces;
|
||||
|
||||
@@ -18,6 +19,7 @@ use requests::{
|
||||
create_request, delete_request, get_all_requests_by_workspace, get_request,
|
||||
get_requests_by_collection, get_standalone_requests_by_workspace, update_request,
|
||||
};
|
||||
use settings::{get_settings, reset_settings, update_settings};
|
||||
use variables::{
|
||||
create_variable, delete_variable, get_collection_variables, get_global_variables,
|
||||
get_request_variables, get_resolved_variables, get_variable, get_workspace_variables,
|
||||
@@ -86,6 +88,10 @@ pub fn run() {
|
||||
delete_variable,
|
||||
// HTTP client
|
||||
send_http_request,
|
||||
// Settings commands
|
||||
get_settings,
|
||||
update_settings,
|
||||
reset_settings,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.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 { Separator } from "$lib/components/ui/separator/index.js";
|
||||
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 {
|
||||
get_settings,
|
||||
update_settings,
|
||||
reset_settings,
|
||||
} from "$lib/services/settings";
|
||||
import {
|
||||
theme as themeStore,
|
||||
themes,
|
||||
themeLabels,
|
||||
} from "$lib/theme-switcher";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
type Props = {
|
||||
@@ -24,9 +30,10 @@
|
||||
|
||||
let settings = $state<AppSettings>({
|
||||
theme: "system",
|
||||
customThemes: [],
|
||||
defaultTimeout: 30000,
|
||||
followRedirects: true,
|
||||
validateSSL: true,
|
||||
validateSsl: true,
|
||||
maxHistoryItems: 100,
|
||||
autoSaveRequests: true,
|
||||
});
|
||||
@@ -56,10 +63,12 @@
|
||||
|
||||
async function handleReset() {
|
||||
settings = await reset_settings();
|
||||
themeStore.applyTheme(settings.theme);
|
||||
}
|
||||
|
||||
function handleThemeChange(value: string) {
|
||||
settings.theme = value as "light" | "dark" | "system";
|
||||
async function handleThemeChange(newTheme: Theme) {
|
||||
settings.theme = newTheme;
|
||||
await themeStore.applyTheme(newTheme);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,42 +86,69 @@
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Tabs.Root value="general" class="flex-1 overflow-hidden">
|
||||
<Tabs.List class="grid w-full grid-cols-3">
|
||||
<Tabs.Root value="appearance" class="flex-1 overflow-hidden">
|
||||
<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="requests">Requests</Tabs.Trigger>
|
||||
<Tabs.Trigger value="advanced">Advanced</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div class="mt-4 overflow-auto max-h-[400px]">
|
||||
<Tabs.Content value="general" class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="theme">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>
|
||||
<Tabs.Content value="appearance" class="space-y-4">
|
||||
<div class="space-y-3">
|
||||
<Label>Theme</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Choose the application color theme.
|
||||
Select a color theme for the application.
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<Label for="maxHistory">History Items</Label>
|
||||
<Input
|
||||
@@ -127,6 +163,8 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="space-y-0.5">
|
||||
<Label>Auto-save Requests</Label>
|
||||
@@ -208,15 +246,15 @@
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={settings.validateSSL}
|
||||
aria-checked={settings.validateSsl}
|
||||
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-input'}"
|
||||
onclick={() => (settings.validateSSL = !settings.validateSSL)}
|
||||
onclick={() => (settings.validateSsl = !settings.validateSsl)}
|
||||
>
|
||||
<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-0'}"
|
||||
></span>
|
||||
|
||||
@@ -1,33 +1,16 @@
|
||||
import type { AppSettings } from "$lib/types/workspace";
|
||||
|
||||
let settings: AppSettings = {
|
||||
theme: "system",
|
||||
defaultTimeout: 30000,
|
||||
followRedirects: true,
|
||||
validateSSL: true,
|
||||
maxHistoryItems: 100,
|
||||
autoSaveRequests: true,
|
||||
};
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type { AppSettings, UpdateSettingsInput } from "$lib/types/workspace";
|
||||
|
||||
export async function get_settings(): Promise<AppSettings> {
|
||||
return { ...settings };
|
||||
return invoke<AppSettings>("get_settings");
|
||||
}
|
||||
|
||||
export async function update_settings(
|
||||
updates: Partial<AppSettings>
|
||||
input: UpdateSettingsInput
|
||||
): Promise<AppSettings> {
|
||||
settings = { ...settings, ...updates };
|
||||
return { ...settings };
|
||||
return invoke<AppSettings>("update_settings", { input });
|
||||
}
|
||||
|
||||
export async function reset_settings(): Promise<AppSettings> {
|
||||
settings = {
|
||||
theme: "system",
|
||||
defaultTimeout: 30000,
|
||||
followRedirects: true,
|
||||
validateSSL: true,
|
||||
maxHistoryItems: 100,
|
||||
autoSaveRequests: true,
|
||||
};
|
||||
return { ...settings };
|
||||
return invoke<AppSettings>("reset_settings");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { writable } from "svelte/store";
|
||||
import { browser } from "$app/environment";
|
||||
import { get_settings, update_settings } from "$lib/services/settings";
|
||||
import type { Theme } from "$lib/types/workspace";
|
||||
|
||||
export const themes = [
|
||||
"system",
|
||||
"light",
|
||||
"dark",
|
||||
"latte",
|
||||
@@ -9,46 +12,105 @@ export const themes = [
|
||||
"macchiato",
|
||||
"mocha",
|
||||
] 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 { subscribe, set } = writable<Theme>("light");
|
||||
const { subscribe, set } = writable<Theme>("system");
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
function getSystemTheme(): "light" | "dark" {
|
||||
if (browser) {
|
||||
document.documentElement.classList.remove(...themes);
|
||||
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
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
const initialTheme = storedTheme ?? systemTheme;
|
||||
applyTheme(initialTheme);
|
||||
}
|
||||
return "light";
|
||||
}
|
||||
|
||||
function resetToSystem() {
|
||||
function applyThemeToDOM(theme: Theme) {
|
||||
if (browser) {
|
||||
localStorage.removeItem("theme");
|
||||
initializeTheme();
|
||||
const allThemes = [
|
||||
"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,
|
||||
applyTheme,
|
||||
initializeTheme,
|
||||
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();
|
||||
|
||||
@@ -46,11 +46,47 @@ export type UpdateSyncGroupInput = {
|
||||
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 = {
|
||||
theme: "light" | "dark" | "system";
|
||||
theme: Theme;
|
||||
customThemes: CustomTheme[];
|
||||
defaultTimeout: number;
|
||||
followRedirects: boolean;
|
||||
validateSSL: boolean;
|
||||
validateSsl: boolean;
|
||||
maxHistoryItems: number;
|
||||
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