├── frontend
├── src
│ ├── version.txt
│ ├── vite-env.d.ts
│ ├── components
│ │ ├── NoteEditor
│ │ │ ├── types.ts
│ │ │ ├── fonts.ts
│ │ │ ├── ToolbarBtn.tsx
│ │ │ ├── hooks
│ │ │ │ └── useDebounce.ts
│ │ │ ├── StatusBar.tsx
│ │ │ └── Toolbar.tsx
│ │ ├── Layout.tsx
│ │ ├── update
│ │ │ ├── UpdateNotification.tsx
│ │ │ └── AutoUpdate.tsx
│ │ ├── modals
│ │ │ └── MainModal.tsx
│ │ ├── Header.tsx
│ │ ├── utils
│ │ │ └── tiptapExtensions.ts
│ │ ├── Settings.tsx
│ │ └── NoteEditor.tsx
│ ├── App.tsx
│ ├── assets
│ │ ├── sidebar-left.svg
│ │ └── sidebar-right.svg
│ ├── main.tsx
│ ├── contexts
│ │ ├── WindowContext.tsx
│ │ ├── SettingsContext.tsx
│ │ └── NotesContext.tsx
│ ├── pages
│ │ ├── Main.tsx
│ │ └── Sidebar.tsx
│ ├── App.css
│ └── index.css
├── package.json.md5
├── postcss.config.cjs
├── tsconfig.node.json
├── vite.config.ts
├── tsconfig.json
├── index.html
├── package.json
└── tailwind.config.cjs
├── scripts
├── build.sh
├── build-macos-intel.sh
├── build-macos-arm.sh
├── build-macos.sh
├── build-windows.sh
├── install-wails-cli.sh
├── postinstall
├── scripts
│ └── postinstall
├── create-dmg.sh
└── dmg-background.svg
├── .vscode
└── settings.json
├── wails.json
├── .gitignore
├── LICENSE
├── go.mod
├── dockerfile
├── main.go
├── README.md
├── CODE_OF_CONDUCT.md
├── go.sum
└── app.go
/frontend/src/version.txt:
--------------------------------------------------------------------------------
1 | v1.0.5
--------------------------------------------------------------------------------
/frontend/package.json.md5:
--------------------------------------------------------------------------------
1 | aa6426b98ee8b068a4fe474314a58a39
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteEditor/types.ts:
--------------------------------------------------------------------------------
1 | export interface FontOption {
2 | label: string;
3 | value: string;
4 | }
--------------------------------------------------------------------------------
/frontend/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Start building the app..."
7 | wails build --clean
8 |
9 | echo -e "End running the script!"
10 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.git": true,
4 | "**/.svn": true,
5 | "**/.hg": true,
6 | "**/.DS_Store": true,
7 | "**/Thumbs.db": true
8 | },
9 | "hide-files.files": []
10 | }
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "nodenext",
5 | "moduleResolution": "nodenext",
6 | "allowSyntheticDefaultImports": true
7 | },
8 | "include": ["vite.config.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/scripts/build-macos-intel.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Start building the app for macos platform..."
7 | wails build --clean --platform darwin
8 |
9 | echo -e "End running the script!"
10 |
--------------------------------------------------------------------------------
/scripts/build-macos-arm.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Start building the app for macos platform..."
7 | wails build --clean --platform darwin/arm64
8 |
9 | echo -e "End running the script!"
10 |
--------------------------------------------------------------------------------
/scripts/build-macos.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Start building the app for macos platform..."
7 | wails build --clean --platform darwin/universal
8 |
9 | echo -e "End running the script!"
10 |
--------------------------------------------------------------------------------
/scripts/build-windows.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Start building the app for windows platform..."
7 | wails build --clean --platform windows/amd64
8 |
9 | echo -e "End running the script!"
10 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteEditor/fonts.ts:
--------------------------------------------------------------------------------
1 | import { FontOption } from './types.js';
2 |
3 | export const fontFamilyOptions: FontOption[] = [
4 | { label: 'Default', value: 'Inter' },
5 | { label: 'Serif', value: 'Georgia' },
6 | { label: 'Mono', value: 'MonoLisa' },
7 | { label: 'Comic', value: 'Comic Sans MS' },
8 | ];
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import path from 'path'
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | resolve: {
9 | alias: {
10 | '@': path.resolve(__dirname, './src'),
11 | },
12 | },
13 | })
14 |
--------------------------------------------------------------------------------
/scripts/install-wails-cli.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 |
3 | echo -e "Start running the script..."
4 | cd ../
5 |
6 | echo -e "Current Go version: \c"
7 | go version
8 |
9 | echo -e "Install the Wails command line tool..."
10 | go install github.com/wailsapp/wails/v2/cmd/wails@latest
11 |
12 | echo -e "Successful installation!"
13 |
14 | echo -e "End running the script!"
15 |
--------------------------------------------------------------------------------
/wails.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voidnotes",
3 | "outputfilename": "voidnotes",
4 | "frontend:install": "bun install",
5 | "frontend:build": "bun run build",
6 | "frontend:dev:watcher": "bun run dev",
7 | "frontend:dev:serverUrl": "auto",
8 | "author": {
9 | "name": "xptea",
10 | "email": "80608102+xptea@users.noreply.github.com"
11 | },
12 | "obfuscate": true
13 | }
14 |
--------------------------------------------------------------------------------
/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Sleep for a moment to ensure the package installation is complete
4 | sleep 1
5 |
6 | # Define paths
7 | APP_NAME="VoidNotes"
8 | TEMP_APP_PATH="/private/tmp/VoidNotes.app"
9 | FINAL_APP_PATH="/Applications/VoidNotes.app"
10 |
11 | # Remove existing app if it exists
12 | rm -rf "$FINAL_APP_PATH"
13 |
14 | # Move the app to Applications folder
15 | mv "$TEMP_APP_PATH" "$FINAL_APP_PATH"
16 |
17 | # Fix permissions and signing
18 | chown -R $USER:staff "$FINAL_APP_PATH"
19 | chmod -R 755 "$FINAL_APP_PATH"
20 | xattr -cr "$FINAL_APP_PATH"
21 | codesign --force --deep --sign - "$FINAL_APP_PATH"
22 |
23 | # Open the app
24 | open "$FINAL_APP_PATH"
25 |
26 | exit 0
27 |
--------------------------------------------------------------------------------
/scripts/scripts/postinstall:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Sleep for a moment to ensure the package installation is complete
4 | sleep 1
5 |
6 | # Define paths
7 | APP_NAME="VoidNotes"
8 | TEMP_APP_PATH="/private/tmp/VoidNotes.app"
9 | FINAL_APP_PATH="/Applications/VoidNotes.app"
10 |
11 | # Remove existing app if it exists
12 | rm -rf "$FINAL_APP_PATH"
13 |
14 | # Move the app to Applications folder
15 | mv "$TEMP_APP_PATH" "$FINAL_APP_PATH"
16 |
17 | # Fix permissions and signing
18 | chown -R $USER:staff "$FINAL_APP_PATH"
19 | chmod -R 755 "$FINAL_APP_PATH"
20 | xattr -cr "$FINAL_APP_PATH"
21 | codesign --force --deep --sign - "$FINAL_APP_PATH"
22 |
23 | # Open the app
24 | open "$FINAL_APP_PATH"
25 |
26 | exit 0
27 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "nodenext",
13 | "moduleResolution": "nodenext",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": ".",
19 | "paths": {
20 | "@/*": ["src/*"]
21 | }
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import Layout from './components/Layout.js';
3 | import { WindowProvider } from './contexts/WindowContext.js';
4 | import { NotesProvider } from './contexts/NotesContext.js';
5 | import { SettingsProvider } from './contexts/SettingsContext.js';
6 |
7 | function App() {
8 | return (
9 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteEditor/ToolbarBtn.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface ToolbarButtonProps {
4 | onClick: () => void;
5 | active?: boolean;
6 | disabled?: boolean;
7 | title: string;
8 | children: React.ReactNode;
9 | }
10 |
11 | export const ToolbarButton: React.FC = ({ onClick, active = false, disabled = false, title, children }) => (
12 |
23 | );
--------------------------------------------------------------------------------
/frontend/src/assets/sidebar-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/assets/sidebar-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteEditor/hooks/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 |
3 | export function useDebounce any>(
4 | func: T,
5 | delay: number
6 | ): (...args: Parameters) => void {
7 | const [timerId, setTimerId] = useState | null>(null);
8 |
9 | useEffect(() => {
10 | return () => {
11 | if (timerId) clearTimeout(timerId);
12 | };
13 | }, [timerId]);
14 |
15 | const debouncedFunction = useCallback(
16 | (...args: Parameters) => {
17 | if (timerId) clearTimeout(timerId);
18 |
19 | const id = setTimeout(() => {
20 | func(...args);
21 | }, delay);
22 |
23 | setTimerId(id);
24 | },
25 | [func, delay, timerId]
26 | );
27 |
28 | return debouncedFunction;
29 | }
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.js'
4 | import './index.css'
5 | import { NotesProvider } from './contexts/NotesContext.js'
6 | import { SettingsProvider } from './contexts/SettingsContext.js'
7 | import { WindowProvider } from './contexts/WindowContext.js'
8 | import { useAutoUpdate } from './components/update/AutoUpdate.js'
9 |
10 | const initAutoUpdate = () => {
11 | const { checkForUpdates } = useAutoUpdate();
12 | checkForUpdates();
13 | setInterval(checkForUpdates, 12 * 60 * 60 * 1000);
14 | };
15 |
16 | const container = document.getElementById('root') as HTMLElement;
17 | const root = createRoot(container);
18 | root.render(
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
30 | initAutoUpdate();
31 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | VoidNotes
7 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | build/bin/
8 |
9 | # Test binary, built with `go test -c`
10 | *.test
11 |
12 | # Output of the go coverage tool, specifically when used with LiteIDE
13 | *.out
14 |
15 | # Dependency directories (remove the comment below to include it)
16 | vendor/
17 |
18 | # Go workspace file
19 | go.work
20 |
21 | # Wails generated files
22 | frontend/wailsjs/
23 |
24 | # Frontend dependencies
25 | frontend/node_modules/
26 | frontend/.pnp
27 | frontend/.pnp.js
28 |
29 | # Frontend testing
30 | frontend/coverage/
31 |
32 | # Frontend production build
33 | frontend/dist/
34 | frontend/build/
35 |
36 | # Misc
37 | .DS_Store
38 | .env.local
39 | .env.development.local
40 | .env.test.local
41 | .env.production.local
42 |
43 | npm-debug.log*
44 | yarn-debug.log*
45 | yarn-error.log*
46 | *.log
47 |
48 | # Editor directories and files
49 | .idea/
50 | .vscode/*
51 | !.vscode/extensions.json
52 | !.vscode/settings.json
53 | *.suo
54 | *.ntvs*
55 | *.njsproj
56 | *.sln
57 | *.sw?
58 | frontend/package.json.md5
59 | frontend/package-lock.json
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Void
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/frontend/src/components/Layout.tsx:
--------------------------------------------------------------------------------
1 | import Sidebar from '../pages/Sidebar.js';
2 | import MainContent from '../pages/Main.js';
3 | import Header from './Header.js';
4 | import Settings from './Settings.js';
5 | import { useEffect, useState } from 'react';
6 | import { Environment } from '../../wailsjs/runtime/runtime.js';
7 |
8 | interface LayoutProps {
9 | children?: React.ReactNode;
10 | }
11 |
12 | const Layout: React.FC = ({ children }) => {
13 | const [isMac, setIsMac] = useState(false);
14 |
15 | useEffect(() => {
16 | Environment().then((env) => {
17 | setIsMac(env.platform === 'darwin');
18 | });
19 | }, []);
20 |
21 | return (
22 |
23 | {!isMac &&
}
24 |
25 |
26 |
27 | {children || }
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Layout;
--------------------------------------------------------------------------------
/frontend/src/contexts/WindowContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useState } from 'react';
2 | import { Environment } from '../../wailsjs/runtime/runtime.js';
3 |
4 | interface WindowContextType {
5 | isMac: boolean;
6 | isWindows: boolean;
7 | }
8 |
9 | const WindowContext = createContext({
10 | isMac: false,
11 | isWindows: true,
12 | });
13 |
14 | export const WindowProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
15 | const [isMac, setIsMac] = useState(false);
16 | const [isWindows, setIsWindows] = useState(true);
17 |
18 | useEffect(() => {
19 | Environment().then((env) => {
20 | const mac = env.platform === 'darwin';
21 | const windows = env.platform === 'windows';
22 | setIsMac(mac);
23 | setIsWindows(windows);
24 | }).catch(err => {
25 | console.error('Failed to detect environment:', err);
26 | });
27 | }, []);
28 |
29 | return (
30 |
31 | {isMac && (
32 |
36 | )}
37 | {children}
38 |
39 | );
40 | };
41 |
42 | export const useWindow = () => useContext(WindowContext);
--------------------------------------------------------------------------------
/frontend/src/pages/Main.tsx:
--------------------------------------------------------------------------------
1 | import { useWindow } from '../contexts/WindowContext.js';
2 | import { useSettings } from '../contexts/SettingsContext.js';
3 | import NoteEditor from '../components/NoteEditor.js';
4 |
5 | interface MainContentProps {
6 | className?: string;
7 | }
8 |
9 | const MainContent: React.FC = ({ className }) => {
10 | const { isMac } = useWindow();
11 | const { getMainAreaStyle } = useSettings();
12 |
13 | return (
14 |
22 |
29 |
30 |
31 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default MainContent;
42 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module testing-ui-go
2 |
3 | go 1.22.0
4 |
5 | toolchain go1.24.1
6 |
7 | require github.com/wailsapp/wails/v2 v2.10.1
8 |
9 | require (
10 | github.com/bep/debounce v1.2.1 // indirect
11 | github.com/go-ole/go-ole v1.3.0 // indirect
12 | github.com/godbus/dbus/v5 v5.1.0 // indirect
13 | github.com/google/uuid v1.6.0 // indirect
14 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
15 | github.com/labstack/echo/v4 v4.13.3 // indirect
16 | github.com/labstack/gommon v0.4.2 // indirect
17 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
18 | github.com/leaanthony/gosod v1.0.4 // indirect
19 | github.com/leaanthony/slicer v1.6.0 // indirect
20 | github.com/leaanthony/u v1.1.1 // indirect
21 | github.com/mattn/go-colorable v0.1.13 // indirect
22 | github.com/mattn/go-isatty v0.0.20 // indirect
23 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
24 | github.com/pkg/errors v0.9.1 // indirect
25 | github.com/rivo/uniseg v0.4.7 // indirect
26 | github.com/samber/lo v1.49.1 // indirect
27 | github.com/tkrajina/go-reflector v0.5.8 // indirect
28 | github.com/valyala/bytebufferpool v1.0.0 // indirect
29 | github.com/valyala/fasttemplate v1.2.2 // indirect
30 | github.com/wailsapp/go-webview2 v1.0.19 // indirect
31 | github.com/wailsapp/mimetype v1.4.1 // indirect
32 | golang.org/x/crypto v0.33.0 // indirect
33 | golang.org/x/net v0.35.0 // indirect
34 | golang.org/x/sys v0.30.0 // indirect
35 | golang.org/x/text v0.22.0 // indirect
36 | )
37 |
--------------------------------------------------------------------------------
/dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.24-bullseye
2 |
3 | WORKDIR /usr/src/app
4 |
5 | # Install base build tools
6 | RUN apt-get update && apt-get install -y --fix-missing \
7 | build-essential \
8 | unzip \
9 | curl
10 |
11 | # Install GTK and WebKit for AMD64
12 | RUN apt-get install -y --fix-missing \
13 | libgtk-3-dev \
14 | libwebkit2gtk-4.0-dev
15 |
16 | # Install additional dependencies for AMD64
17 | RUN apt-get install -y --fix-missing \
18 | libpango-1.0-0 \
19 | libpangocairo-1.0-0 \
20 | libharfbuzz0b \
21 | libatk1.0-0 \
22 | libcairo2 \
23 | libcairo-gobject2 \
24 | libgdk-pixbuf-2.0-0 \
25 | libsoup2.4-1 \
26 | libglib2.0-0 \
27 | libglib2.0-dev \
28 | libjavascriptcoregtk-4.0-18
29 |
30 | # Install X11 libraries
31 | RUN apt-get install -y --fix-missing \
32 | x11proto-dev \
33 | libx11-dev \
34 | libxrender-dev \
35 | libxext-dev
36 |
37 | # Clear cache
38 | RUN rm -rf /var/lib/apt/lists/*
39 |
40 | # Install required tooling
41 | RUN go install github.com/wailsapp/wails/v2/cmd/wails@latest
42 | RUN curl -fsSL https://bun.sh/install | bash
43 | ENV BUN_INSTALL="/root/.bun"
44 | ENV PATH="${BUN_INSTALL}/bin:${PATH}"
45 |
46 | # Copy project files
47 | COPY . .
48 |
49 | # Install frontend dependencies
50 | WORKDIR /usr/src/app/frontend
51 | RUN bun install
52 | WORKDIR /usr/src/app
53 |
54 | # Build for Linux AMD64
55 | RUN CGO_ENABLED=1 GOOS=linux GOARCH=amd64 wails build -platform linux/amd64
56 |
57 | FROM scratch AS extract
58 | COPY --from=0 /usr/src/app/build/bin/ /
59 |
60 |
61 | # run this build it. docker build --target extract --output type=local,dest=./build/bin .
--------------------------------------------------------------------------------
/frontend/src/components/update/UpdateNotification.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useAutoUpdate } from './AutoUpdate.js';
3 |
4 | interface UpdateNotificationProps {
5 | className?: string;
6 | showVersionOnly?: boolean;
7 | }
8 |
9 | const UpdateNotification: React.FC = ({ className = '', showVersionOnly = false }) => {
10 | const {
11 | currentVersion,
12 | latestVersion,
13 | isUpdateAvailable,
14 | isChecking,
15 | checkForUpdates,
16 | handleUpdate
17 | } = useAutoUpdate();
18 |
19 | if (showVersionOnly) {
20 | return {currentVersion};
21 | }
22 |
23 | return (
24 |
25 |
26 | Version: {currentVersion}
27 | {isUpdateAvailable && (
28 |
35 | )}
36 |
37 | {isUpdateAvailable && (
38 |
New version available: {latestVersion}
39 | )}
40 |
47 |
48 | );
49 | };
50 |
51 | export default UpdateNotification;
--------------------------------------------------------------------------------
/scripts/create-dmg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
3 | PROJECT_ROOT="$SCRIPT_DIR/.."
4 | APP_NAME="VoidNotes"
5 | APP_PATH="$PROJECT_ROOT/build/bin/voidnotes.app"
6 | PKG_PATH="$PROJECT_ROOT/build/bin/${APP_NAME}_installer.pkg"
7 |
8 | # Post-install script creation
9 | POSTINSTALL_SCRIPT="$SCRIPT_DIR/postinstall"
10 | cat << EOF > "$POSTINSTALL_SCRIPT"
11 | #!/bin/bash
12 |
13 | # Sleep for a moment to ensure the package installation is complete
14 | sleep 1
15 |
16 | # Define paths
17 | APP_NAME="VoidNotes"
18 | TEMP_APP_PATH="/private/tmp/$APP_NAME.app"
19 | FINAL_APP_PATH="/Applications/$APP_NAME.app"
20 |
21 | # Remove existing app if it exists
22 | rm -rf "\$FINAL_APP_PATH"
23 |
24 | # Move the app to Applications folder
25 | mv "\$TEMP_APP_PATH" "\$FINAL_APP_PATH"
26 |
27 | # Fix permissions and signing
28 | chown -R \$USER:staff "\$FINAL_APP_PATH"
29 | chmod -R 755 "\$FINAL_APP_PATH"
30 | xattr -cr "\$FINAL_APP_PATH"
31 | codesign --force --deep --sign - "\$FINAL_APP_PATH"
32 |
33 | # Open the app
34 | open "\$FINAL_APP_PATH"
35 |
36 | exit 0
37 | EOF
38 |
39 | chmod +x "$POSTINSTALL_SCRIPT"
40 |
41 | # Build the .pkg
42 | mkdir -p "$SCRIPT_DIR/scripts"
43 | cp "$POSTINSTALL_SCRIPT" "$SCRIPT_DIR/scripts/postinstall"
44 |
45 | pkgbuild --root "$APP_PATH" \
46 | --identifier com.void.voidnotes \
47 | --version 1.0 \
48 | --install-location "/private/tmp/$APP_NAME.app" \
49 | --scripts "$SCRIPT_DIR/scripts" \
50 | "$PKG_PATH"
51 |
52 | if [ -f "$PKG_PATH" ]; then
53 | echo "Installer package created successfully at $PKG_PATH"
54 | echo "Users can now install by running the .pkg file."
55 | else
56 | echo "Failed to create installer package"
57 | exit 1
58 | fi
59 |
60 | # Run this command to build the app and create the PKG after you wail build
61 | # cd /Users/void/docs/GitHub/VoidNotes && ./scripts/create-pkg.sh
62 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | border: none !important;
3 | box-shadow: none !important;
4 | }
5 |
6 | .h-full {
7 | height: 100%;
8 | }
9 |
10 | .vertical-text {
11 | writing-mode: vertical-rl;
12 | text-orientation: mixed;
13 | transform: rotate(180deg);
14 | white-space: nowrap;
15 | }
16 |
17 | ::-webkit-scrollbar {
18 | width: 8px;
19 | height: 8px;
20 | }
21 |
22 | ::-webkit-scrollbar-track {
23 | background: rgba(125, 50, 95, 0.1);
24 | border-radius: 4px;
25 | }
26 |
27 | ::-webkit-scrollbar-thumb {
28 | background: rgba(125, 50, 95, 0.4);
29 | border-radius: 4px;
30 | }
31 |
32 | ::-webkit-scrollbar-thumb:hover {
33 | background: rgba(125, 50, 95, 0.6);
34 | }
35 |
36 | .scrollbar-custom::-webkit-scrollbar {
37 | width: 8px;
38 | height: 8px;
39 | }
40 |
41 | .scrollbar-custom::-webkit-scrollbar-track {
42 | background: var(--scrollbar-track-color, transparent);
43 | border-radius: 4px;
44 | }
45 |
46 | .scrollbar-custom::-webkit-scrollbar-thumb {
47 | background: var(--scrollbar-thumb-color, rgba(100, 40, 80, 0.8));
48 | border-radius: 4px;
49 | }
50 |
51 | .scrollbar-custom::-webkit-scrollbar-thumb:hover {
52 | background: var(--scrollbar-thumb-color, rgba(100, 40, 80, 1));
53 | opacity: 1;
54 | }
55 |
56 | .scrollbar-custom {
57 | scrollbar-width: thin;
58 | scrollbar-color: var(--scrollbar-thumb-color, rgba(100, 40, 80, 0.8)) var(--scrollbar-track-color, transparent);
59 | }
60 |
61 | .app {
62 | height: 100vh;
63 | width: 100vw;
64 | background: transparent !important;
65 | border: none !important;
66 | box-shadow: none !important;
67 | }
68 |
69 | body, html {
70 | margin: 0;
71 | padding: 0;
72 | background: transparent !important;
73 | border: none !important;
74 | box-shadow: none !important;
75 | }
76 |
77 | #root {
78 | height: 100vh;
79 | width: 100vw;
80 | background: transparent !important;
81 | overflow: hidden;
82 | border: none !important;
83 | box-shadow: none !important;
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "testing-ui-go",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "preview": "vite preview",
10 | "setup": "bun install"
11 | },
12 | "dependencies": {
13 | "@tiptap/extension-blockquote": "^3.0.0",
14 | "@tiptap/extension-bullet-list": "^3.0.0",
15 | "@tiptap/extension-code-block": "^3.0.0",
16 | "@tiptap/extension-code-block-lowlight": "^3.0.0",
17 | "@tiptap/extension-color": "^3.0.0",
18 | "@tiptap/extension-dropcursor": "^3.0.0",
19 | "@tiptap/extension-font-family": "^3.0.0",
20 | "@tiptap/extension-heading": "^3.0.0",
21 | "@tiptap/extension-highlight": "^3.0.0",
22 | "@tiptap/extension-horizontal-rule": "^3.0.0",
23 | "@tiptap/extension-image": "^3.0.0",
24 | "@tiptap/extension-link": "^3.0.0",
25 | "@tiptap/extension-ordered-list": "^3.0.0",
26 | "@tiptap/extension-placeholder": "^3.0.0",
27 | "@tiptap/extension-subscript": "^3.0.0",
28 | "@tiptap/extension-superscript": "^3.0.0",
29 | "@tiptap/extension-table": "^3.0.0",
30 | "@tiptap/extension-table-cell": "^3.0.0",
31 | "@tiptap/extension-table-header": "^3.0.0",
32 | "@tiptap/extension-table-row": "^3.0.0",
33 | "@tiptap/extension-task-item": "^3.0.0",
34 | "@tiptap/extension-task-list": "^3.0.0",
35 | "@tiptap/extension-text-style": "^3.0.0",
36 | "@tiptap/extension-underline": "^3.0.0",
37 | "@tiptap/react": "^3.0.0",
38 | "@tiptap/starter-kit": "^3.0.0",
39 | "@uiw/react-md-editor": "^4.0.5",
40 | "highlight.js": "^11.11.1",
41 | "lowlight": "^3.3.0",
42 | "react": "^19.0.0",
43 | "react-dom": "^19.0.0",
44 | "react-markdown": "^10.1.0",
45 | "refractor": "^5.0.0",
46 | "rehype-sanitize": "^6.0.0",
47 | "remark-gfm": "^4.0.1"
48 | },
49 | "devDependencies": {
50 | "@tailwindcss/typography": "^0.5.16",
51 | "@types/react": "^19.0.11",
52 | "@types/react-dom": "^19.0.4",
53 | "@vitejs/plugin-react": "^4.3.4",
54 | "autoprefixer": "^10.4.21",
55 | "postcss": "^8.5.3",
56 | "tailwindcss": "^3.4.17",
57 | "typescript": "^5.8.2",
58 | "vite": "^6.2.2"
59 | },
60 | "author": "xptea"
61 | }
--------------------------------------------------------------------------------
/frontend/src/components/update/AutoUpdate.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import localVersion from '../../version.txt?raw';
3 |
4 | interface AutoUpdateHookProps {
5 | onUpdate?: () => void;
6 | }
7 |
8 | export const useAutoUpdate = ({ onUpdate }: AutoUpdateHookProps = {}) => {
9 | const [currentVersion] = useState(localVersion.trim());
10 | const [latestVersion, setLatestVersion] = useState(localVersion.trim());
11 | const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
12 | const [isChecking, setIsChecking] = useState(false);
13 | const [lastChecked, setLastChecked] = useState(null);
14 |
15 | const fetchLatestVersion = async () => {
16 | try {
17 | setIsChecking(true);
18 | const response = await fetch('https://raw.githubusercontent.com/xptea/VoidNotes/refs/heads/main/frontend/src/version.txt', { cache: 'no-store' });
19 | if (response.ok) {
20 | const remoteVersion = await response.text();
21 | setLatestVersion(remoteVersion.trim());
22 |
23 | const isNewer = compareVersions(remoteVersion.trim(), currentVersion);
24 | setIsUpdateAvailable(isNewer);
25 | }
26 | } catch (error) {
27 | console.error('Failed to check for updates:', error);
28 | } finally {
29 | setIsChecking(false);
30 | setLastChecked(new Date());
31 | }
32 | };
33 |
34 | const compareVersions = (v1: string, v2: string): boolean => {
35 | const v1Parts = v1.replace('v', '').split('.').map(Number);
36 | const v2Parts = v2.replace('v', '').split('.').map(Number);
37 |
38 | for (let i = 0; i < v1Parts.length; i++) {
39 | if (v1Parts[i] > v2Parts[i]) return true;
40 | if (v1Parts[i] < v2Parts[i]) return false;
41 | }
42 |
43 | return false;
44 | };
45 |
46 | useEffect(() => {
47 | fetchLatestVersion();
48 |
49 | const interval = setInterval(fetchLatestVersion, 60 * 60 * 1000);
50 |
51 | return () => clearInterval(interval);
52 | }, []);
53 |
54 | const handleUpdate = () => {
55 | if (onUpdate) {
56 | onUpdate();
57 | } else {
58 | window.open('https://github.com/xptea/VoidNotes/releases', '_blank');
59 | }
60 | };
61 |
62 | return {
63 | currentVersion,
64 | latestVersion,
65 | isUpdateAvailable,
66 | isChecking,
67 | lastChecked,
68 | checkForUpdates: fetchLatestVersion,
69 | handleUpdate
70 | };
71 | };
72 |
73 | export default useAutoUpdate;
--------------------------------------------------------------------------------
/scripts/dmg-background.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "log"
6 | "runtime"
7 |
8 | "github.com/wailsapp/wails/v2"
9 | "github.com/wailsapp/wails/v2/pkg/logger"
10 | "github.com/wailsapp/wails/v2/pkg/options"
11 | "github.com/wailsapp/wails/v2/pkg/options/assetserver"
12 | "github.com/wailsapp/wails/v2/pkg/options/mac"
13 | "github.com/wailsapp/wails/v2/pkg/options/windows"
14 | )
15 |
16 | //go:embed all:frontend/dist
17 | var assets embed.FS
18 |
19 | //go:embed build/appicon.png
20 | var icon []byte
21 |
22 | func main() {
23 | app := NewApp()
24 |
25 | err := wails.Run(&options.App{
26 | Title: "VoidNotes",
27 | Width: 1200,
28 | Height: 800,
29 | MinWidth: 500,
30 | MinHeight: 500,
31 | DisableResize: false,
32 | Fullscreen: false,
33 | Frameless: runtime.GOOS == "windows",
34 | StartHidden: false,
35 | HideWindowOnClose: false,
36 | BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 0},
37 | AssetServer: &assetserver.Options{
38 | Assets: assets,
39 | },
40 | Menu: nil,
41 | Logger: nil,
42 | LogLevel: logger.DEBUG,
43 | OnStartup: app.startup,
44 | OnDomReady: app.domReady,
45 | OnBeforeClose: app.beforeClose,
46 | OnShutdown: app.shutdown,
47 | WindowStartState: options.Normal,
48 | Bind: []interface{}{
49 | app,
50 | },
51 | Windows: &windows.Options{
52 | WebviewIsTransparent: true,
53 | WindowIsTranslucent: true,
54 | DisableWindowIcon: false,
55 | DisableFramelessWindowDecorations: false,
56 | BackdropType: windows.None,
57 | Theme: windows.SystemDefault,
58 | CustomTheme: &windows.ThemeSettings{
59 | DarkModeTitleBar: windows.RGB(0, 0, 0),
60 | DarkModeTitleText: windows.RGB(255, 255, 255),
61 | DarkModeBorder: windows.RGB(0, 0, 0),
62 | LightModeTitleBar: windows.RGB(0, 0, 0),
63 | LightModeTitleText: windows.RGB(255, 255, 255),
64 | LightModeBorder: windows.RGB(0, 0, 0),
65 | },
66 | },
67 | Mac: &mac.Options{
68 | TitleBar: &mac.TitleBar{
69 | TitlebarAppearsTransparent: true,
70 | HideTitle: true,
71 | HideTitleBar: false,
72 | FullSizeContent: true,
73 | UseToolbar: false,
74 | HideToolbarSeparator: true,
75 | },
76 | Appearance: mac.NSAppearanceNameDarkAqua,
77 | WebviewIsTransparent: true,
78 | WindowIsTranslucent: true,
79 | About: &mac.AboutInfo{
80 | Title: "VoidNotes",
81 | Message: "A simple note taking app",
82 | Icon: icon,
83 | },
84 | },
85 | })
86 |
87 | if err != nil {
88 | log.Fatal(err)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/frontend/src/components/modals/MainModal.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | export interface InputModalProps {
4 | isOpen: boolean;
5 | onClose: () => void;
6 | onConfirm: (value: string) => void;
7 | title: string;
8 | message?: string;
9 | placeholder?: string;
10 | confirmText?: string;
11 | initialValue?: string;
12 | inputType?: 'text' | 'url';
13 | showInput?: boolean;
14 | cancelText?: string;
15 | }
16 |
17 | const InputModal: React.FC = ({
18 | isOpen,
19 | onClose,
20 | onConfirm,
21 | title,
22 | message,
23 | placeholder = '',
24 | confirmText = 'Confirm',
25 | initialValue = '',
26 | inputType = 'text',
27 | showInput = true,
28 | cancelText = 'Cancel'
29 | }) => {
30 | const [value, setValue] = useState(initialValue);
31 |
32 | if (!isOpen) return null;
33 |
34 | const handleSubmit = (e: React.FormEvent) => {
35 | e.preventDefault();
36 | if (showInput) {
37 | if (value.trim()) {
38 | onConfirm(value.trim());
39 | setValue('');
40 | }
41 | } else {
42 | onConfirm('');
43 | }
44 | };
45 |
46 | const handleKeyDown = (e: React.KeyboardEvent) => {
47 | if (e.key === 'Escape') {
48 | onClose();
49 | }
50 | };
51 |
52 | return (
53 |
90 | );
91 | };
92 |
93 | export default InputModal;
--------------------------------------------------------------------------------
/frontend/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 |
2 | /** @type {import('tailwindcss').Config} */
3 | module.exports = {
4 | content: [
5 | "./src/**/*.{js,jsx,ts,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | typography: (theme) => ({
10 | invert: {
11 | css: {
12 | '--tw-prose-body': theme('colors.white / 90'),
13 | '--tw-prose-headings': theme('colors.white'),
14 | '--tw-prose-links': theme('colors.blue.400'),
15 | '--tw-prose-bold': theme('colors.white'),
16 | '--tw-prose-quotes': theme('colors.white / 90'),
17 | '--tw-prose-code': theme('colors.white'),
18 | '--tw-prose-hr': theme('colors.white / 20'),
19 | '--tw-prose-th-borders': theme('colors.white / 20'),
20 | '--tw-prose-td-borders': theme('colors.white / 10'),
21 | 'H1': {
22 | marginTop: '0.1em',
23 |
24 | marginBottom: '1rem',
25 | },
26 | 'H2': {
27 | marginTop: '0.1em',
28 |
29 | marginBottom: '1rem',
30 | },
31 | 'H3': {
32 | marginTop: '0.1em',
33 |
34 | marginBottom: '1rem',
35 | },
36 | 'p': {
37 | marginTop: '0.1em',
38 | marginBottom: '0.1em',
39 | },
40 | 'table': {
41 | backgroundColor: 'rgba(255, 255, 255, 0.05)',
42 | borderRadius: '0.5rem',
43 | overflow: 'hidden',
44 | },
45 | 'th': {
46 | backgroundColor: 'rgba(255, 255, 255, 0.1)',
47 | padding: '0.75rem',
48 | },
49 | 'td': {
50 | padding: '0.75rem',
51 | },
52 | 'ul[data-type="taskList"]': {
53 | listStyle: 'none',
54 | padding: 0,
55 | },
56 | 'ul[data-type="taskList"] li': {
57 | display: 'flex',
58 | alignItems: 'center',
59 | gap: '0.5rem',
60 | },
61 | 'ul[data-type="taskList"] input[type="checkbox"]': {
62 | width: '1rem',
63 | height: '1rem',
64 | borderRadius: '0.25rem',
65 | backgroundColor: 'rgba(255, 255, 255, 0.1)',
66 | border: '1px solid rgba(255, 255, 255, 0.2)',
67 | },
68 | 'blockquote': {
69 | borderLeftColor: theme('colors.blue.500'),
70 | backgroundColor: 'rgba(255, 255, 255, 0.05)',
71 | borderRadius: '0.25rem',
72 | },
73 | 'img': {
74 | borderRadius: '0.5rem',
75 | maxHeight: '20rem',
76 | objectFit: 'contain',
77 | },
78 | 'hr': {
79 | margin: '2rem 0',
80 | }
81 | }
82 | }
83 | })
84 | },
85 | },
86 | plugins: [
87 | require('@tailwindcss/typography'),
88 | ],
89 | }
90 |
--------------------------------------------------------------------------------
/frontend/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { WindowMinimise, WindowToggleMaximise, Quit, EventsOn, EventsOff } from '../../wailsjs/runtime/runtime.js';
2 | import { useEffect, useState } from 'react';
3 | import { useSettings } from '../contexts/SettingsContext.js';
4 |
5 | interface HeaderProps {
6 | title?: string;
7 | }
8 |
9 | const Header: React.FC = ({ title = "VoidWorks" }) => {
10 | const [isMaximized, setIsMaximized] = useState(false);
11 | const { getHeaderStyle } = useSettings();
12 |
13 | useEffect(() => {
14 | const unsubscribeMaximize = EventsOn('wails:window-maximised', () => {
15 | setIsMaximized(true);
16 | });
17 |
18 | const unsubscribeRestore = EventsOn('wails:window-normal', () => {
19 | setIsMaximized(false);
20 | });
21 |
22 | return () => {
23 | unsubscribeMaximize();
24 | unsubscribeRestore();
25 | EventsOff('wails:window-maximised');
26 | EventsOff('wails:window-normal');
27 | };
28 | }, []);
29 |
30 | const handleMinimize = () => {
31 | WindowMinimise();
32 | };
33 |
34 | const handleMaximize = () => {
35 | WindowToggleMaximise();
36 | };
37 |
38 | const handleClose = () => {
39 | Quit();
40 | };
41 |
42 | return (
43 |
54 |
{title}
55 |
56 |
64 |
65 |
80 |
81 |
89 |
90 |
91 | );
92 | };
93 |
94 | export default Header;
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VoidNotes
2 | https://notes.voidworks.xyz/
3 |
4 | A modern, lightweight, and customizable note-taking application built with Go and React. VoidNotes combines elegant design with powerful features to help you organize your thoughts efficiently.
5 |
6 | 
7 |
8 | ## Features
9 |
10 | - 📝 Rich Text Editor
11 | - 🏷️ Note organization with tags
12 | - 🔍 Full-text search across notes and tags
13 | - 🎨 Fully customizable UI themes
14 | - 💨 Lightweight and fast performance
15 | - 🖥️ Cross-platform support (Windows & MacOS)
16 | - ⚡ Real-time saving
17 | - 📊 Word and character count
18 | - 📋 Task lists and tables support
19 |
20 | ## Installation
21 |
22 | 1. Go to the [releases page](https://github.com/xptea/VoidNotes/releases)
23 |
24 | ### Windows
25 | - Download and run the `voidnotes-amd64-installer.exe`, and then open it.
26 |
27 | ### Mac
28 | - Download and open the `VoidNotes_installer.pkg`.
29 | For Mac, since the installer is not signed, when you first open the file, you will have to click "Done".
30 |
31 | 
32 |
33 | After that, go to **Settings**, then **Privacy & Security**, and scroll all the way to the bottom. You'll find a pop-up like this:
34 |
35 | 
36 |
37 | Click **Open Anyway**
38 |
39 | 
40 | Lastly, click **Open Anyway** to continue to the installer
41 | - Alternatively, you can rebuild and compile the project yourself from source
42 |
43 |
44 | ## Usage
45 |
46 | ### Basic Navigation
47 | - Create new notes using the + button
48 | - Search notes using the search bar
49 | - Tag notes for better organization
50 | - Customize the UI colors in settings
51 |
52 | ### Editor Features
53 | - Rich text formatting
54 | - Code blocks with syntax highlighting
55 | - Task lists
56 | - Tables
57 | - Image embedding
58 | - Links
59 |
60 | ## Customization
61 |
62 | VoidNotes offers extensive UI customization options:
63 |
64 | - Sidebar color and opacity
65 | - Main area color and opacity
66 | - Header appearance
67 | - Custom scrollbars
68 | - Light/Dark theme support
69 |
70 | 
71 | 
72 |
73 | ## Roadmap
74 |
75 | ### 2025
76 | - [x] Auto Update
77 | - [ ] Search text in note
78 | - [ ] Image embedding support
79 | - [ ] Cloud sync support
80 | - [ ] Enhanced text styling options
81 | - [ ] Note templates
82 | - [ ] Export options (PDF, HTML, Txt)
83 | - [ ] Custom themes marketplace
84 | - [ ] Note version history
85 | - [ ] Note encryption
86 |
87 | We welcome contributions! If you have suggestions or want to contribute to the code:
88 |
89 | 1. Fork the repository
90 | 2. Create your feature branch
91 | 3. Commit your changes
92 | 4. Push to the branch
93 | 5. Create a Pull Request
94 |
95 | ## Support
96 |
97 | If you encounter any issues or have questions:
98 |
99 | - Open an issue on GitHub
100 | - Email support: void@terradream.games
101 |
102 | ## License
103 |
104 | VoidNotes is released under the MIT License. See the LICENSE file for more details.
105 |
106 | ## Credits
107 |
108 | Built with ❤️ by VoidWorks
109 |
110 | - [Wails](https://wails.io/)
111 | - [React](https://reactjs.org/)
112 | - [TipTap](https://tiptap.dev/)
113 | - [Tailwind CSS](https://tailwindcss.com/)
114 |
115 | ---
116 |
117 | **Note**: This is a continuously evolving project. Check back regularly for updates and new features!
118 |
--------------------------------------------------------------------------------
/frontend/src/components/NoteEditor/StatusBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { Note } from '../../contexts/NotesContext.js';
3 |
4 | interface StatusBarProps {
5 | note: Note;
6 | wordCount: number;
7 | charCount: number;
8 | isSaving: boolean;
9 | appDataDir?: string;
10 | }
11 |
12 | export const StatusBar: React.FC = ({ wordCount, charCount, isSaving, appDataDir }) => {
13 | const [showSaved, setShowSaved] = useState(false);
14 | const [isHovering, setIsHovering] = useState(false);
15 |
16 | useEffect(() => {
17 | let timer: ReturnType | null = null;
18 |
19 | if (!isSaving && !showSaved) {
20 | setShowSaved(true);
21 | timer = setTimeout(() => {
22 | setShowSaved(false);
23 | }, 1500);
24 | }
25 |
26 | return () => {
27 | if (timer) clearTimeout(timer);
28 | };
29 | }, [isSaving]);
30 |
31 | return (
32 |
33 |
setIsHovering(true)}
38 | onMouseLeave={() => setIsHovering(false)}
39 | >
40 | {isSaving ? (
41 | <>
42 |
46 |
Saving...
47 | >
48 | ) : showSaved || isHovering ? (
49 | <>
50 |
53 | {showSaved || isHovering ?
Saved : null}
54 | >
55 | ) : (
56 |
59 | )}
60 |
61 | {/* Storage location tooltip - positioned outside the parent element */}
62 | {isHovering && appDataDir && (
63 |
64 |
Notes are stored at:
65 |
{appDataDir}/notes
66 |
67 | )}
68 |
69 |
70 |
71 |
72 |
73 |
76 |
77 | {wordCount}
78 |
79 |
80 |
81 |
82 |
85 |
86 | {charCount}
87 |
88 |
89 |
90 | );
91 | };
--------------------------------------------------------------------------------
/frontend/src/contexts/SettingsContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect, CSSProperties} from 'react';
2 |
3 |
4 | export interface CustomCSSProperties extends CSSProperties {
5 | '--scrollbar-thumb-color'?: string;
6 | '--scrollbar-track-color'?: string;
7 | }
8 | interface AppSettings {
9 | sidebarColor: string;
10 | sidebarOpacity: number;
11 | mainAreaColor: string;
12 | mainAreaOpacity: number;
13 | headerColor: string;
14 | headerOpacity: number;
15 | }
16 |
17 | interface SettingsContextType {
18 | settings: AppSettings;
19 | updateSettings: (settings: Partial) => void;
20 | isSettingsOpen: boolean;
21 | toggleSettings: () => void;
22 | getSidebarStyle: () => React.CSSProperties;
23 | getMainAreaStyle: () => React.CSSProperties;
24 | getHeaderStyle: () => React.CSSProperties;
25 | getScrollbarStyle: () => CustomCSSProperties;
26 | }
27 |
28 | const defaultSettings: AppSettings = {
29 | sidebarColor: "125, 50, 95",
30 | sidebarOpacity: 0.85,
31 | mainAreaColor: "125, 50, 95",
32 | mainAreaOpacity: 0.85,
33 | headerColor: "125, 50, 95",
34 | headerOpacity: 0.85,
35 | };
36 |
37 | const SettingsContext = createContext({
38 | settings: defaultSettings,
39 | updateSettings: () => {},
40 | isSettingsOpen: false,
41 | toggleSettings: () => {},
42 | getSidebarStyle: () => ({}),
43 | getMainAreaStyle: () => ({}),
44 | getHeaderStyle: () => ({}),
45 | getScrollbarStyle: () => ({} as CustomCSSProperties),
46 | });
47 |
48 | export const SettingsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
49 | const [settings, setSettings] = useState(() => {
50 | const savedSettings = localStorage.getItem('voidnotes-settings');
51 | return savedSettings ? JSON.parse(savedSettings) : defaultSettings;
52 | });
53 |
54 | const [isSettingsOpen, setIsSettingsOpen] = useState(false);
55 |
56 | useEffect(() => {
57 | localStorage.setItem('voidnotes-settings', JSON.stringify(settings));
58 | }, [settings]);
59 |
60 | const updateSettings = (newSettings: Partial) => {
61 | setSettings(prev => ({ ...prev, ...newSettings }));
62 | };
63 |
64 | const toggleSettings = () => {
65 | setIsSettingsOpen(prev => !prev);
66 | };
67 |
68 | const getSidebarStyle = (): React.CSSProperties => {
69 | return {
70 | backgroundColor: `rgba(${settings.sidebarColor}, ${settings.sidebarOpacity})`,
71 | backdropFilter: "blur(20px)",
72 | WebkitBackdropFilter: "blur(20px)",
73 | background: `rgba(${settings.sidebarColor}, ${settings.sidebarOpacity})`,
74 | };
75 | };
76 |
77 | const getMainAreaStyle = (): React.CSSProperties => {
78 | return {
79 | backgroundColor: `rgba(${settings.mainAreaColor}, ${settings.mainAreaOpacity})`,
80 | backdropFilter: "blur(20px)",
81 | WebkitBackdropFilter: "blur(20px)",
82 | background: `rgba(${settings.mainAreaColor}, ${settings.mainAreaOpacity})`,
83 | };
84 | };
85 |
86 | const getHeaderStyle = (): React.CSSProperties => {
87 | return {
88 | backgroundColor: `rgba(${settings.headerColor}, ${settings.headerOpacity})`,
89 | backdropFilter: "blur(20px)",
90 | WebkitBackdropFilter: "blur(20px)",
91 | background: `rgba(${settings.headerColor}, ${settings.headerOpacity})`,
92 | };
93 | };
94 |
95 | const darkenColor = (rgb: string, factor: number = 0.7): string => {
96 | const [r, g, b] = rgb.split(',').map(val => Math.floor(Number(val) * factor));
97 | return `${r}, ${g}, ${b}`;
98 | };
99 |
100 | const getScrollbarStyle = (): CustomCSSProperties => {
101 | const darkerColor = darkenColor(settings.sidebarColor, 0.6);
102 |
103 | return {
104 | '--scrollbar-thumb-color': `rgba(${darkerColor}, 0.8)`,
105 | '--scrollbar-track-color': 'transparent'
106 | };
107 | };
108 |
109 | return (
110 |
122 | {children}
123 |
124 | );
125 | };
126 |
127 | export const useSettings = () => useContext(SettingsContext);
--------------------------------------------------------------------------------
/frontend/src/components/utils/tiptapExtensions.ts:
--------------------------------------------------------------------------------
1 | import BulletList from '@tiptap/extension-bullet-list';
2 | import OrderedList from '@tiptap/extension-ordered-list';
3 | import Link from '@tiptap/extension-link';
4 | import Underline from '@tiptap/extension-underline';
5 | import TextStyle from '@tiptap/extension-text-style';
6 | import Color from '@tiptap/extension-color';
7 | import Highlight from '@tiptap/extension-highlight';
8 | import FontFamily from '@tiptap/extension-font-family';
9 | import Superscript from '@tiptap/extension-superscript';
10 | import Subscript from '@tiptap/extension-subscript';
11 | import Table from '@tiptap/extension-table';
12 | import TableRow from '@tiptap/extension-table-row';
13 | import TableCell from '@tiptap/extension-table-cell';
14 | import TableHeader from '@tiptap/extension-table-header';
15 | import TaskList from '@tiptap/extension-task-list';
16 | import TaskItem from '@tiptap/extension-task-item';
17 | import Blockquote from '@tiptap/extension-blockquote';
18 | import Dropcursor from '@tiptap/extension-dropcursor';
19 | import Image from '@tiptap/extension-image';
20 | import HorizontalRule from '@tiptap/extension-horizontal-rule';
21 | import { Extension } from '@tiptap/core';
22 | import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
23 |
24 | import { common, createLowlight } from 'lowlight';
25 | const lowlight = createLowlight(common);
26 | import javascript from 'highlight.js/lib/languages/javascript';
27 | import typescript from 'highlight.js/lib/languages/typescript';
28 | import python from 'highlight.js/lib/languages/python';
29 | import css from 'highlight.js/lib/languages/css';
30 | import xml from 'highlight.js/lib/languages/xml';
31 | import json from 'highlight.js/lib/languages/json';
32 | import bash from 'highlight.js/lib/languages/bash';
33 | import go from 'highlight.js/lib/languages/go';
34 | import rust from 'highlight.js/lib/languages/rust';
35 | import cpp from 'highlight.js/lib/languages/cpp';
36 | import java from 'highlight.js/lib/languages/java';
37 | import csharp from 'highlight.js/lib/languages/csharp';
38 |
39 | lowlight.register('javascript', javascript);
40 | lowlight.register('typescript', typescript);
41 | lowlight.register('python', python);
42 | lowlight.register('css', css);
43 | lowlight.register('html', xml);
44 | lowlight.register('json', json);
45 | lowlight.register('bash', bash);
46 | lowlight.register('go', go);
47 | lowlight.register('rust', rust);
48 | lowlight.register('cpp', cpp);
49 | lowlight.register('java', java);
50 | lowlight.register('csharp', csharp);
51 |
52 | const DisableSpellcheckInCode = Extension.create({
53 | name: 'disableSpellcheckInCode',
54 | addGlobalAttributes() {
55 | return [
56 | {
57 | types: ['codeBlock', 'code'],
58 | attributes: {
59 | spellcheck: {
60 | default: 'false',
61 | parseHTML: (element: { getAttribute: (arg0: string) => any; }) => element.getAttribute('spellcheck') || 'false',
62 | renderHTML: () => {
63 | return {
64 | spellcheck: 'false',
65 | }
66 | },
67 | },
68 | },
69 | },
70 | ]
71 | },
72 | });
73 |
74 | const CodeBlock = CodeBlockLowlight.configure({
75 | lowlight,
76 | defaultLanguage: 'javascript',
77 | HTMLAttributes: {
78 | class: 'hljs',
79 | spellcheck: 'false',
80 | }
81 | }).extend({
82 | addNodeView() {
83 | return ({ node }) => {
84 | const dom = document.createElement('pre');
85 |
86 | dom.setAttribute('spellcheck', 'false');
87 |
88 | dom.classList.add('hljs');
89 |
90 | const content = document.createElement('code');
91 | content.setAttribute('spellcheck', 'false');
92 |
93 | if (node.attrs.language) {
94 | content.classList.add(`language-${node.attrs.language}`);
95 | }
96 |
97 | dom.append(content);
98 |
99 | return {
100 | dom,
101 | contentDOM: content,
102 | };
103 | };
104 | }
105 | });
106 |
107 | export {
108 | BulletList,
109 | OrderedList,
110 | CodeBlock,
111 | DisableSpellcheckInCode,
112 | Link,
113 | Underline,
114 | TextStyle,
115 | Color,
116 | Highlight,
117 | FontFamily,
118 | Superscript,
119 | Subscript,
120 | Table,
121 | TableRow,
122 | TableCell,
123 | TableHeader,
124 | TaskList,
125 | TaskItem,
126 | Blockquote,
127 | Dropcursor,
128 | Image,
129 | HorizontalRule,
130 | };
131 |
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | html, body {
6 | margin: 0;
7 | padding: 0;
8 | background: transparent !important;
9 | border: none !important;
10 | }
11 |
12 | #root {
13 | height: 100vh;
14 | width: 100vw;
15 | background: transparent !important;
16 | border: none !important;
17 | }
18 |
19 | * {
20 | box-sizing: border-box;
21 | }
22 |
23 | :root {
24 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
25 | line-height: 1.5;
26 | font-weight: 400;
27 |
28 | color-scheme: dark;
29 | background-color: rgb(15, 15, 15);
30 | font-synthesis: none;
31 | text-rendering: optimizeLegibility;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-osx-font-smoothing: grayscale;
34 | background: transparent !important;
35 | border: none !important;
36 | }
37 |
38 | /* Custom Scrollbar Styles */
39 | .scrollbar-custom::-webkit-scrollbar {
40 | width: 8px;
41 | height: 8px;
42 | }
43 |
44 | .scrollbar-custom::-webkit-scrollbar-track {
45 | background: var(--scrollbar-track-color);
46 | border-radius: 4px;
47 | }
48 |
49 | .scrollbar-custom::-webkit-scrollbar-thumb {
50 | background: var(--scrollbar-thumb-color);
51 | border-radius: 4px;
52 | }
53 |
54 | .scrollbar-custom::-webkit-scrollbar-corner {
55 | background: transparent;
56 | }
57 |
58 | /* TipTap Editor Styles */
59 | .ProseMirror {
60 | height: 100%;
61 | overflow-y: auto;
62 | overflow-x: hidden;
63 | }
64 |
65 | .ProseMirror:focus {
66 | outline: none;
67 | }
68 |
69 | /* Table Styles */
70 | .ProseMirror table {
71 | border-collapse: collapse;
72 | margin: 0;
73 | overflow: hidden;
74 | table-layout: fixed;
75 | width: 100%;
76 | }
77 |
78 | .ProseMirror td,
79 | .ProseMirror th {
80 | position: relative;
81 | min-width: 1em;
82 | border: 1px solid rgba(255, 255, 255, 0.1);
83 | margin: 0;
84 | padding: 3px 5px;
85 | vertical-align: top;
86 | box-sizing: border-box;
87 | position: relative;
88 | }
89 |
90 | .ProseMirror .selectedCell:after {
91 | z-index: 2;
92 | position: absolute;
93 | content: "";
94 | left: 0; right: 0; top: 0; bottom: 0;
95 | background: rgba(200, 200, 255, 0.1);
96 | pointer-events: none;
97 | }
98 |
99 | /* Column resize handle */
100 | .ProseMirror .column-resize-handle {
101 | position: absolute;
102 | right: -2px;
103 | top: 0;
104 | bottom: 0;
105 | width: 4px;
106 | background-color: rgba(255, 255, 255, 0.5);
107 | pointer-events: none;
108 | }
109 |
110 | .ProseMirror.resize-cursor {
111 | cursor: ew-resize;
112 | cursor: col-resize;
113 | }
114 |
115 | /* Task List Styles */
116 | ul[data-type="taskList"] {
117 | list-style: none;
118 | padding: 0;
119 | }
120 |
121 | ul[data-type="taskList"] li {
122 | display: flex;
123 | align-items: flex-start;
124 | margin-bottom: 0.5em;
125 | }
126 |
127 | ul[data-type="taskList"] li > label {
128 | flex: 0 0 auto;
129 | margin-right: 0.5em;
130 | user-select: none;
131 | }
132 |
133 | ul[data-type="taskList"] li > div {
134 | flex: 1 1 auto;
135 | }
136 |
137 | /* Image Styles */
138 | .ProseMirror img {
139 | max-width: 100%;
140 | height: auto;
141 | transition: filter 0.2s ease;
142 | }
143 |
144 | .ProseMirror img.ProseMirror-selectednode {
145 | outline: 2px solid #4299e1;
146 | filter: brightness(90%);
147 | }
148 |
149 | /* Placeholder Styles */
150 | .ProseMirror p.is-editor-empty:first-child::before {
151 | color: rgba(255, 255, 255, 0.4);
152 | content: attr(data-placeholder);
153 | float: left;
154 | height: 0;
155 | pointer-events: none;
156 | }
157 |
158 | /* Custom Code Block Styling */
159 | pre {
160 | padding: 1rem;
161 | border-radius: 0.5rem;
162 | background-color: rgba(22, 22, 30, 0.95) !important;
163 | overflow: auto;
164 | margin: 1rem 0;
165 | }
166 |
167 | pre, pre code {
168 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
169 | font-size: 0.9em;
170 | line-height: 1.5;
171 | }
172 |
173 | /* Force spellcheck to be disabled on code blocks */
174 | pre, pre code {
175 | -webkit-spellcheck: false !important;
176 | }
177 |
178 | /* Ensure code block content is colored properly */
179 | pre code:not(.hljs) {
180 | color: #e2e8f0;
181 | }
182 |
183 | /* Ensure code blocks have proper spacing */
184 | .ProseMirror pre {
185 | margin: 1em 0;
186 | }
187 |
188 | /* Ensure code elements inherit colors from highlight.js */
189 | .hljs {
190 | display: block;
191 | overflow-x: auto;
192 | padding: 1em;
193 | background: #1e1e1e !important;
194 | color: #dcdcdc;
195 | }
196 |
197 | /* Make sure padding is consistent */
198 | .tiptap pre {
199 | padding: 1rem !important;
200 | }
201 |
202 | /* Ensure code blocks stand out in the editor */
203 | .ProseMirror pre {
204 | background-color: rgba(22, 22, 30, 0.95) !important;
205 | border: 1px solid rgba(255, 255, 255, 0.1);
206 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/frontend/src/components/Settings.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useSettings } from '../contexts/SettingsContext.js';
3 | import UpdateNotification from './update/UpdateNotification.js';
4 |
5 | interface ColorOption {
6 | name: string;
7 | value: string;
8 | }
9 |
10 | const Settings: React.FC = () => {
11 | const { settings, updateSettings, isSettingsOpen, toggleSettings } = useSettings();
12 |
13 | const colorOptions: ColorOption[] = [
14 | { name: 'Purple', value: '125, 50, 95' },
15 | { name: 'Blue', value: '50, 80, 125' },
16 | { name: 'Green', value: '50, 125, 80' },
17 | { name: 'Dark', value: '20, 20, 30' },
18 | { name: 'Neutral', value: '60, 60, 75' }
19 | ];
20 |
21 | if (!isSettingsOpen) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 |
30 |
Settings
31 |
32 |
33 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | {colorOptions.map((color) => (
48 |
61 |
62 |
63 |
64 |
67 | updateSettings({ headerOpacity: parseInt(e.target.value) / 100 })}
73 | className="w-full bg-white/20 rounded-lg appearance-none h-2"
74 | />
75 |
76 |
77 |
78 |
79 |
80 | {colorOptions.map((color) => (
81 |
94 |
95 |
96 |
97 |
100 | updateSettings({ sidebarOpacity: parseInt(e.target.value) / 100 })}
106 | className="w-full bg-white/20 rounded-lg appearance-none h-2"
107 | />
108 |
109 |
110 |
111 |
112 |
113 | {colorOptions.map((color) => (
114 |
127 |
128 |
129 |
130 |
133 | updateSettings({ mainAreaOpacity: parseInt(e.target.value) / 100 })}
139 | className="w-full bg-white/20 rounded-lg appearance-none h-2"
140 | />
141 |
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | export default Settings;
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
2 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
6 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
7 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
8 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
12 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
13 | github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
14 | github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
15 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
16 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
17 | github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
18 | github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
19 | github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
20 | github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
21 | github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
22 | github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
23 | github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
24 | github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
25 | github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
26 | github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
27 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
28 | github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
29 | github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
30 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
31 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
32 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
33 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
34 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
35 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
36 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
37 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
38 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
41 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
42 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
43 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
44 | github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
45 | github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
46 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
47 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
48 | github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
49 | github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
50 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
51 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
52 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
53 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
54 | github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
55 | github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
56 | github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
57 | github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
58 | github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
59 | github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
60 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
61 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
62 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
63 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
64 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
65 | golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
67 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
68 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
69 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
71 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
72 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
73 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
74 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
75 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
76 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
77 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
78 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
79 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
80 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "log"
8 | "os"
9 | "path/filepath"
10 | "runtime"
11 | "time"
12 | )
13 |
14 | type Note struct {
15 | ID string `json:"id"`
16 | Title string `json:"title"`
17 | Content string `json:"content"`
18 | CreatedAt string `json:"createdAt"`
19 | UpdatedAt string `json:"updatedAt"`
20 | Tags []string `json:"tags,omitempty"`
21 | }
22 |
23 | type App struct {
24 | ctx context.Context
25 | notesDir string
26 | appDataDir string
27 | logger *log.Logger
28 | }
29 |
30 | func NewApp() *App {
31 | return &App{}
32 | }
33 |
34 | func (a *App) startup(ctx context.Context) {
35 | a.ctx = ctx
36 |
37 | logFile, err := os.OpenFile(filepath.Join(os.TempDir(), "voidnotes.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
38 | if err == nil {
39 | a.logger = log.New(logFile, "", log.LstdFlags)
40 | } else {
41 | a.logger = log.New(os.Stderr, "", log.LstdFlags)
42 | a.logger.Printf("Failed to open log file: %v", err)
43 | }
44 |
45 | a.logger.Println("App starting up...")
46 |
47 | if err := a.initAppDataDir(); err != nil {
48 | a.logger.Printf("Failed to initialize app data directory: %v", err)
49 | } else {
50 | a.logger.Printf("App data directory initialized: %s", a.appDataDir)
51 | a.logger.Printf("Notes directory: %s", a.notesDir)
52 | }
53 | }
54 |
55 | func (a *App) initAppDataDir() error {
56 | var appDataPath string
57 |
58 | if runtime.GOOS == "windows" {
59 | appDataPath = os.Getenv("LOCALAPPDATA")
60 | if appDataPath == "" {
61 | appDataPath = filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local")
62 | }
63 | a.appDataDir = filepath.Join(appDataPath, "VoidNotes")
64 | } else if runtime.GOOS == "darwin" {
65 | homeDir, err := os.UserHomeDir()
66 | if err != nil {
67 | return err
68 | }
69 | a.appDataDir = filepath.Join(homeDir, "Library", "Application Support", "VoidNotes")
70 | } else {
71 | homeDir, err := os.UserHomeDir()
72 | if err != nil {
73 | return err
74 | }
75 | a.appDataDir = filepath.Join(homeDir, ".voidnotes")
76 | }
77 |
78 | if err := os.MkdirAll(a.appDataDir, 0755); err != nil {
79 | return err
80 | }
81 |
82 | a.notesDir = filepath.Join(a.appDataDir, "notes")
83 | if err := os.MkdirAll(a.notesDir, 0755); err != nil {
84 | return err
85 | }
86 |
87 | return nil
88 | }
89 |
90 | func (a *App) domReady(ctx context.Context) {
91 | a.logger.Println("DOM ready")
92 | }
93 |
94 | func (a *App) beforeClose(ctx context.Context) (prevent bool) {
95 | a.logger.Println("Application is closing")
96 | return false
97 | }
98 |
99 | func (a *App) shutdown(ctx context.Context) {
100 | a.logger.Println("Application shutting down")
101 | }
102 |
103 | func (a *App) Greet(name string) string {
104 | return fmt.Sprintf("Hello %s, It's show timee!", name)
105 | }
106 |
107 | func (a *App) GetAppDataDir() string {
108 | return a.appDataDir
109 | }
110 |
111 | func (a *App) GetNotesDir() string {
112 | return a.notesDir
113 | }
114 |
115 | func (a *App) SaveNote(noteData string) error {
116 | startTime := time.Now()
117 | if a.logger != nil {
118 | a.logger.Println("SaveNote called with data length:", len(noteData))
119 | }
120 |
121 | var note Note
122 | if err := json.Unmarshal([]byte(noteData), ¬e); err != nil {
123 | if a.logger != nil {
124 | a.logger.Printf("Failed to unmarshal note: %v", err)
125 | }
126 | return fmt.Errorf("failed to unmarshal note: %w", err)
127 | }
128 |
129 | filename := filepath.Join(a.notesDir, note.ID+".json")
130 | if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
131 | if a.logger != nil {
132 | a.logger.Printf("Failed to create directory for note: %v", err)
133 | }
134 | return fmt.Errorf("failed to create directory: %w", err)
135 | }
136 |
137 | if a.logger != nil {
138 | a.logger.Printf("Saving note ID: %s, Title: %s, Content length: %d",
139 | note.ID, note.Title, len(note.Content))
140 | a.logger.Printf("Writing to file: %s", filename)
141 | }
142 |
143 | tempFilename := filename + ".tmp"
144 | if err := os.WriteFile(tempFilename, []byte(noteData), 0644); err != nil {
145 | if a.logger != nil {
146 | a.logger.Printf("Failed to write temporary note file: %v", err)
147 | }
148 | return fmt.Errorf("failed to write temporary note file: %w", err)
149 | }
150 |
151 | data, err := os.ReadFile(tempFilename)
152 | if err != nil {
153 | if a.logger != nil {
154 | a.logger.Printf("Failed to read back temporary file: %v", err)
155 | }
156 | return fmt.Errorf("failed to verify temporary file: %w", err)
157 | }
158 |
159 | if len(data) != len(noteData) {
160 | if a.logger != nil {
161 | a.logger.Printf("Verification failed: file size mismatch. Expected %d, got %d",
162 | len(noteData), len(data))
163 | }
164 | return fmt.Errorf("file size mismatch after writing")
165 | }
166 |
167 | if err := os.Rename(tempFilename, filename); err != nil {
168 | if a.logger != nil {
169 | a.logger.Printf("Failed to rename temporary file: %v", err)
170 | }
171 | return fmt.Errorf("failed to finalize note file: %w", err)
172 | }
173 |
174 | if a.logger != nil {
175 | a.logger.Printf("Note saved successfully. ID: %s, Time taken: %v",
176 | note.ID, time.Since(startTime))
177 | }
178 |
179 | fi, err := os.Stat(filename)
180 | if err != nil {
181 | if a.logger != nil {
182 | a.logger.Printf("Failed to stat the saved file: %v", err)
183 | }
184 | return fmt.Errorf("saved file cannot be accessed: %w", err)
185 | }
186 |
187 | if fi.Size() != int64(len(noteData)) {
188 | if a.logger != nil {
189 | a.logger.Printf("Final verification failed: file size mismatch. Expected %d, got %d",
190 | len(noteData), fi.Size())
191 | }
192 | return fmt.Errorf("final file size mismatch")
193 | }
194 |
195 | return nil
196 | }
197 |
198 | func (a *App) LoadNotes() (string, error) {
199 | startTime := time.Now()
200 | if a.logger != nil {
201 | a.logger.Println("LoadNotes called")
202 | }
203 |
204 | files, err := os.ReadDir(a.notesDir)
205 | if err != nil {
206 | if a.logger != nil {
207 | a.logger.Printf("Failed to read notes directory: %v", err)
208 | }
209 | return "", fmt.Errorf("failed to read notes directory: %w", err)
210 | }
211 |
212 | notes := []Note{}
213 |
214 | if a.logger != nil {
215 | a.logger.Printf("Found %d files in notes directory", len(files))
216 | }
217 |
218 | for _, file := range files {
219 | if file.IsDir() || filepath.Ext(file.Name()) != ".json" {
220 | continue
221 | }
222 |
223 | path := filepath.Join(a.notesDir, file.Name())
224 | content, err := os.ReadFile(path)
225 | if err != nil {
226 | if a.logger != nil {
227 | a.logger.Printf("Failed to read file %s: %v", file.Name(), err)
228 | }
229 | continue
230 | }
231 |
232 | var note Note
233 | if err := json.Unmarshal(content, ¬e); err != nil {
234 | if a.logger != nil {
235 | a.logger.Printf("Failed to parse note file %s: %v", file.Name(), err)
236 | }
237 | continue
238 | }
239 |
240 | if a.logger != nil {
241 | a.logger.Printf("Loaded note ID: %s, Title: %s", note.ID, note.Title)
242 | }
243 |
244 | notes = append(notes, note)
245 | }
246 |
247 | notesJSON, err := json.Marshal(notes)
248 | if err != nil {
249 | if a.logger != nil {
250 | a.logger.Printf("Failed to marshal notes: %v", err)
251 | }
252 | return "", fmt.Errorf("failed to marshal notes: %w", err)
253 | }
254 |
255 | if a.logger != nil {
256 | a.logger.Printf("Loaded %d notes. Time taken: %v", len(notes), time.Since(startTime))
257 | }
258 |
259 | return string(notesJSON), nil
260 | }
261 |
262 | func (a *App) DeleteNote(noteID string) error {
263 | if a.logger != nil {
264 | a.logger.Printf("DeleteNote called for ID: %s", noteID)
265 | }
266 |
267 | filename := filepath.Join(a.notesDir, noteID+".json")
268 |
269 | if err := os.Remove(filename); err != nil {
270 | if a.logger != nil {
271 | a.logger.Printf("Failed to delete note %s: %v", noteID, err)
272 | }
273 | return fmt.Errorf("failed to delete note: %w", err)
274 | }
275 |
276 | if a.logger != nil {
277 | a.logger.Printf("Note %s deleted successfully", noteID)
278 | }
279 |
280 | return nil
281 | }
282 |
--------------------------------------------------------------------------------
/frontend/src/contexts/NotesContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
2 | import { LoadNotes, SaveNote, DeleteNote } from '../../wailsjs/go/main/App.js';
3 |
4 | export interface Note {
5 | id: string;
6 | title: string;
7 | content: string;
8 | createdAt: Date;
9 | updatedAt: Date;
10 | tags?: string[];
11 | }
12 |
13 | interface NotesContextType {
14 | notes: Note[];
15 | filteredNotes: Note[];
16 | activeNoteId: string | null;
17 | searchQuery: string;
18 | setSearchQuery: (query: string) => void;
19 | setActiveNoteId: (id: string | null) => void;
20 | addNote: () => void;
21 | updateNote: (note: Note) => Promise;
22 | deleteNote: (id: string) => void;
23 | getActiveNote: () => Note | null;
24 | isLoading: boolean;
25 | saveStatus: {
26 | isSaving: boolean;
27 | lastSavedId: string | null;
28 | error: string | null;
29 | };
30 | }
31 |
32 | const NotesContext = createContext({
33 | notes: [],
34 | filteredNotes: [],
35 | activeNoteId: null,
36 | searchQuery: '',
37 | setSearchQuery: () => {},
38 | setActiveNoteId: () => {},
39 | addNote: () => {},
40 | updateNote: async () => false,
41 | deleteNote: () => {},
42 | getActiveNote: () => null,
43 | isLoading: false,
44 | saveStatus: {
45 | isSaving: false,
46 | lastSavedId: null,
47 | error: null
48 | }
49 | });
50 |
51 | const initialNotes: Note[] = [
52 | {
53 | id: '1',
54 | title: 'Welcome to VoidNotes',
55 | content: 'Welcome to VoidNotes! Get started by creating your first note.
',
56 | createdAt: new Date(),
57 | updatedAt: new Date(),
58 | tags: ['welcome', 'info']
59 | }
60 | ];
61 |
62 | export const NotesProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
63 | const [notes, setNotes] = useState([]);
64 | const [isLoading, setIsLoading] = useState(true);
65 | const [searchQuery, setSearchQuery] = useState('');
66 | const [activeNoteId, setActiveNoteId] = useState(null);
67 | const [saveStatus, setSaveStatus] = useState({
68 | isSaving: false,
69 | lastSavedId: null as string | null,
70 | error: null as string | null
71 | });
72 |
73 | const saveQueue = useRef