Compare commits

...

2 Commits

Author SHA1 Message Date
nicwands
2ab76684cc preferences inputs WIP 2026-03-26 15:32:44 -04:00
nicwands
3c0184cf63 Preferences inputs WIP 2026-03-26 15:32:13 -04:00
11 changed files with 2156 additions and 146 deletions

View File

@@ -3,6 +3,8 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import { app, ipcMain, BrowserWindow, shell } from "electron";
import filesystemPlugin from "@takerofnotes/plugin-filesystem";
import supabasePlugin from "@takerofnotes/plugin-supabase";
import s3Plugin from "@takerofnotes/plugin-s3";
import postgresPlugin from "@takerofnotes/plugin-postgre-sql";
import sodium from "libsodium-wrappers";
import { v4 } from "uuid";
import { Index } from "flexsearch";
@@ -323,10 +325,8 @@ const initPluginManager = (runtime, plugins, config) => {
registry.register(plugin);
}
const manager = createPluginManager(registry);
manager.setActivePlugin(
config.activeAdapter,
config.adapters[config.activeAdapter]
);
const activeConfig = config.adapters?.[config.activeAdapter] || {};
manager.setActivePlugin(config.activeAdapter, activeConfig);
return manager;
};
const initConfigManager = async (runtime) => {
@@ -439,7 +439,12 @@ app.whenReady().then(async () => {
const { pluginManager, configManager } = await initializeCore(
"electron-main",
{
plugins: [filesystemPlugin, supabasePlugin]
plugins: [
filesystemPlugin,
s3Plugin,
postgresPlugin,
supabasePlugin
]
}
);
ipcMain.handle("pluginManager:call", async (_, method, ...args) => {

1820
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,8 @@
"@fuzzco/font-loader": "^1.0.2",
"@takerofnotes/plugin-browser": "^0.2.0",
"@takerofnotes/plugin-filesystem": "^0.3.0",
"@takerofnotes/plugin-postgre-sql": "^0.1.0",
"@takerofnotes/plugin-s3": "^0.2.0",
"@takerofnotes/plugin-supabase": "^0.2.0",
"@tiptap/extension-code-block-lowlight": "^3.20.0",
"@tiptap/extension-document": "^3.19.0",

View File

@@ -56,9 +56,7 @@ export const initializeCore = async (runtime, { plugins }) => {
const config = await configManager.loadConfig()
const pluginManager = initPluginManager(runtime, plugins, config)
// -------------------------
// NotesAPI bootstrap
// -------------------------
let notesAPI = null
let initPromise = null

View File

@@ -3,6 +3,8 @@ import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import { app, shell, BrowserWindow, ipcMain } from 'electron'
import filesystemPlugin from '@takerofnotes/plugin-filesystem'
import supabasePlugin from '@takerofnotes/plugin-supabase'
import s3Plugin from '@takerofnotes/plugin-s3'
import postgresPlugin from '@takerofnotes/plugin-postgre-sql'
import { initializeCore } from '../core/index.js'
import { join } from 'path'
@@ -78,7 +80,12 @@ app.whenReady().then(async () => {
const { pluginManager, configManager } = await initializeCore(
'electron-main',
{
plugins: [filesystemPlugin, supabasePlugin],
plugins: [
filesystemPlugin,
s3Plugin,
postgresPlugin,
supabasePlugin,
],
},
)

View File

@@ -0,0 +1,40 @@
<template>
<div class="preferences-directory-input">
<input v-model="model" type="text" />
<button @click="openDirectoryPicker">Browse</button>
<input
ref="fileInput"
type="file"
webkitdirectory
style="display: none"
@change="handleDirectoryChange"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
const model = defineModel()
const fileInput = ref(null)
function openDirectoryPicker() {
fileInput.value?.click()
}
function handleDirectoryChange(event) {
const files = event.target.files
if (files.length > 0) {
const path = files[0].webkitRelativePath || files[0].path
const directoryPath = path.split('/').slice(0, -1).join('/')
model.value = directoryPath || files[0].path
}
}
</script>
<style lang="scss">
.preferences-directory-input {
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<div v-if="config" class="preferences-encryption">
<h2 class="section-title h1 mono">Encryption</h2>
<preferences-text-input
v-model.trim="config.encryptionKey"
type="text"
label="Encryption Key"
key="encryptionKey"
required
/>
</div>
</template>
<script setup>
import PreferencesTextInput from '@/components/preferences/TextInput.vue'
import useConfig from '@/composables/useConfig'
import { onMounted } from 'vue'
const { config, ensureConfig } = useConfig()
onMounted(async () => {
await ensureConfig()
})
const validate = async () => {
if (!config.value.encryptionKey) {
throw new Error('Please fill in the encryption key')
}
}
defineExpose({ validate })
</script>
<style lang="scss">
.preferences-encryption {
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<div v-if="config" class="preferences-storage">
<h2 class="section-title h1 mono">Storage Plugin</h2>
<div v-for="plugin in plugins" class="plugin" :key="plugin.id">
<input
v-model="selectedPluginId"
name="plugins"
type="radio"
:id="plugin.id"
:value="plugin.id"
/>
<div class="info">
<p class="name bold">{{ plugin.name }}</p>
<p class="description">{{ plugin.description }}</p>
<div v-if="plugin.configSchema.length" class="config">
<div
v-for="field in plugin.configSchema"
class="config-field"
:key="field.key"
>
<!-- Inputs -->
<text-input
v-if="
[
'text',
'email',
'password',
'number',
].includes(field.type)
"
v-model="config.adapters[plugin.id][field.key]"
v-bind="field"
/>
<directory-input
v-else-if="field.type === 'directory'"
v-model="config.adapters[plugin.id][field.key]"
v-bind="field"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import DirectoryInput from '@/components/preferences/DirectoryInput.vue'
import TextInput from '@/components/preferences/TextInput.vue'
import usePlugins from '@/composables/usePlugins'
import useConfig from '@/composables/useConfig'
import { ref, computed, onMounted } from 'vue'
const { plugins, setActivePlugin, testPlugin } = await usePlugins()
const { config, ensureConfig } = useConfig()
onMounted(async () => {
await ensureConfig()
// Make sure every plugin config has defaults set
const normalizeConfig = () => {
if (!config.value.adapters) {
config.value.adapters = {}
}
for (const plugin of plugins.value) {
if (!config.value.adapters[plugin.id]) {
config.value.adapters[plugin.id] = {}
}
for (const field of plugin.configSchema) {
if (config.value.adapters[plugin.id][field.key] === undefined) {
config.value.adapters[plugin.id][field.key] =
field.default ?? ''
}
}
}
}
normalizeConfig()
})
// Track selected plugin
const selectedPluginId = ref(config.value.activeAdapter)
const selectedPlugin = computed(() => {
return plugins.value.find((p) => p.id === selectedPluginId.value)
})
// Validation for parent
const validate = async () => {
const plugin = selectedPlugin.value
const adapterConfig = config.value.adapters[plugin.id] || {}
if (plugin && plugin.configSchema.length) {
// Check required fields
for (const field of plugin.configSchema) {
if (field.required && !adapterConfig[field.key]) {
throw new Error(
`Please fill in all required fields for ${plugin.name}`,
)
}
}
// Test connection
// const testResult = await testPlugin(plugin.id, adapterConfig)
// console.log(testResult)
// if (!testResult) {
// validationError.value = `Failed to connect to ${plugin.name}`
// }
}
await setActivePlugin(selectedPluginId.value, adapterConfig)
}
defineExpose({ validate })
</script>
<style lang="scss">
.preferences-storage {
.plugin {
display: flex;
margin-bottom: 16px;
}
input[type='radio'] {
display: block;
flex-shrink: 0;
width: 10px;
height: 10px;
margin-right: 10px;
border: 1px solid var(--theme-fg);
cursor: pointer;
&:checked {
background-color: var(--theme-fg);
}
}
.info {
.description {
color: var(--grey-100);
margin-top: 6px;
}
}
.config {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 16px;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="preferences-text-input">
<label :for="key"> {{ label }}{{ required ? '*' : '' }} </label>
<input
v-model="model"
:id="key"
:type="type"
:placeholder="default"
:required="required"
/>
</div>
</template>
<script setup>
const props = defineProps({
label: String,
key: String,
required: Boolean,
type: String,
default: String,
})
const model = defineModel()
</script>
<style lang="scss">
.preferences-text-input {
label {
display: block;
margin-bottom: 6px;
}
input {
width: 100%;
border: 1px solid var(--grey-100);
border-radius: 0.2em;
padding: 0.2em 0.5em;
}
}
</style>

View File

@@ -1,7 +1,7 @@
import { useEnvironment } from '@/composables/useEnvironment'
import { initializeCore } from '@core/index.js'
import supabasePlugin from '@takerofnotes/plugin-supabase'
import browserPlugin from '@takerofnotes/plugin-browser'
import supabasePlugin from '@takerofnotes/plugin-supabase'
export const initCore = async (app) => {
const environment = useEnvironment()
@@ -10,7 +10,7 @@ export const initCore = async (app) => {
const runtime = environment === 'electron' ? 'electron-renderer' : 'web'
// Plugins that are valid for web (electron uses IPC)
const plugins = [supabasePlugin, browserPlugin]
const plugins = [browserPlugin, supabasePlugin]
const core = await initializeCore(runtime, {
plugins,

View File

@@ -1,43 +1,18 @@
<template>
<main class="preferences layout-block">
<h1 class="mono">Storage Plugin</h1>
<h1 class="title">Preferences</h1>
<div v-for="plugin in plugins" class="plugin" :key="plugin.id">
<input
v-model="selectedPluginId"
name="plugins"
type="radio"
:id="plugin.id"
:value="plugin.id"
/>
<div class="info">
<p class="name bold">{{ plugin.name }}</p>
<p class="description">{{ plugin.description }}</p>
<div class="sections">
<preferences-encryption ref="encryption" />
<div v-if="plugin.configSchema.length" class="config">
<div
v-for="field in plugin.configSchema"
class="config-field"
:key="field.key"
>
<label :for="field.key">
{{ field.label }} {{ field.required ? '*' : '' }}
</label>
<input
v-model="config.adapters[plugin.id][field.key]"
:id="field.key"
:type="field.type"
:placeholder="field.default"
:required="field.required"
/>
</div>
</div>
</div>
<suspense @resolve="ready = true">
<preferences-storage ref="storage" />
</suspense>
</div>
<p v-if="validationError" class="error">{{ validationError }}</p>
<button @click="save" class="save-btn">
<button @click="save" class="save-btn" :disabled="!ready">
<svg-spinner v-if="saving" />
<span v-else-if="saved">Saved</span>
<span v-else>Save</span>
@@ -46,74 +21,39 @@
</template>
<script setup>
import PreferencesEncryption from '@/components/preferences/Encryption.vue'
import PreferencesStorage from '@/components/preferences/Storage.vue'
import SvgSpinner from '@/components/svg/Spinner.vue'
import usePlugins from '@/composables/usePlugins'
import useConfig from '@/composables/useConfig'
import { ref, computed } from 'vue'
const { plugins, setActivePlugin, testPlugin } = await usePlugins()
const { config, ensureConfig } = useConfig()
await ensureConfig()
const normalizeConfig = () => {
if (!config.value.adapters) {
config.value.adapters = {}
}
for (const plugin of plugins.value) {
if (!config.value.adapters[plugin.id]) {
config.value.adapters[plugin.id] = {}
}
for (const field of plugin.configSchema) {
if (config.value.adapters[plugin.id][field.key] === undefined) {
config.value.adapters[plugin.id][field.key] =
field.default ?? ''
}
}
}
}
normalizeConfig()
const selectedPluginId = ref(config.value.activeAdapter)
const validationError = ref('')
const selectedPlugin = computed(() => {
return plugins.value.find((p) => p.id === selectedPluginId.value)
})
import { ref } from 'vue'
const ready = ref(false)
const saving = ref(false)
const saved = ref(false)
const validationError = ref('')
// Sections
const storage = ref()
const encryption = ref()
const save = async () => {
saving.value = true
saved.value = false
validationError.value = ''
const plugin = selectedPlugin.value
const adapterConfig = config.value.adapters[plugin.id] || {}
try {
await storage.value.validate()
await encryption.value.validate()
if (plugin && plugin.configSchema.length) {
// Check required fields
for (const field of plugin.configSchema) {
if (field.required && !adapterConfig[field.key]) {
validationError.value = `Please fill in all required fields for ${plugin.name}`
return
}
}
// Test connection
// const testResult = await testPlugin(plugin.id, adapterConfig)
// console.log(testResult)
// if (!testResult) {
// validationError.value = `Failed to connect to ${plugin.name}`
// }
saved.value = true
setTimeout(() => {
saved.value = false
}, 2000)
} catch (error) {
console.error(error.message)
validationError.value = error.message
} finally {
saving.value = false
}
await setActivePlugin(selectedPluginId.value, adapterConfig)
saving.value = false
saved.value = true
setTimeout(() => {
saved.value = false
}, 2000)
}
</script>
@@ -122,55 +62,18 @@ const save = async () => {
padding-top: var(--nav-height);
padding-bottom: 60px;
h1 {
.title {
margin-bottom: 25px;
}
.sections {
display: flex;
flex-direction: column;
gap: 30px;
}
.section-title {
margin-bottom: 20px;
}
.plugin {
display: flex;
margin-bottom: 16px;
}
input[type='radio'] {
display: block;
flex-shrink: 0;
width: 10px;
height: 10px;
margin-right: 10px;
border: 1px solid var(--theme-fg);
cursor: pointer;
&:checked {
background-color: var(--theme-fg);
}
}
.info {
.description {
color: var(--grey-100);
margin-top: 6px;
}
}
.config {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 16px;
}
.config-field {
display: flex;
flex-direction: column;
gap: 4px;
input {
width: 100%;
border: 1px solid var(--grey-100);
border-radius: 0.2em;
padding: 0.2em 0.5em;
}
}
.error {
color: red;
margin-top: 16px;
@@ -193,6 +96,10 @@ const save = async () => {
&:hover {
color: var(--theme-accent);
}
&:disabled {
opacity: 0.5;
cursor: default;
}
}
}
</style>