7 Commits

Author SHA1 Message Date
nicwands
4feb6a880c Merge branch 'main' of https://github.com/nicwands/takerofnotes-app
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-12 14:44:36 -04:00
nicwands
45a6952c60 update light theme 2026-03-12 14:44:31 -04:00
nicwands
85c6c44393 fix config bugs 2026-03-12 14:42:51 -04:00
nicwands
c93fc2cc58 Note moving
Some checks failed
Build Electron App / build (macos-latest, build:mac) (push) Has been cancelled
Build Electron App / build (ubuntu-latest, build:linux) (push) Has been cancelled
Build Electron App / build (windows-latest, build:win) (push) Has been cancelled
2026-03-12 13:25:56 -04:00
nicwands
93edf204ce instructions content rendering 2026-03-11 13:45:10 -04:00
nicwands
4d04f4f2ff Preferences config WIP 2026-03-11 13:36:10 -04:00
nicwands
99e6761e92 Preferences WIP 2026-03-11 13:05:28 -04:00
38 changed files with 572 additions and 98037 deletions

1
.gitignore vendored
View File

@@ -14,6 +14,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
out
# Editor directories and files # Editor directories and files
.vscode/* .vscode/*

View File

@@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@@ -68,12 +68,14 @@ class Config {
} }
parsed = { parsed = {
...parsed ? parsed : {}, ...parsed ? parsed : {},
activeAdapter: this.defaultPlugin.id activeAdapter: this.defaultPlugin.id,
adapters: {}
}; };
parsed.adapters[this.defaultPlugin.id] = defaultConfig; parsed.adapters[this.defaultPlugin.id] = defaultConfig;
parsed[theme] = "dark";
await this.write(parsed); await this.write(parsed);
} else { } else {
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig); parsed.adapters = this._resolveDefaults(parsed.adapters);
} }
return parsed; return parsed;
} }
@@ -82,7 +84,7 @@ class Config {
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
const resolvedConfig = { const resolvedConfig = {
...configObject, ...configObject,
adapterConfig: this._resolveDefaults(configObject.adapterConfig) adapters: this._resolveDefaults(configObject.adapters)
}; };
await fs.writeFile( await fs.writeFile(
this.configPath, this.configPath,
@@ -91,12 +93,14 @@ class Config {
); );
} }
} }
const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 };
const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 };
const preloadPath = join(__dirname, "../preload/index.mjs"); const preloadPath = join(__dirname, "../preload/index.mjs");
const rendererPath = join(__dirname, "../renderer/index.html"); const rendererPath = join(__dirname, "../renderer/index.html");
function createWindow() { function createWindow() {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 354, width: DEFAULT_WINDOW_SIZE.width,
height: 549, height: DEFAULT_WINDOW_SIZE.height,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
@@ -119,8 +123,8 @@ function createWindow() {
} }
function createNoteWindow(noteId) { function createNoteWindow(noteId) {
const noteWindow = new BrowserWindow({ const noteWindow = new BrowserWindow({
width: 354, width: DEFAULT_WINDOW_SIZE.width,
height: 549, height: DEFAULT_WINDOW_SIZE.height,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload: preloadPath, preload: preloadPath,
@@ -143,6 +147,11 @@ app.whenReady().then(async () => {
ipcMain.on("open-note-window", (_, noteId) => { ipcMain.on("open-note-window", (_, noteId) => {
createNoteWindow(noteId); createNoteWindow(noteId);
}); });
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data);
});
};
const registry = new PluginRegistry(); const registry = new PluginRegistry();
registry.register(filesystemPlugin); registry.register(filesystemPlugin);
registry.register(supabasePlugin); registry.register(supabasePlugin);
@@ -162,6 +171,7 @@ app.whenReady().then(async () => {
} }
return await adapter[method](...args); return await adapter[method](...args);
}); });
broadcastNoteChange("plugin-changed", pluginId);
return true; return true;
}; };
await setActivePlugin(initialConfig.activeAdapter); await setActivePlugin(initialConfig.activeAdapter);
@@ -177,14 +187,29 @@ app.whenReady().then(async () => {
ipcMain.handle("setActivePlugin", async (_, pluginId) => { ipcMain.handle("setActivePlugin", async (_, pluginId) => {
return await setActivePlugin(pluginId); return await setActivePlugin(pluginId);
}); });
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data);
});
};
ipcMain.on("note-changed", (_, event, data) => { ipcMain.on("note-changed", (_, event, data) => {
broadcastNoteChange(event, data); broadcastNoteChange(event, data);
}); });
ipcMain.handle("move-opened", (_) => {
const activeWindow = BrowserWindow.getFocusedWindow();
const windowSize = activeWindow.getSize();
if (windowSize[0] < DEFAULT_MOVE_WINDOW_SIZE.width) {
activeWindow.setSize(
DEFAULT_MOVE_WINDOW_SIZE.width,
DEFAULT_MOVE_WINDOW_SIZE.height
);
}
});
ipcMain.handle("move-closed", (_) => {
const activeWindow = BrowserWindow.getFocusedWindow();
const windowSize = activeWindow.getSize();
if (windowSize[0] === 708) {
activeWindow.setSize(
DEFAULT_WINDOW_SIZE.width,
DEFAULT_WINDOW_SIZE.height
);
}
});
electronApp.setAppUserModelId("com.electron"); electronApp.setAppUserModelId("com.electron");
app.on("browser-window-created", (_, window) => { app.on("browser-window-created", (_, window) => {
optimizer.watchWindowShortcuts(window); optimizer.watchWindowShortcuts(window);

View File

@@ -16,8 +16,17 @@ const api = {
onNoteDeleted: (callback) => { onNoteDeleted: (callback) => {
ipcRenderer.on("note-deleted", (_, data) => callback(data)); ipcRenderer.on("note-deleted", (_, data) => callback(data));
}, },
onPluginChanged: (callback) => {
ipcRenderer.on("plugin-changed", (_, data) => callback(data));
},
notifyNoteChanged: (event, data) => { notifyNoteChanged: (event, data) => {
ipcRenderer.send("note-changed", event, data); ipcRenderer.send("note-changed", event, data);
},
moveOpened: () => {
ipcRenderer.invoke("move-opened");
},
moveClosed: () => {
ipcRenderer.invoke("move-closed");
} }
}; };
const adapter = { const adapter = {

View File

@@ -1,826 +0,0 @@
@charset "UTF-8";
:root {
--black: #181818;
--white: #D5D5D5;
--grey-100: #747474;
--green: #87FF5B;
--blue: #5B92FF;
}
:root .theme-dark {
--theme-bg: #181818;
--theme-fg: #D5D5D5;
--theme-accent: #87FF5B;
--theme-link: #5B92FF;
}
:root .theme-light {
--theme-bg: #D5D5D5;
--theme-fg: #181818;
--theme-accent: #87FF5B;
--theme-link: #5B92FF;
}
:root {
--ease-in-quad: cubic-bezier(0.55, 0.085, 0.68, 0.53);
--ease-in-cubic: cubic-bezier(0.55, 0.055, 0.675, 0.19);
--ease-in-quart: cubic-bezier(0.895, 0.03, 0.685, 0.22);
--ease-in-quint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
--ease-in-expo: cubic-bezier(0.95, 0.05, 0.795, 0.035);
--ease-in-circ: cubic-bezier(0.6, 0.04, 0.98, 0.335);
--ease-out-quad: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-out-cubic: cubic-bezier(0.215, 0.61, 0.355, 1);
--ease-out-quart: cubic-bezier(0.165, 0.84, 0.44, 1);
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
--ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
--ease-out-circ: cubic-bezier(0.075, 0.82, 0.165, 1);
--ease-in-out-quad: cubic-bezier(0.455, 0.03, 0.515, 0.955);
--ease-in-out-cubic: cubic-bezier(0.645, 0.045, 0.355, 1);
--ease-in-out-quart: cubic-bezier(0.77, 0, 0.175, 1);
--ease-in-out-quint: cubic-bezier(0.86, 0, 0.07, 1);
--ease-in-out-expo: cubic-bezier(1, 0, 0, 1);
--ease-in-out-circ: cubic-bezier(0.785, 0.135, 0.15, 0.86);
--ease-custom: cubic-bezier(0.315, 0.365, 0.23, 0.985);
}
/***
The new CSS reset - version 1.9 (last updated 19.6.2023)
GitHub page: https://github.com/elad2412/the-new-css-reset
***/
/*
Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property
- The "symbol *" part is to solve Firefox SVG sprite bug
- The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36)
*/
*:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) {
all: unset;
display: revert;
}
/* Preferred box-sizing value */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Reapply the pointer cursor for anchor tags */
a,
button {
cursor: revert;
}
/* For images to not be able to exceed their container */
img {
max-inline-size: 100%;
max-block-size: 100%;
}
/* removes spacing between cells in tables */
table {
border-collapse: collapse;
}
/* Safari - solving issue when using user-select:none on the <body> text input doesn't working */
input,
textarea {
-webkit-user-select: auto;
}
/* revert the 'white-space' property for textarea elements on Safari */
textarea {
white-space: revert;
}
/* minimum style to allow to style meter element */
meter {
-webkit-appearance: revert;
appearance: revert;
}
/* preformatted text - use only for this feature */
:where(pre) {
all: revert;
}
/* reset default text opacity of input placeholder */
::placeholder {
color: unset;
}
/* remove default dot (•) sign */
::marker {
content: initial;
}
/* fix the feature of 'hidden' attribute.
display:revert; revert to element instead of attribute */
:where([hidden]) {
display: none;
}
/* revert for bug in Chromium browsers
- fix for the content editable attribute will work properly.
- webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element*/
:where([contenteditable]:not([contenteditable=false])) {
-moz-user-modify: read-write;
-webkit-user-modify: read-write;
overflow-wrap: break-word;
-webkit-line-break: after-white-space;
-webkit-user-select: auto;
}
/* apply back the draggable feature - exist only in Chromium and Safari */
:where([draggable=true]) {
-webkit-user-drag: element;
}
/* Revert Modal native behavior */
:where(dialog:modal) {
all: revert;
}
.menu {
z-index: 10;
}
.full-width {
width: 100vw;
position: relative;
left: 50%;
right: 50%;
margin-left: -50vw;
margin-right: -50vw;
}
.overflow-hidden {
overflow: hidden;
}
.relative {
position: relative;
}
html:not(.has-scroll-smooth) .hide-on-native-scroll {
display: none;
}
/*
Font Weights:
100 - Thin
200 - Extra Light (Ultra Light)
300 - Light
400 - Normal
500 - Medium
600 - Semi Bold (Demi Bold)
700 - Bold
800 - Extra Bold (Ultra Bold)
900 - Black (Heavy)
*/
/* Leibniz Fraktur */
@font-face {
font-family: "Leibniz Fraktur";
font-style: normal;
font-weight: 400;
src: url("./neuefraktur-A4S1ACH2.woff2") format("woff2"), url("./neuefraktur-CwjUIZ0G.woff") format("woff");
}
/* Geist Mono */
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-weight: 700;
src: url("./geist-mono-bold-CTLtpKvJ.woff2") format("woff2"), url("./geist-mono-bold-Bz_UliG4.woff") format("woff");
}
@font-face {
font-family: "Geist Mono";
font-style: normal;
font-weight: 400;
src: url("./geist-mono-BzrJhchg.woff2") format("woff2"), url("./geist-mono-OFKGen7b.woff") format("woff");
}
:root {
--font-display: 'Leibniz Fraktur', serif;
--font-mono: 'Geist Mono', monospace;
}
:root {
--layout-column-count: 6;
--layout-column-gap: 10px;
--layout-margin: 20px;
--layout-width: calc(100vw - (2 * var(--layout-margin)));
--layout-column-width: calc(
(
var(--layout-width) -
(
(var(--layout-column-count) - 1) *
var(--layout-column-gap)
)
) /
var(--layout-column-count)
);
}
.layout-block, .layout-grid {
padding-left: var(--layout-margin);
padding-right: var(--layout-margin);
width: 100%;
}
.layout-grid {
display: grid;
grid-template-columns: repeat(var(--layout-column-count), minmax(0, 1fr));
grid-gap: var(--layout-column-gap);
}
html,
html * {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
html::-webkit-scrollbar,
html *::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 400ms;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.quick-fade-enter-active,
.quick-fade-leave-active {
transition: opacity 100ms;
}
.quick-fade-enter-from,
.quick-fade-leave-to {
opacity: 0;
}
.slow-fade-enter-active,
.slow-fade-leave-active {
transition: opacity 600ms;
}
.slow-fade-enter-from,
.slow-fade-leave-to {
opacity: 0;
}
.slide-up-enter-active,
.slide-up-leave-active {
transition: transform 400ms var(--ease-out-quad);
}
.slide-up-enter-from,
.slide-up-leave-to {
transform: translateY(-100%);
}
.slide-left-enter-active,
.slide-left-leave-active {
transition: transform 400ms var(--ease-out-quad);
}
.slide-left-enter-from,
.slide-left-leave-to {
transform: translateX(-100%);
}
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
background: var(--black);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
}
.h1,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.3;
font-size: 30px;
}
.h1.mono,
h1.mono,
h2.mono,
h3.mono,
h4.mono,
h5.mono,
h6.mono {
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 22px;
}
.p,
p,
a,
button,
input,
pre,
span {
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 12px;
}
.bold {
font-weight: 700;
}
#app {
min-height: 100vh;
}
button {
cursor: pointer;
}
::selection {
color: var(--theme-bg);
background: var(--theme-fg);
}
::-moz-selection {
color: var(--theme-bg);
background: var(--theme-fg);
}.theme-switcher {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.theme-switcher button {
background: var(--theme-bg);
display: block;
width: 14px;
height: 14px;
border-radius: 50%;
border: none;
cursor: pointer;
}
.theme-switcher button.active {
border: 1px solid var(--theme-fg);
}
.theme-switcher:hover {
color: var(--theme-fg) !important;
}.nav {
display: flex;
align-items: center;
gap: 10px;
padding-top: 9px;
color: var(--grey-100);
}.menu {
position: fixed;
top: 0;
left: 0;
right: 0;
background: var(--theme-bg);
border-bottom: 1px solid var(--grey-100);
}
.menu .menu-wrap {
display: flex;
flex-direction: column;
padding-top: 3px;
padding-bottom: 10px;
}
.menu .menu-wrap .menu-item {
padding: 16px 0;
text-align: center;
}
.menu .menu-wrap .menu-item:not(:last-child) {
border-bottom: 1px dashed currentColor;
}
.menu .menu-wrap .menu-item:hover {
color: var(--theme-accent);
}
.menu.menu-enter-active, .menu.menu-leave-active {
transition: transform 300ms var(--ease-out-expo);
}
.menu.menu-enter-from, .menu.menu-leave-to {
transform: translateY(-100%);
}.scroll-bar {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 8px;
will-change: transform;
border-left: 1px solid var(--grey-100);
}
.scroll-bar .inner {
height: 100%;
position: relative;
}
.scroll-bar .inner .handle {
width: 100%;
height: 388px;
background: var(--grey-100);
border-radius: 20px;
position: absolute;
right: 0;
will-change: transform;
}.container {
min-height: calc(100 * var(--vh));
max-width: 100vw;
overflow-x: clip;
background: var(--theme-bg);
color: var(--theme-fg);
transition: opacity 400ms;
}
.container:not(.fonts-ready) {
opacity: 0;
}.category-row {
display: grid;
grid-template-columns: 26px 1fr auto;
align-items: flex-start;
width: 100%;
position: relative;
padding: 8px 0 6px;
border-bottom: 1px dashed currentColor;
cursor: pointer;
}
.category-row .index {
margin-top: 19px;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 12px;
}
.category-row .title {
display: block;
width: 100%;
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.category-row .category-input {
display: block;
width: 100%;
font-family: var(--font-display);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.3;
font-size: 30px;
}
.category-row .category-input:focus {
outline: none;
}
.category-row .edit-button,
.category-row .save-button {
color: var(--grey-100);
cursor: pointer;
padding-right: 0.5em;
padding-left: 0.5em;
margin-top: 1.5em;
}
.category-row .edit-button {
opacity: 0;
pointer-events: none;
}
.category-row.editable:hover .edit-button {
opacity: 1;
pointer-events: auto;
}
.category-row.router-link-exact-active, .category-row.editable {
cursor: default;
}
.category-row:hover:not(.router-link-exact-active):not(.editable) {
color: var(--theme-accent);
}.note-row {
grid-template-columns: auto 1fr;
display: grid;
width: 100%;
gap: 20px;
cursor: pointer;
}
.note-row .title {
width: calc(100% - 43.2px);
position: relative;
}
.note-row .title::after {
content: "(open)";
position: absolute;
bottom: 0;
right: 0;
transform: translateX(100%);
font-weight: 700;
opacity: 0;
}
.note-row:hover {
color: var(--theme-accent);
}
.note-row:hover .title::after {
opacity: 1;
}main.directory {
padding-top: 18px;
padding-bottom: 30px;
}
main.directory .label {
text-transform: uppercase;
margin: 17px 0 24px;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 12px;
}
main.directory .notes {
display: flex;
flex-direction: column;
gap: 14px;
}.note-menu {
display: flex;
gap: 5px;
border: 1px solid var(--grey-100);
color: var(--grey-100);
border-radius: 0.2em;
background: var(--theme-bg);
}
.note-menu button {
cursor: pointer;
padding: 0.2em;
border-radius: 0.2em;
}
.note-menu button:hover {
background: var(--grey-100);
color: var(--theme-bg);
}
.note-menu button.active {
background: var(--theme-fg);
color: var(--theme-bg);
}
.note-editor h1 {
font-weight: 700 !important;
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 12px;
}
.note-editor h1:first-child {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 400 !important;
}
.note-editor h1:first-child:first-child::first-letter {
font-family: var(--font-display);
font-size: 42px;
}
.note-editor h1.is-editor-empty:first-child::before {
color: var(--grey-100);
content: attr(data-placeholder);
pointer-events: none;
font-family: var(--font-mono);
font-size: 12px;
font-weight: 400 !important;
}
.note-editor h1.is-editor-empty:first-child::before:first-child::first-letter {
font-family: var(--font-display);
font-size: 42px;
}
.note-editor p strong {
font-weight: 700;
}
.note-editor p em {
font-style: italic;
}
.note-editor hr {
border: 1px dashed currentColor;
}
.note-editor ul {
list-style-type: disc;
}
.note-editor ul li {
display: list-item;
margin-left: 1em;
}
.note-editor ul li *:not(:last-child) {
margin-bottom: 0.5em;
}
.note-editor ol {
list-style-type: decimal;
}
.note-editor ol li {
display: list-item;
margin-left: 1.5em;
}
.note-editor ol li::marker {
font-family: var(--font-mono);
font-weight: 400;
line-height: 1;
font-size: 12px;
}
.note-editor li:not(:last-child) {
margin-bottom: 0.5em;
}
.note-editor a {
color: var(--theme-link);
cursor: pointer;
}
.note-editor code {
border: 1px solid var(--grey-100);
color: var(--theme-accent);
padding: 0 0.2em;
border-radius: 0.2em;
}
.note-editor pre code {
display: block;
color: inherit;
padding: 1em;
/* Code styling */
}
.note-editor pre code .hljs-comment,
.note-editor pre code .hljs-quote {
color: #616161;
}
.note-editor pre code .hljs-variable,
.note-editor pre code .hljs-template-variable,
.note-editor pre code .hljs-attribute,
.note-editor pre code .hljs-tag,
.note-editor pre code .hljs-name,
.note-editor pre code .hljs-regexp,
.note-editor pre code .hljs-link,
.note-editor pre code .hljs-name,
.note-editor pre code .hljs-selector-id,
.note-editor pre code .hljs-selector-class {
color: #f98181;
}
.note-editor pre code .hljs-number,
.note-editor pre code .hljs-meta,
.note-editor pre code .hljs-built_in,
.note-editor pre code .hljs-builtin-name,
.note-editor pre code .hljs-literal,
.note-editor pre code .hljs-type,
.note-editor pre code .hljs-params {
color: #fbbc88;
}
.note-editor pre code .hljs-string,
.note-editor pre code .hljs-symbol,
.note-editor pre code .hljs-bullet {
color: #b9f18d;
}
.note-editor pre code .hljs-title,
.note-editor pre code .hljs-section {
color: #faf594;
}
.note-editor pre code .hljs-keyword,
.note-editor pre code .hljs-selector-tag {
color: #70cff8;
}
.note-editor pre code .hljs-emphasis {
font-style: italic;
}
.note-editor pre code .hljs-strong {
font-weight: 700;
}
.note-editor blockquote {
border-left: 4px solid var(--grey-100);
padding-left: 0.5em;
}
.note-editor s {
position: relative;
}
.note-editor s::after {
content: " ";
display: block;
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: currentColor;
}
.note-editor mark {
background: var(--theme-accent);
color: var(--theme-bg);
padding: 0 0.2em;
}
.note-editor ul[data-type=taskList] {
list-style: none;
margin-left: 0;
padding: 0;
}
.note-editor ul[data-type=taskList] li {
align-items: center;
display: flex;
margin-left: 0;
}
.note-editor ul[data-type=taskList] li > label {
flex: 0 0 auto;
margin-right: 0.5rem;
user-select: none;
margin-bottom: 0;
}
.note-editor ul[data-type=taskList] li > label input {
margin: 0;
}
.note-editor ul[data-type=taskList] li > div {
flex: 1 1 auto;
}
.note-editor ul[data-type=taskList] input[type=checkbox] {
cursor: pointer;
display: block;
width: 1.5em;
height: 1.5em;
border: 1px solid var(--grey-100);
border-radius: 0.2em;
}
.note-editor ul[data-type=taskList] input[type=checkbox]::after {
content: "✓";
font-size: 1.5em;
opacity: 0;
}
.note-editor ul[data-type=taskList] input[type=checkbox]:checked::after {
opacity: 1;
}
.note-editor ul[data-type=taskList] ul[data-type=taskList] {
margin: 0;
}
.note-editor .editor-wrap > div {
display: flex;
flex-direction: column;
gap: 20px;
}
.note-editor .editor-wrap > div:focus {
outline: none;
}main.note {
padding-top: 8px;
padding-bottom: 20px;
}main.category .back {
display: block;
opacity: 0.25;
margin-top: 9px;
}
main.category .category-row {
margin-top: 4px;
}
main.category .notes {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 9px;
}
main.category .new-note {
display: block;
margin: 50px auto 0;
}main.instructions .back-link {
opacity: 0.25;
display: block;
margin-top: 9px;
margin-bottom: 14px;
}main.search .back {
display: block;
opacity: 0.25;
margin-top: 9px;
}
main.search .input-wrap {
margin-top: 19px;
position: relative;
}
main.search .input-wrap input {
display: block;
width: 100%;
position: relative;
padding: 5px 15px 6px;
background: var(--theme-bg);
--clip-start: 16px;
clip-path: polygon(var(--clip-start) 1px, calc(100% - var(--clip-start)) 1px, calc(100% - 1.5px) 50%, calc(100% - var(--clip-start)) calc(100% - 1px), var(--clip-start) calc(100% - 1px), 1.5px 50%);
}
main.search .input-wrap::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--theme-fg);
--clip-start: 15px;
clip-path: polygon(var(--clip-start) 0, calc(100% - var(--clip-start)) 0, 100% 50%, calc(100% - var(--clip-start)) 100%, var(--clip-start) 100%, 0% 50%);
}
main.search .results {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/> />
<script type="module" crossorigin src="./assets/index-CoqDP7Z2.js"></script> <script type="module" crossorigin src="./assets/index-D2TWwJ08.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CVyE7-c9.css"> <link rel="stylesheet" crossorigin href="./assets/index-BFwBEQYI.css">
</head> </head>
<body> <body>

1
package-lock.json generated
View File

@@ -29,6 +29,7 @@
"libsodium-wrappers": "^0.8.2", "libsodium-wrappers": "^0.8.2",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lowlight": "^3.3.0", "lowlight": "^3.3.0",
"markdown-it": "^14.1.1",
"sass": "^1.97.3", "sass": "^1.97.3",
"sass-embedded": "^1.97.3", "sass-embedded": "^1.97.3",
"tempus": "^1.0.0-dev.17", "tempus": "^1.0.0-dev.17",

View File

@@ -45,6 +45,7 @@
"libsodium-wrappers": "^0.8.2", "libsodium-wrappers": "^0.8.2",
"lodash": "^4.17.23", "lodash": "^4.17.23",
"lowlight": "^3.3.0", "lowlight": "^3.3.0",
"markdown-it": "^14.1.1",
"sass": "^1.97.3", "sass": "^1.97.3",
"sass-embedded": "^1.97.3", "sass-embedded": "^1.97.3",
"tempus": "^1.0.0-dev.17", "tempus": "^1.0.0-dev.17",

View File

@@ -50,13 +50,16 @@ export default class Config {
parsed = { parsed = {
...(parsed ? parsed : {}), ...(parsed ? parsed : {}),
activeAdapter: this.defaultPlugin.id, activeAdapter: this.defaultPlugin.id,
adapters: {},
} }
parsed.adapters[this.defaultPlugin.id] = defaultConfig parsed.adapters[this.defaultPlugin.id] = defaultConfig
parsed[theme] = 'dark'
await this.write(parsed) await this.write(parsed)
} else { } else {
// Ensure any "__DEFAULT_USER_DATA__" values are resolved on load // Ensure any "__DEFAULT_USER_DATA__" values are resolved on load
parsed.adapterConfig = this._resolveDefaults(parsed.adapterConfig) parsed.adapters = this._resolveDefaults(parsed.adapters)
} }
return parsed return parsed
@@ -71,7 +74,7 @@ export default class Config {
// Resolve defaults before writing // Resolve defaults before writing
const resolvedConfig = { const resolvedConfig = {
...configObject, ...configObject,
adapterConfig: this._resolveDefaults(configObject.adapterConfig), adapters: this._resolveDefaults(configObject.adapters),
} }
await fs.writeFile( await fs.writeFile(

View File

@@ -7,14 +7,17 @@ import PluginRegistry from './core/PluginRegistry.js'
import Config from './core/Config.js' import Config from './core/Config.js'
import { join } from 'path' import { join } from 'path'
const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 }
const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 }
const preloadPath = join(__dirname, '../preload/index.mjs') const preloadPath = join(__dirname, '../preload/index.mjs')
const rendererPath = join(__dirname, '../renderer/index.html') const rendererPath = join(__dirname, '../renderer/index.html')
// Main window // Main window
function createWindow() { function createWindow() {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 354, width: DEFAULT_WINDOW_SIZE.width,
height: 549, height: DEFAULT_WINDOW_SIZE.height,
show: false, show: false,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
@@ -42,8 +45,8 @@ function createWindow() {
// Open note in new window // Open note in new window
function createNoteWindow(noteId) { function createNoteWindow(noteId) {
const noteWindow = new BrowserWindow({ const noteWindow = new BrowserWindow({
width: 354, width: DEFAULT_WINDOW_SIZE.width,
height: 549, height: DEFAULT_WINDOW_SIZE.height,
autoHideMenuBar: true, autoHideMenuBar: true,
webPreferences: { webPreferences: {
preload: preloadPath, preload: preloadPath,
@@ -70,6 +73,13 @@ app.whenReady().then(async () => {
createNoteWindow(noteId) createNoteWindow(noteId)
}) })
// Broadcast note changes to all windows
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data)
})
}
// Create plugin registry // Create plugin registry
const registry = new PluginRegistry() const registry = new PluginRegistry()
@@ -102,6 +112,8 @@ app.whenReady().then(async () => {
return await adapter[method](...args) return await adapter[method](...args)
}) })
broadcastNoteChange('plugin-changed', pluginId)
return true return true
} }
@@ -124,18 +136,35 @@ app.whenReady().then(async () => {
return await setActivePlugin(pluginId) return await setActivePlugin(pluginId)
}) })
// Broadcast note changes to all windows
const broadcastNoteChange = (event, data) => {
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send(event, data)
})
}
// Handle note change events from renderer // Handle note change events from renderer
ipcMain.on('note-changed', (_, event, data) => { ipcMain.on('note-changed', (_, event, data) => {
broadcastNoteChange(event, data) broadcastNoteChange(event, data)
}) })
// Handle resizing for note "move" functionality
ipcMain.handle('move-opened', (_) => {
const activeWindow = BrowserWindow.getFocusedWindow()
const windowSize = activeWindow.getSize()
if (windowSize[0] < DEFAULT_MOVE_WINDOW_SIZE.width) {
activeWindow.setSize(
DEFAULT_MOVE_WINDOW_SIZE.width,
DEFAULT_MOVE_WINDOW_SIZE.height,
)
}
})
ipcMain.handle('move-closed', (_) => {
const activeWindow = BrowserWindow.getFocusedWindow()
const windowSize = activeWindow.getSize()
if (windowSize[0] === 708) {
activeWindow.setSize(
DEFAULT_WINDOW_SIZE.width,
DEFAULT_WINDOW_SIZE.height,
)
}
})
electronApp.setAppUserModelId('com.electron') electronApp.setAppUserModelId('com.electron')
app.on('browser-window-created', (_, window) => { app.on('browser-window-created', (_, window) => {

View File

@@ -19,9 +19,18 @@ const api = {
onNoteDeleted: (callback) => { onNoteDeleted: (callback) => {
ipcRenderer.on('note-deleted', (_, data) => callback(data)) ipcRenderer.on('note-deleted', (_, data) => callback(data))
}, },
onPluginChanged: (callback) => {
ipcRenderer.on('plugin-changed', (_, data) => callback(data))
},
notifyNoteChanged: (event, data) => { notifyNoteChanged: (event, data) => {
ipcRenderer.send('note-changed', event, data) ipcRenderer.send('note-changed', event, data)
}, },
moveOpened: () => {
ipcRenderer.invoke('move-opened')
},
moveClosed: () => {
ipcRenderer.invoke('move-closed')
},
} }
// Implement adapter API - communicates with plugin adapter in main process // Implement adapter API - communicates with plugin adapter in main process

View File

@@ -2,7 +2,13 @@
<div :class="classes" :style="styles"> <div :class="classes" :style="styles">
<Nav /> <Nav />
<router-view :key="$route.fullPath" /> <Suspense>
<div class="layout-container">
<router-view :key="$route.fullPath" />
<MoveMenu />
</div>
</Suspense>
<Menu /> <Menu />
@@ -16,6 +22,7 @@ import loadFonts from '@fuzzco/font-loader'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import Menu from '@/components/Menu.vue' import Menu from '@/components/Menu.vue'
import Nav from '@/components/Nav.vue' import Nav from '@/components/Nav.vue'
import MoveMenu from '@/components/MoveMenu.vue'
import ScrollBar from '@/components/ScrollBar.vue' import ScrollBar from '@/components/ScrollBar.vue'
import useConfig from '@/composables/useConfig' import useConfig from '@/composables/useConfig'
@@ -65,6 +72,12 @@ const styles = computed(() => ({
color: var(--theme-fg); color: var(--theme-fg);
transition: opacity 400ms; transition: opacity 400ms;
.layout-container {
display: grid;
grid-template-columns: 1fr auto;
min-height: calc(100 * var(--vh));
}
&:not(.fonts-ready) { &:not(.fonts-ready) {
opacity: 0; opacity: 0;
} }

View File

@@ -39,6 +39,7 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: () => false, default: () => false,
}, },
wrapper: String,
}) })
const emit = defineEmits(['edited']) const emit = defineEmits(['edited'])
@@ -70,7 +71,7 @@ const onSave = async () => {
} }
const wrapper = computed(() => { const wrapper = computed(() => {
return props.editable ? 'div' : RouterLink return props.wrapper || (props.editable ? 'div' : RouterLink)
}) })
</script> </script>

