feat(settings): Implement remaining preference types

This commit introduces support for all remaining preference types as specified in the API, including password, checkbox, appPicker, file, and directory. Eventually, we will probably have to somehow make the password preferences encrypted.
This commit is contained in:
ByteAtATime 2025-06-25 20:14:21 -07:00
parent f2191cbdb4
commit 1bdb0f534e
No known key found for this signature in database
4 changed files with 88 additions and 21 deletions

View file

@ -121,7 +121,7 @@ export const PreferenceSchema = z.object({
name: z.string(),
title: z.string().optional(),
description: z.string().optional(),
type: z.enum(['textfield', 'dropdown', 'checkbox', 'directory']),
type: z.enum(['textfield', 'password', 'checkbox', 'dropdown', 'appPicker', 'file', 'directory']),
required: z.boolean().optional(),
default: z.union([z.string(), z.boolean()]).optional(),
data: z

View file

@ -27,6 +27,7 @@ import { showHUD } from './hud';
import { BrowserExtensionAPI } from './browserExtension';
import { Clipboard } from './clipboard';
import * as OAuth from './oauth';
import type { Preference } from '@raycast-linux/protocol';
const Image = {
Mask: {
@ -36,20 +37,9 @@ const Image = {
};
let currentPluginName: string | null = null;
let currentPluginPreferences: Array<{
name: string;
title: string;
description?: string;
type: 'textfield' | 'dropdown' | 'checkbox' | 'directory';
required?: boolean;
default?: string | boolean;
data?: Array<{ title: string; value: string }>;
}> = [];
let currentPluginPreferences: Preference[] = [];
export const setCurrentPlugin = (
pluginName: string,
preferences?: typeof currentPluginPreferences
) => {
export const setCurrentPlugin = (pluginName: string, preferences?: Preference[]) => {
currentPluginName = pluginName;
currentPluginPreferences = preferences || [];
};

View file

@ -0,0 +1,32 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Button } from '$lib/components/ui/button';
import { Eye, EyeOff } from '@lucide/svelte';
import type { HTMLInputAttributes } from 'svelte/elements';
type Props = {
value: string | undefined | null;
} & HTMLInputAttributes;
let { value, ...restProps }: Props = $props();
let showPassword = $state(false);
const inputType = $derived(showPassword ? 'text' : 'password');
</script>
<div class="relative">
<Input {...restProps} {value} type={inputType} class="pr-10" />
<Button
type="button"
variant="ghost"
size="sm"
class="absolute top-1/2 right-0 h-full w-10 -translate-y-1/2 rounded-lg"
onclick={() => (showPassword = !showPassword)}
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{#if showPassword}
<EyeOff class="size-4" />
{:else}
<Eye class="size-4" />
{/if}
</Button>
</div>

View file

@ -7,6 +7,9 @@
import BaseList from './BaseList.svelte';
import { Button } from './ui/button';
import path from 'path';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { appsStore } from '$lib/apps.svelte';
import PasswordInput from './PasswordInput.svelte';
type Props = {
plugins: PluginInfo[];
@ -30,6 +33,7 @@
let selectedIndex = $state(0);
let preferenceValues = $state<Record<string, unknown>>({});
let searchText = $state('');
const { apps } = $derived(appsStore);
$effect(() => {
// This effect syncs the local preference values with the prop.
@ -135,6 +139,16 @@
function getPreferenceValue(pref: Preference): unknown {
return (preferenceValues as Record<string, unknown>)[pref.name] ?? pref.default ?? '';
}
async function browse(type: 'file' | 'directory', prefName: string) {
const result = await openDialog({
directory: type === 'directory',
multiple: false
});
if (typeof result === 'string') {
handlePreferenceChange(prefName, result);
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
@ -222,6 +236,13 @@
handlePreferenceChange(pref.name, (e.target as HTMLInputElement)?.value)}
placeholder={pref.default as string}
/>
{:else if pref.type === 'password'}
<PasswordInput
value={getPreferenceValue(pref) as string}
onchange={(e) =>
handlePreferenceChange(pref.name, (e.target as HTMLInputElement)?.value)}
placeholder="••••••••••••"
/>
{:else if pref.type === 'checkbox'}
<label class="flex items-center gap-2">
<Checkbox
@ -250,13 +271,37 @@
{/each}
</Select.Content>
</Select.Root>
{:else if pref.type === 'directory'}
<Input
value={getPreferenceValue(pref) as string}
onchange={(e) =>
handlePreferenceChange(pref.name, (e.target as HTMLInputElement)?.value)}
placeholder={pref.default as string}
/>
{:else if pref.type === 'appPicker'}
<Select.Root
value={(getPreferenceValue(pref) as string) || undefined}
onValueChange={(value) => handlePreferenceChange(pref.name, value)}
>
<Select.Trigger class="w-full">
{@const selectedApp = apps.find((a) => a.exec === getPreferenceValue(pref))}
{selectedApp?.name ?? 'Select Application'}
</Select.Trigger>
<Select.Content>
{#each apps as app (app.exec)}
<Select.Item value={app.exec}>{app.name}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{:else if pref.type === 'file' || pref.type === 'directory'}
<div class="flex items-center gap-2">
<Input
value={getPreferenceValue(pref) as string}
onchange={(e) =>
handlePreferenceChange(pref.name, (e.target as HTMLInputElement)?.value)}
placeholder={pref.default as string}
class="flex-grow"
/>
<Button
variant="outline"
onclick={() => browse(pref.type as 'file' | 'directory', pref.name)}
>
Browse...
</Button>
</div>
{/if}
</div>
{/each}