generated from nhcarrigan/template
feat: add multiple productivity features and UI enhancements (#68)
## Summary This PR adds a collection of productivity features and UI enhancements to improve the Hikari Desktop experience: ### New Features - **Clipboard History** (#25) - Track and manage copied code snippets with language detection, search, filtering, and pinning - **Quick Actions Panel** (#15) - Buttons for common quick actions like "Review PR", "Run tests", "Explain file", with customizable actions - **Git Integration Panel** (#24) - View current branch, changed/staged files, quick git actions (commit, push, pull), and branch management - **Session Import/Export** (#8) - Export conversations to JSON and import previously saved sessions - **Snippet Library** (#22) - Save and reuse common prompts with categories and quick insert - **Session History** (#14) - Auto-save conversations with browsable history and search - **High Contrast Mode** (#20) - Accessibility theme with improved visibility - **Minimize to System Tray** (#11) - System tray support with right-click menu ### UI Enhancements - Trans-pride gradient theme applied across UI elements - Copy button added to code blocks - Linter formatting and eslint-disable comments for cleaner code ## Closes Closes #8 Closes #11 Closes #14 Closes #15 Closes #20 Closes #22 Closes #24 Closes #25 Closes #34 Closes #35 Closes #36 Closes #37 Closes #69 Closes #70 ## Test Plan - [ ] Verify clipboard history captures code from code block copy buttons - [ ] Verify clipboard history captures manually selected text from terminal - [ ] Test snippet library CRUD operations and insertion - [ ] Test quick actions panel with default and custom actions - [ ] Test git panel shows correct status, branch, and performs git operations - [ ] Test session history auto-save and restore - [ ] Test session import/export roundtrip - [ ] Verify high contrast mode provides adequate contrast - [ ] Test minimize to tray functionality and tray menu - [ ] Verify trans-pride gradient theme displays correctly in all themes --- *✨ This PR was created with help from Hikari~ 🌸* Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Reviewed-on: #68 Co-authored-by: Hikari <hikari@nhcarrigan.com> Co-committed-by: Hikari <hikari@nhcarrigan.com>
This commit was merged in pull request #68.
This commit is contained in:
+137
-6
@@ -1,7 +1,18 @@
|
||||
import { writable, derived } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export type Theme = "dark" | "light";
|
||||
export type Theme = "dark" | "light" | "high-contrast" | "custom";
|
||||
|
||||
export interface CustomThemeColors {
|
||||
bg_primary: string | null;
|
||||
bg_secondary: string | null;
|
||||
bg_terminal: string | null;
|
||||
accent_primary: string | null;
|
||||
accent_secondary: string | null;
|
||||
text_primary: string | null;
|
||||
text_secondary: string | null;
|
||||
border_color: string | null;
|
||||
}
|
||||
|
||||
export interface HikariConfig {
|
||||
model: string | null;
|
||||
@@ -15,9 +26,17 @@ export interface HikariConfig {
|
||||
notifications_enabled: boolean;
|
||||
notification_volume: number;
|
||||
always_on_top: boolean;
|
||||
minimize_to_tray: boolean;
|
||||
update_checks_enabled: boolean;
|
||||
character_panel_width: number | null;
|
||||
font_size: number;
|
||||
streamer_mode: boolean;
|
||||
streamer_hide_paths: boolean;
|
||||
compact_mode: boolean;
|
||||
profile_name: string | null;
|
||||
profile_avatar_path: string | null;
|
||||
profile_bio: string | null;
|
||||
custom_theme_colors: CustomThemeColors;
|
||||
}
|
||||
|
||||
const defaultConfig: HikariConfig = {
|
||||
@@ -32,9 +51,26 @@ const defaultConfig: HikariConfig = {
|
||||
notifications_enabled: true,
|
||||
notification_volume: 0.7,
|
||||
always_on_top: false,
|
||||
minimize_to_tray: false,
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: null,
|
||||
font_size: 14,
|
||||
streamer_mode: false,
|
||||
streamer_hide_paths: false,
|
||||
compact_mode: false,
|
||||
profile_name: null,
|
||||
profile_avatar_path: null,
|
||||
profile_bio: null,
|
||||
custom_theme_colors: {
|
||||
bg_primary: null,
|
||||
bg_secondary: null,
|
||||
bg_terminal: null,
|
||||
accent_primary: null,
|
||||
accent_secondary: null,
|
||||
text_primary: null,
|
||||
text_secondary: null,
|
||||
border_color: null,
|
||||
},
|
||||
};
|
||||
|
||||
function createConfigStore() {
|
||||
@@ -90,9 +126,24 @@ function createConfigStore() {
|
||||
closeSidebar: () => isSidebarOpen.set(false),
|
||||
toggleSidebar: () => isSidebarOpen.update((open) => !open),
|
||||
|
||||
setTheme: async (theme: Theme) => {
|
||||
await updateConfig({ theme });
|
||||
applyTheme(theme);
|
||||
setTheme: async (theme: Theme, customColors?: CustomThemeColors) => {
|
||||
const updates: Partial<HikariConfig> = { theme };
|
||||
if (customColors) {
|
||||
updates.custom_theme_colors = customColors;
|
||||
}
|
||||
await updateConfig(updates);
|
||||
let currentConfig: HikariConfig = defaultConfig;
|
||||
config.subscribe((c) => (currentConfig = c))();
|
||||
applyTheme(theme, currentConfig.custom_theme_colors);
|
||||
},
|
||||
|
||||
setCustomThemeColors: async (colors: CustomThemeColors) => {
|
||||
await updateConfig({ custom_theme_colors: colors });
|
||||
let currentConfig: HikariConfig = defaultConfig;
|
||||
config.subscribe((c) => (currentConfig = c))();
|
||||
if (currentConfig.theme === "custom") {
|
||||
applyCustomThemeColors(colors);
|
||||
}
|
||||
},
|
||||
|
||||
setFontSize: async (size: number) => {
|
||||
@@ -143,15 +194,72 @@ function createConfigStore() {
|
||||
config.subscribe((c) => (currentConfig = c))();
|
||||
return currentConfig;
|
||||
},
|
||||
|
||||
toggleStreamerMode: async () => {
|
||||
let currentConfig: HikariConfig = defaultConfig;
|
||||
config.subscribe((c) => (currentConfig = c))();
|
||||
await updateConfig({ streamer_mode: !currentConfig.streamer_mode });
|
||||
},
|
||||
|
||||
toggleCompactMode: async () => {
|
||||
let currentConfig: HikariConfig = defaultConfig;
|
||||
config.subscribe((c) => (currentConfig = c))();
|
||||
await updateConfig({ compact_mode: !currentConfig.compact_mode });
|
||||
},
|
||||
|
||||
setCompactMode: async (enabled: boolean) => {
|
||||
await updateConfig({ compact_mode: enabled });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function applyTheme(theme: Theme) {
|
||||
export function applyTheme(theme: Theme, customColors?: CustomThemeColors) {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
// For custom theme, we use dark as the base and override with custom colors
|
||||
document.documentElement.setAttribute("data-theme", theme === "custom" ? "dark" : theme);
|
||||
|
||||
// Clear any previously applied custom colors
|
||||
clearCustomThemeColors();
|
||||
|
||||
// Apply custom colors if theme is custom
|
||||
if (theme === "custom" && customColors) {
|
||||
applyCustomThemeColors(customColors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyCustomThemeColors(colors: CustomThemeColors) {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
if (colors.bg_primary) root.style.setProperty("--bg-primary", colors.bg_primary);
|
||||
if (colors.bg_secondary) root.style.setProperty("--bg-secondary", colors.bg_secondary);
|
||||
if (colors.bg_terminal) root.style.setProperty("--bg-terminal", colors.bg_terminal);
|
||||
if (colors.accent_primary) root.style.setProperty("--accent-primary", colors.accent_primary);
|
||||
if (colors.accent_secondary)
|
||||
root.style.setProperty("--accent-secondary", colors.accent_secondary);
|
||||
if (colors.text_primary) root.style.setProperty("--text-primary", colors.text_primary);
|
||||
if (colors.text_secondary) root.style.setProperty("--text-secondary", colors.text_secondary);
|
||||
if (colors.border_color) root.style.setProperty("--border-color", colors.border_color);
|
||||
}
|
||||
|
||||
export function clearCustomThemeColors() {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const customProperties = [
|
||||
"--bg-primary",
|
||||
"--bg-secondary",
|
||||
"--bg-terminal",
|
||||
"--accent-primary",
|
||||
"--accent-secondary",
|
||||
"--text-primary",
|
||||
"--text-secondary",
|
||||
"--border-color",
|
||||
];
|
||||
customProperties.forEach((prop) => root.style.removeProperty(prop));
|
||||
}
|
||||
|
||||
const MIN_FONT_SIZE = 10;
|
||||
const MAX_FONT_SIZE = 24;
|
||||
const DEFAULT_FONT_SIZE = 14;
|
||||
@@ -172,3 +280,26 @@ export { MIN_FONT_SIZE, MAX_FONT_SIZE, DEFAULT_FONT_SIZE };
|
||||
export const configStore = createConfigStore();
|
||||
|
||||
export const isDarkTheme = derived(configStore.config, ($config) => $config.theme === "dark");
|
||||
|
||||
export const isStreamerMode = derived(configStore.config, ($config) => $config.streamer_mode);
|
||||
export const isCompactMode = derived(configStore.config, ($config) => $config.compact_mode);
|
||||
export const shouldHidePaths = derived(
|
||||
configStore.config,
|
||||
($config) => $config.streamer_mode && $config.streamer_hide_paths
|
||||
);
|
||||
|
||||
/**
|
||||
* Masks file paths in text when streamer mode with hide paths is enabled.
|
||||
* Replaces username portion of paths with asterisks.
|
||||
*/
|
||||
export function maskPaths(text: string, hidePaths: boolean): string {
|
||||
if (!hidePaths) return text;
|
||||
|
||||
// Match Unix paths like /home/username/... or /Users/username/...
|
||||
// and Windows paths like C:\Users\username\...
|
||||
return text
|
||||
.replace(/\/home\/([^/\s]+)/g, "/home/****")
|
||||
.replace(/\/Users\/([^/\s]+)/g, "/Users/****")
|
||||
.replace(/C:\\Users\\([^\\\s]+)/gi, "C:\\Users\\****")
|
||||
.replace(/~\//g, "****/");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user