View File

@@ -5,15 +5,18 @@
<div class="menu-wrap layout-block"> <div class="menu-wrap layout-block">
<new-note class="menu-item" @noteOpened="closeMenu" /> <new-note class="menu-item" @noteOpened="closeMenu" />
<router-link class="menu-item" to="/category" <router-link class="menu-item" to="/category">
>+ New Capitulum</router-link + New Capitulum
> </router-link>
<theme-switcher class="menu-item" /> <theme-switcher class="menu-item" />
<router-link class="menu-item" to="/instructions" <router-link class="menu-item" to="/instructions">
>Instructio</router-link Instructio
> </router-link>
<button class="menu-item">Import</button> <button class="menu-item">Import</button>
<button class="menu-item">Export</button> <button class="menu-item">Export</button>
<router-link class="menu-item" to="/preferences">
Preferences
</router-link>
</div> </div>
</div> </div>
</transition> </transition>
@@ -61,7 +64,7 @@ const openNewCategory = () => {}
.menu-wrap { .menu-wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 3px; padding-top: 1.2em;
padding-bottom: 10px; padding-bottom: 10px;
.menu-item { .menu-item {

View File

@@ -0,0 +1,57 @@
<template>
<div v-if="open" class="move-menu layout-block">
<template v-for="(category, i) in categories">
<category-row
v-if="category !== fromCategory"
:category="category"
:index="i"
wrapper="button"
@click="onCategoryClick(category)"
:key="category"
/>
</template>
</div>
</template>
<script setup>
import CategoryRow from '@/components/CategoryRow.vue'
import { computed, onBeforeUnmount, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import useNotes from '@/composables/useNotes'
import useState from '@/composables/useState'
import _omit from 'lodash/omit'
const { categories, updateNote } = useNotes()
const route = useRoute()
const router = useRouter()
const open = computed(() => route.query.move !== undefined)
const noteId = computed(() => route.query.move)
const fromCategory = computed(() => route.params.id)
const close = async () => {
await router.push({
query: _omit(route.query, ['move']),
})
await window.api.moveClosed()
}
const onCategoryClick = async (category) => {
if (!category || !noteId.value) return
await updateNote(noteId.value, { category: category })
await close()
}
watch(open, async () => {
if (!open.value) await close()
})
</script>
<style lang="scss">
.move-menu {
width: 50vw;
height: 100%;
border-left: 1px solid var(--grey-100);
}
</style>

View File

@@ -8,7 +8,6 @@
<script setup> <script setup>
import useMenu from '@/composables/useMenu' import useMenu from '@/composables/useMenu'
import { onMounted } from 'vue'
const { menuOpen, closeMenu, openMenu } = useMenu() const { menuOpen, closeMenu, openMenu } = useMenu()
@@ -19,18 +18,11 @@ const toggleMenu = () => {
openMenu() openMenu()
} }
} }
onMounted(() => {
// Initialize menu state or perform any other necessary setup
// Example: Check if the user is logged in and update menu accordingly
// if (isLoggedIn()) {
// openMenu()
// }
})
</script> </script>
<style lang="scss"> <style lang="scss">
.nav { .nav {
position: absolute;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;

View File

@@ -1,12 +1,26 @@
<template> <template>
<button class="note-row" @click="openNote(note.id)"> <div :class="['note-row', { 'move-active': moveActive }]">
<span class="date">{{ formatDate(note.createdAt) }}</span> <span class="date">{{ formatDate(note.createdAt) }}</span>
<span class="title bold">{{ note.title }}</span> <div class="title-actions">
</button> <button class="title bold" @click="openNote(note.id)">
{{ note.title }}
</button>
<button class="action bold" @click="openNote(note.id)">
(open)
</button>
<button class="action bold move" @click="onMoveOpened">
(move)
</button>
</div>
</div>
</template> </template>
<script setup> <script setup>
import useOpenNote from '@/composables/useOpenNote' import useOpenNote from '@/composables/useOpenNote'
import useState from '@/composables/useState'
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'
import { format } from 'fecha' import { format } from 'fecha'
const props = defineProps({ note: Object }) const props = defineProps({ note: Object })
@@ -17,6 +31,21 @@ const formatDate = (date) => {
const d = new Date(date) const d = new Date(date)
return format(d, 'MM/DD/YYYY') return format(d, 'MM/DD/YYYY')
} }
// Moving
const route = useRoute()
const router = useRouter()
const onMoveOpened = async () => {
await window.api.moveOpened()
await router.push({
query: {
...route.query,
move: props.note.id,
},
})
console.log(route.query)
}
const moveActive = computed(() => route.query.move === props.note.id)
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -25,28 +54,33 @@ const formatDate = (date) => {
display: grid; display: grid;
width: 100%; width: 100%;
gap: 20px; gap: 20px;
cursor: pointer;
.title { .title-actions {
width: calc(100% - 43.2px); display: grid;
position: relative; grid-template-columns: 1fr auto auto;
align-items: flex-start;
gap: 2px;
&::after { .action {
content: '(open)';
position: absolute;
bottom: 0;
right: 0;
transform: translateX(100%);
font-weight: 700;
opacity: 0; opacity: 0;
&:not(:hover) {
color: var(--grey-100);
}
} }
} }
&:hover { &:hover,
&.move-active {
color: var(--theme-accent); color: var(--theme-accent);
.title::after { .title-actions .action {
opacity: 1; opacity: 1;
} }
} }
&.move-active {
.title-actions .move {
color: var(--theme-accent);
}
}
} }
</style> </style>

View File

@@ -116,7 +116,8 @@ onBeforeUnmount(() => {
font-weight: 700; font-weight: 700;
} }
p em { p em {
font-style: italic; /* font-style: italic; */
color: var(--grey-100);
} }
hr { hr {
border: 1px dashed currentColor; border: 1px dashed currentColor;

View File

@@ -0,0 +1,32 @@
<template>
<svg
class="svg-spinner"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="margin: auto; display: block"
width="18px"
height="18px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<circle
cx="50"
cy="50"
r="40"
stroke-width="4"
stroke="currentColor"
stroke-dasharray="62 62"
fill="none"
stroke-linecap="round"
>
<animateTransform
attributeName="transform"
type="rotate"
repeatCount="indefinite"
dur="1s"
keyTimes="0;1"
values="0 50 50;360 50 50"
></animateTransform>
</circle>
</svg>
</template>

View File

@@ -1,10 +0,0 @@
<template>
<svg
class="svg-icon-hr"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 11H4V13H2V11ZM6 11H18V13H6V11ZM20 11H22V13H20V11Z"></path>
</svg>
</template>

View File

@@ -29,8 +29,14 @@ export default () => {
return configPromise return configPromise
} }
const refreshConfig = async () => {
config.value = await window.api.getConfig()
configResolve()
}
return { return {
config, config,
ensureConfig, ensureConfig,
refreshConfig,
} }
} }

View File

@@ -24,6 +24,12 @@ const setupListeners = () => {
window.api.onNoteCreated(updateCacheCount) window.api.onNoteCreated(updateCacheCount)
window.api.onNoteUpdated(updateCacheCount) window.api.onNoteUpdated(updateCacheCount)
window.api.onPluginChanged(async () => {
const api = await getNotesAPI()
await api.init()
notesChangeCount.value++
})
// Todo update cache // Todo update cache
window.api.onNoteDeleted(() => { window.api.onNoteDeleted(() => {

View File

@@ -1,14 +1,16 @@
import { ref, onMounted } from 'vue' import { ref } from 'vue'
import useConfig from './useConfig'
export default async () => {
const { refreshConfig } = useConfig()
export default () => {
const plugins = ref([]) const plugins = ref([])
onMounted(async () => { plugins.value = await window.api.listPlugins()
plugins.value = await window.api.listPlugins()
})
const setActivePlugin = async (pluginId) => { const setActivePlugin = async (pluginId) => {
await window.api.setActivePlugin(pluginId) await window.api.setActivePlugin(pluginId)
await refreshConfig()
} }
return { return {

View File

@@ -0,0 +1,8 @@
import { createGlobalState } from '@vueuse/core'
import { ref } from 'vue'
export default createGlobalState(() => {
const moveMenuOpen = ref(false)
return { moveMenuOpen }
})

View File

@@ -0,0 +1,28 @@
Medieval Translation
Nota = Note\
Capitulum = Category\
Intructio = Instructions\
Tabula = Index/Overview
\*This can be disabled via toolbar
---
Program Key Commands
cmd + s = save\
cmd + t = new capitulum\
cmd + n = new nota\
cmd + x = close window\
dbl click = change name / open nota\
paste hyperlink twice = activated url
---
Text Markdowns
cmd + b = Bold\
cmd + u = underline\
--- = ---------- (ruled line break)\
/_text_/ = Desaturated text

View File

@@ -110,6 +110,7 @@ export default class NotesAPI {
async init() { async init() {
await this._initSodium() await this._initSodium()
await this.adapter.init() await this.adapter.init()
this.notesCache.clear()
const encryptedNotes = await this.adapter.getAll() const encryptedNotes = await this.adapter.getAll()

View File

@@ -3,7 +3,6 @@ import IpcAdapter from '@/libs/core/IpcAdapter.js'
import useConfig from '@/composables/useConfig.js' import useConfig from '@/composables/useConfig.js'
// Singleton pattern to make sure only one instance of NotesAPI exists // Singleton pattern to make sure only one instance of NotesAPI exists
let notesAPI = null let notesAPI = null
let initPromise = null let initPromise = null

View File

@@ -4,6 +4,7 @@ const colors = {
'grey-100': '#747474', 'grey-100': '#747474',
green: '#87FF5B', green: '#87FF5B',
blue: '#5B92FF', blue: '#5B92FF',
purple: '#94079E',
} }
const themes = { const themes = {
@@ -16,7 +17,7 @@ const themes = {
light: { light: {
bg: colors.white, bg: colors.white,
fg: colors.black, fg: colors.black,
accent: colors.green, accent: colors.purple,
link: colors.blue, link: colors.blue,
}, },
} }

View File

@@ -6,6 +6,7 @@ import CreateCategory from '@/views/CreateCategory.vue'
import Category from '@/views/Category.vue' import Category from '@/views/Category.vue'
import Instructions from '@/views/Instructions.vue' import Instructions from '@/views/Instructions.vue'
import Search from '@/views/Search.vue' import Search from '@/views/Search.vue'
import Preferences from '@/views/Preferences.vue'
const routes = [ const routes = [
{ path: '/', name: 'directory', component: Directory }, { path: '/', name: 'directory', component: Directory },
@@ -14,6 +15,7 @@ const routes = [
{ path: '/category/:id', name: 'category', component: Category }, { path: '/category/:id', name: 'category', component: Category },
{ path: '/instructions', name: 'instructions', component: Instructions }, { path: '/instructions', name: 'instructions', component: Instructions },
{ path: '/search', name: 'search', component: Search }, { path: '/search', name: 'search', component: Search },
{ path: '/preferences', name: 'preferences', component: Preferences },
] ]
export const router = createRouter({ export const router = createRouter({

View File

@@ -47,7 +47,9 @@ a,
button, button,
input, input,
pre, pre,
span { span,
label,
li {
@include p; @include p;
} }
.bold { .bold {

View File

@@ -61,6 +61,8 @@ const categoryIndex = computed(() => {
<style lang="scss"> <style lang="scss">
main.category { main.category {
padding-top: 1.24em;
.back { .back {
display: block; display: block;
opacity: 0.25; opacity: 0.25;

View File

@@ -14,3 +14,9 @@ const onCategoryEdited = (name) => {
router.push({ name: 'category', params: { id: name } }) router.push({ name: 'category', params: { id: name } })
} }
</script> </script>
<style lang="scss">
.create-category {
padding-top: 1.2em;
}
</style>

View File

@@ -7,22 +7,11 @@
:key="category" :key="category"
/> />
<h2 class="label">Summarium</h2> <h2 v-if="notes?.length" class="label">Summarium</h2>
<div class="notes"> <div class="notes">
<note-row v-for="note in notes" :note="note" :key="note.id" /> <note-row v-for="note in notes" :note="note" :key="note.id" />
</div> </div>
<div v-for="plugin in plugins" :key="plugin.id">
<input
v-model="activePlugin"
type="radio"
name="plugins"
:value="plugin.id"
:id="plugin.id"
/>
<label :for="plugin.id">{{ plugin.name }}</label>
</div>
</main> </main>
</template> </template>
@@ -38,9 +27,6 @@ const { categories, loadCategories, loadCategoryNotes, notesChangeCount } =
useNotes() useNotes()
const { config } = useConfig() const { config } = useConfig()
const { plugins, setActivePlugin } = usePlugins()
const activePlugin = ref(config.value?.activeAdapter)
const notes = ref() const notes = ref()
@@ -56,29 +42,13 @@ onMounted(async () => {
watch(notesChangeCount, async () => { watch(notesChangeCount, async () => {
await refreshNotes() await refreshNotes()
}) })
watch(activePlugin, async (pluginId) => {
await setActivePlugin(pluginId)
await refreshNotes()
})
</script> </script>
<style lang="scss"> <style lang="scss">
main.directory { main.directory {
padding-top: 18px; padding-top: 26px;
padding-bottom: 30px; padding-bottom: 30px;
input[type='radio'] {
display: block;
width: 10px;
height: 10px;
margin-right: 10px;
border: 1px solid white;
&:checked {
background-color: white;
}
}
.label { .label {
text-transform: uppercase; text-transform: uppercase;
margin: 17px 0 24px; margin: 17px 0 24px;

View File

@@ -2,73 +2,37 @@
<main class="instructions layout-block"> <main class="instructions layout-block">
<router-link class="back-link" to="/"><- Go Back</router-link> <router-link class="back-link" to="/"><- Go Back</router-link>
<p> <div class="content" v-html="renderedContent" />
Medieval Translation Nota = Note Capitulum = Category Intructio =
Instructions Tabula = Index/Overview *This can be disabled via
toolbar -------------------------------------------- Program Key
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
cmd + x = close window dbl click = change name / open nota paste
hyperlink twice = activated url
-------------------------------------------- Text Markdowns cmd + b
= Bold cmd + u = underline --- = ---------- (ruled line break)
/*text*/ = Desaturated text
</p>
<p>
Medieval Translation Nota = Note Capitulum = Category Intructio =
Instructions Tabula = Index/Overview *This can be disabled via
toolbar -------------------------------------------- Program Key
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
cmd + x = close window dbl click = change name / open nota paste
hyperlink twice = activated url
-------------------------------------------- Text Markdowns cmd + b
= Bold cmd + u = underline --- = ---------- (ruled line break)
/*text*/ = Desaturated text
</p>
<p>
Medieval Translation Nota = Note Capitulum = Category Intructio =
Instructions Tabula = Index/Overview *This can be disabled via
toolbar -------------------------------------------- Program Key
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
cmd + x = close window dbl click = change name / open nota paste
hyperlink twice = activated url
-------------------------------------------- Text Markdowns cmd + b
= Bold cmd + u = underline --- = ---------- (ruled line break)
/*text*/ = Desaturated text
</p>
<p>
Medieval Translation Nota = Note Capitulum = Category Intructio =
Instructions Tabula = Index/Overview *This can be disabled via
toolbar -------------------------------------------- Program Key
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
cmd + x = close window dbl click = change name / open nota paste
hyperlink twice = activated url
-------------------------------------------- Text Markdowns cmd + b
= Bold cmd + u = underline --- = ---------- (ruled line break)
/*text*/ = Desaturated text
</p>
<p>
Medieval Translation Nota = Note Capitulum = Category Intructio =
Instructions Tabula = Index/Overview *This can be disabled via
toolbar -------------------------------------------- Program Key
Commands cmd + s = save cmd + t = new capitulum cmd + n = new nota
cmd + x = close window dbl click = change name / open nota paste
hyperlink twice = activated url
-------------------------------------------- Text Markdowns cmd + b
= Bold cmd + u = underline --- = ---------- (ruled line break)
/*text*/ = Desaturated text
</p>
</main> </main>
</template> </template>
<script setup></script> <script setup>
import content from '@/content/instructions.md?raw'
import MarkdownIt from 'markdown-it'
const md = new MarkdownIt()
const renderedContent = md.render(content)
</script>
<style lang="scss"> <style lang="scss">
main.instructions { main.instructions {
padding-top: 1.2em;
.back-link { .back-link {
opacity: 0.25; opacity: 0.25;
display: block; display: block;
margin-top: 9px; margin-top: 9px;
margin-bottom: 14px; margin-bottom: 14px;
} }
.content {
display: flex;
flex-direction: column;
gap: 20px;
hr {
border-bottom: 1px dashed currentColor;
}
}
} }
</style> </style>

View File

@@ -23,7 +23,7 @@ watchEffect(() => {
<style lang="scss"> <style lang="scss">
main.note { main.note {
padding-top: 8px; padding-top: 2.2em;
padding-bottom: 20px; padding-bottom: 20px;
} }
</style> </style>

View File

@@ -0,0 +1,198 @@
<template>
<main class="preferences layout-block">
<router-link to="/" class="back"><- Back</router-link>
<h1 class="mono">Storage Plugin</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 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>
</div>
<p v-if="validationError" class="error">{{ validationError }}</p>
<button @click="save" class="save-btn">
<svg-spinner v-if="saving" />
<span v-else-if="saved">Saved</span>
<span v-else>Save</span>
</button>
</main>
</template>
<script setup>
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 } = 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)
})
const saving = ref(false)
const saved = ref(false)
const save = async () => {
saving.value = true
validationError.value = ''
const plugin = selectedPlugin.value
if (plugin && plugin.configSchema.length) {
const adapterConfig = config.value.adapters[plugin.id] || {}
for (const field of plugin.configSchema) {
if (field.required && !adapterConfig[field.key]) {
validationError.value = `Please fill in all required fields for ${plugin.name}`
return
}
}
}
await setActivePlugin(selectedPluginId.value)
saving.value = false
saved.value = true
setTimeout(() => {
saved.value = false
}, 2000)
}
</script>
<style lang="scss">
.preferences {
padding-top: 1.2em;
padding-bottom: 60px;
.back {
opacity: 0.25;
display: block;
margin-top: 9px;
margin-bottom: 14px;
}
h1 {
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;
}
.save-btn {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px 0;
text-align: center;
border-top: 1px dashed currentColor;
background: var(--theme-bg);
.svg-spinner {
width: 1em;
height: 1em;
}
&:hover {
color: var(--theme-accent);
}
}
}
</style>

View File

@@ -25,7 +25,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from 'vue' import { ref, onMounted, onBeforeUnmount } from 'vue'
import useNotes from '@/composables/useNotes' import useNotes from '@/composables/useNotes'
import NoteRow from '@/components/NoteRow.vue' import NoteRow from '@/components/NoteRow.vue'
import _debounce from 'lodash/debounce' import _debounce from 'lodash/debounce'
@@ -39,6 +39,10 @@ onMounted(async () => {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
searchInput.value?.focus() searchInput.value?.focus()
}) })
onBeforeUnmount(() => {
query.value = ''
searchResults.value = []
})
const onSearch = async () => { const onSearch = async () => {
await search(query.value) await search(query.value)
@@ -50,6 +54,8 @@ const onInput = _debounce(async () => {
<style lang="scss"> <style lang="scss">
main.search { main.search {
padding-top: 1.2em;
.back { .back {
display: block; display: block;
opacity: 0.25; opacity: 0.25;