├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── release.yml ├── renovate.json └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── pull_request.yml │ ├── release.yml │ └── tag.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── diagrams │ ├── architecture.excalidraw │ ├── event_outbox.excalidraw │ └── server.excalidraw ├── postman │ ├── api.postman_collection.json │ └── environment.postman_environment.json └── website │ ├── .gitignore │ ├── README.md │ ├── docs │ ├── contributing │ │ ├── _category_.json │ │ ├── code-of-conduct.md │ │ └── pull-requests.md │ ├── developers │ │ ├── _category_.json │ │ ├── architecture.md │ │ ├── backend.md │ │ ├── frontend.md │ │ ├── repo.md │ │ └── setup.md │ ├── features │ │ ├── _category_.json │ │ ├── creating-notes.md │ │ ├── inline-tasks.md │ │ ├── note-editor.md │ │ ├── organizing-notes.md │ │ ├── real-time-updates.md │ │ ├── trash.md │ │ └── user-profile.md │ ├── installation │ │ ├── _category_.json │ │ ├── binary.md │ │ ├── docker-compose.md │ │ ├── docker.md │ │ ├── kubernetes.md │ │ ├── source.md │ │ └── system-requirements.md │ ├── overview │ │ ├── _category_.json │ │ ├── intro.md │ │ └── quick-start.md │ └── troubleshooting │ │ ├── _category_.json │ │ ├── common-issues.md │ │ └── faq.md │ ├── docusaurus.config.ts │ ├── package-lock.json │ ├── package.json │ ├── sidebars.ts │ ├── src │ ├── components │ │ ├── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── RoadmapFeatures │ │ │ ├── styles.module.css │ │ │ └── timeline.tsx │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── roadmap.tsx │ ├── static │ ├── .nojekyll │ └── img │ │ ├── developers │ │ ├── architecture.png │ │ ├── event_outbox.png │ │ └── server.png │ │ ├── favicon.ico │ │ ├── logo │ │ ├── owlistic-192x192.png │ │ ├── owlistic-512x512.png │ │ ├── owlistic-w-text.png │ │ ├── owlistic.png │ │ └── owlistic.svg │ │ └── screenshots │ │ ├── add_plus_button.png │ │ ├── editor │ │ ├── editor.png │ │ ├── editor_toolbar.png │ │ └── note_scrolling.gif │ │ ├── home.png │ │ ├── login.png │ │ ├── notebooks │ │ ├── add_notebook_dialog.png │ │ ├── notebook_actions.png │ │ ├── notebook_add_note_button.png │ │ ├── notebook_add_note_dialog.png │ │ └── notebooks.png │ │ ├── notes │ │ ├── add_note_dialog.png │ │ ├── import_markdown_button.png │ │ ├── import_markdown_dialog.png │ │ ├── move_note_button.png │ │ ├── move_note_dialog.png │ │ ├── note_actions.png │ │ └── notes.png │ │ ├── profile │ │ ├── edit_profile.png │ │ └── profile.png │ │ ├── real_time_updates.gif │ │ ├── register.png │ │ ├── sidebar.png │ │ ├── tasks │ │ ├── add_task_dialog.png │ │ ├── task_actions.png │ │ └── tasks.png │ │ └── trash │ │ ├── delete_permanently_dialog.png │ │ └── trash.png │ └── tsconfig.json └── src ├── backend ├── Dockerfile ├── broker │ ├── consumer.go │ ├── consumer_test.go │ ├── events.go │ ├── producer.go │ └── producer_test.go ├── cmd │ └── main.go ├── config │ └── config.go ├── database │ ├── db.go │ ├── db_test.go │ └── migrations.go ├── go.mod ├── go.sum ├── middleware │ ├── auth_middleware.go │ └── cors_middleware.go ├── models │ ├── block.go │ ├── block_test.go │ ├── event.go │ ├── event_test.go │ ├── note.go │ ├── note_test.go │ ├── notebook.go │ ├── notebook_test.go │ ├── notification_event.go │ ├── notification_event_test.go │ ├── role.go │ ├── task.go │ ├── user.go │ └── websocket_message.go ├── routes │ ├── auth.go │ ├── blocks.go │ ├── blocks_test.go │ ├── debug.go │ ├── notebooks.go │ ├── notebooks_test.go │ ├── notes.go │ ├── notes_test.go │ ├── roles.go │ ├── tasks.go │ ├── tasks_test.go │ ├── trash.go │ ├── users.go │ ├── users_test.go │ └── websocket.go ├── services │ ├── auth_service.go │ ├── block_service.go │ ├── block_service_test.go │ ├── errors.go │ ├── event_handler_service.go │ ├── event_handler_service_test.go │ ├── note_service.go │ ├── note_service_test.go │ ├── notebook_service.go │ ├── notebook_service_test.go │ ├── notification_service.go │ ├── notification_service_test.go │ ├── role_service.go │ ├── role_service_test.go │ ├── sync_handler.go │ ├── task_service.go │ ├── task_service_test.go │ ├── trash_service.go │ ├── trash_service_test.go │ ├── user_service.go │ ├── user_service_test.go │ ├── websocket_service.go │ └── websocket_service_test.go ├── testutils │ ├── event_utils.go │ ├── mock_context.go │ ├── mock_services.go │ └── mockdb.go └── utils │ └── token │ └── token.go └── frontend ├── .metadata ├── Dockerfile ├── README.md ├── analysis_options.yaml ├── assets ├── icon │ └── owlistic.png └── logo │ ├── owlistic-w-text.png │ └── owlistic.png ├── devtools_options.yaml ├── docs └── architecture.md ├── lib ├── core │ ├── providers.dart │ ├── router.dart │ └── theme.dart ├── main.dart ├── models │ ├── block.dart │ ├── note.dart │ ├── notebook.dart │ ├── subscription.dart │ ├── task.dart │ └── user.dart ├── providers │ ├── home_provider.dart │ ├── login_provider.dart │ ├── note_editor_provider.dart │ ├── notebooks_provider.dart │ ├── notes_provider.dart │ ├── register_provider.dart │ ├── tasks_provider.dart │ ├── theme_provider.dart │ ├── trash_provider.dart │ ├── user_profile_provider.dart │ └── websocket_provider.dart ├── screens │ ├── home_screen.dart │ ├── login_screen.dart │ ├── note_editor_screen.dart │ ├── notebook_detail_screen.dart │ ├── notebooks_screen.dart │ ├── notes_screen.dart │ ├── register_screen.dart │ ├── tasks_screen.dart │ ├── trash_screen.dart │ └── user_profile_screen.dart ├── services │ ├── app_state_service.dart │ ├── auth_service.dart │ ├── base_service.dart │ ├── block_service.dart │ ├── note_service.dart │ ├── notebook_service.dart │ ├── task_service.dart │ ├── theme_service.dart │ ├── trash_service.dart │ ├── user_service.dart │ └── websocket_service.dart ├── utils │ ├── attributed_text_utils.dart │ ├── block_node_mapping.dart │ ├── data_converter.dart │ ├── document_builder.dart │ ├── editor_toolbar.dart │ ├── logger.dart │ ├── super_editor_item_selector.dart │ └── websocket_message_parser.dart ├── viewmodel │ ├── base_viewmodel.dart │ ├── home_viewmodel.dart │ ├── login_viewmodel.dart │ ├── note_editor_viewmodel.dart │ ├── notebooks_viewmodel.dart │ ├── notes_viewmodel.dart │ ├── register_viewmodel.dart │ ├── tasks_viewmodel.dart │ ├── theme_viewmodel.dart │ ├── trash_viewmodel.dart │ ├── user_profile_viewmodel.dart │ └── websocket_viewmodel.dart └── widgets │ ├── app_bar_common.dart │ ├── app_drawer.dart │ ├── app_logo.dart │ ├── card_container.dart │ ├── empty_state.dart │ └── theme_switcher.dart ├── pubspec.lock └── pubspec.yaml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | categories: 3 | - title: 🏕 Features 4 | labels: 5 | - feature 6 | 7 | - title: 🚀 Enhancements 8 | labels: 9 | - enhancement 10 | 11 | - title: 🐛 Bug fixes 12 | labels: 13 | - bug 14 | 15 | - title: 📚 Documentation 16 | labels: 17 | - documentation 18 | 19 | - title: 🌐 Translations 20 | labels: 21 | - translation 22 | 23 | - title: 🚨 Breaking Changes 24 | labels: 25 | - breaking-change 26 | 27 | - title: 🫥 Deprecated Changes 28 | labels: 29 | - deprecated 30 | 31 | - title: 🔒 Security 32 | labels: 33 | - security 34 | 35 | - title: 👒 Dependencies 36 | authors: 37 | - renovatebot 38 | 39 | - title: Other Changes 40 | labels: 41 | - "*" 42 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ], 6 | "dependencyDashboard": true, 7 | "dependencyDashboardTitle": "Renovate (Mend) Dashboard", 8 | "suppressNotifications": ["prIgnoreNotification"], 9 | "rebaseWhen": "conflicted", 10 | "commitBodyTable": true, 11 | "platformCommit": "enabled", 12 | "gitAuthor": "Davide Rutigliano ", 13 | "commitBody": "Signed-off-by: Davide Rutigliano ", 14 | "ignorePaths": [], 15 | "commitMessageTopic": "{{depName}}", 16 | "commitMessageExtra": "to {{newVersion}}", 17 | "commitMessageSuffix": "", 18 | "packageRules": [ 19 | { 20 | "matchUpdateTypes": ["major"], 21 | "semanticCommitType": "feat", 22 | "commitMessagePrefix": "{{semanticCommitType}}({{semanticCommitScope}})!:", 23 | "commitMessageExtra": "( {{currentVersion}} → {{newVersion}} )" 24 | }, 25 | { 26 | "matchUpdateTypes": ["minor"], 27 | "semanticCommitType": "feat", 28 | "commitMessageExtra": "( {{currentVersion}} → {{newVersion}} )" 29 | }, 30 | { 31 | "matchUpdateTypes": ["patch"], 32 | "semanticCommitType": "fix", 33 | "commitMessageExtra": "( {{currentVersion}} → {{newVersion}} )" 34 | }, 35 | { 36 | "matchUpdateTypes": ["digest"], 37 | "semanticCommitType": "chore", 38 | "commitMessageExtra": "( {{currentDigestShort}} → {{newDigestShort}} )" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Generate Docs 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - "src/**" 10 | - ".github/**" 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | docs: 18 | name: Deploy and Publish Docs 19 | runs-on: ubuntu-22.04 20 | permissions: 21 | contents: write 22 | defaults: 23 | run: 24 | working-directory: docs/website 25 | steps: 26 | - uses: actions/checkout@v4 27 | 28 | - name: Setup Node.js 29 | uses: actions/setup-node@v4 30 | with: 31 | node-version: '24' 32 | 33 | - name: Get yarn cache 34 | id: yarn-cache 35 | run: echo "YARN_CACHE_DIR=$(yarn cache dir)" >> "${GITHUB_OUTPUT}" 36 | 37 | - name: Cache dependencies 38 | uses: actions/cache@v4 39 | with: 40 | path: ${{ steps.yarn-cache.outputs.YARN_CACHE_DIR }} 41 | key: ${{ runner.os }}-website-${{ hashFiles('**/yarn.lock') }} 42 | restore-keys: | 43 | ${{ runner.os }}-website- 44 | 45 | - run: yarn install --frozen-lockfile 46 | - run: yarn build 47 | 48 | - name: Deploy 49 | uses: peaceiris/actions-gh-pages@v4 50 | if: github.ref == 'refs/heads/main' 51 | with: 52 | github_token: ${{ secrets.REPO_TOKEN }} 53 | publish_dir: ./docs/website/build 54 | -------------------------------------------------------------------------------- /.github/workflows/tag.yml: -------------------------------------------------------------------------------- 1 | name: Tag for Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Release version (e.g., v1.2.3)' 8 | required: true 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | release: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - name: Create Tag 26 | id: tag_version 27 | uses: mathieudutour/github-tag-action@v6.2 28 | with: 29 | github_token: ${{ secrets.REPO_TOKEN }} 30 | custom_tag: ${{ github.event.inputs.version }} 31 | - name: Create GitHub Release 32 | uses: ncipollo/release-action@v1 33 | with: 34 | tag: ${{ steps.tag_version.outputs.new_tag }} 35 | name: Release ${{ steps.tag_version.outputs.new_tag }} 36 | body: ${{ steps.tag_version.outputs.changelog }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Python virtual environments 2 | venv/ 3 | __pycache__/ 4 | 5 | # VSCode settings 6 | .vscode/ 7 | .continue/ 8 | 9 | # intellij 10 | .idea/ 11 | 12 | # Ignore Go build artifacts 13 | *.exe 14 | *.out 15 | *.test 16 | 17 | # Node.js dependencies 18 | node_modules/ 19 | 20 | # Logs 21 | *.log 22 | 23 | # Docker 24 | *.env 25 | 26 | # macOS 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | Icon? 31 | 32 | # Flutter 33 | .flutter-plugins 34 | .flutter-plugins-dependencies 35 | .packages 36 | .pub-cache/ 37 | .pub/ 38 | build/ 39 | ._* 40 | .Spotlight-V100 41 | .Trashes 42 | *.swp 43 | *.dart_tool/ 44 | 45 | # Linux 46 | *~ 47 | .nfs* 48 | 49 | # Git 50 | *.orig 51 | *.rej 52 | *.patch 53 | *.diff 54 | 55 | # Go 56 | *.exe 57 | *.test 58 | *.out 59 | *.log 60 | *.prof 61 | *.cover 62 | vendor/ 63 | bin/ 64 | build/ 65 | dist/ 66 | 67 | # Docker 68 | *.dockerfile 69 | docker-compose.override.yml 70 | .env 71 | .env.* 72 | .dockerignore -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | For detailed guidelines please refer to [Contributing Guidelines](https://owlistic-notes.github.io/owlistic/docs/category/contributing) 2 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | owlistic: 5 | # image: ghcr.io/owlistic-notes/owlistic:0.1.0 6 | build: 7 | context: ./src/backend 8 | dockerfile: Dockerfile 9 | args: 10 | TARGETARCH: ${TARGETARCH:-arm64} 11 | ports: 12 | - "8080:8080" 13 | depends_on: 14 | - postgres 15 | - nats 16 | environment: 17 | - APP_ORIGINS=http://localhost*,http://owlistic*,http://owlistic-app* 18 | - BROKER_ADDRESS=nats:4222 19 | - DB_HOST=postgres 20 | - DB_PORT=5432 21 | - DB_USER=admin 22 | - DB_PASSWORD=admin 23 | - DB_NAME=postgres 24 | networks: 25 | - server 26 | - events 27 | - db 28 | 29 | owlistic-app: 30 | # image: ghcr.io/owlistic-notes/owlistic-app:0.1.0 31 | build: 32 | context: ./src/frontend 33 | dockerfile: Dockerfile 34 | args: 35 | TARGETARCH: ${TARGETARCH:-arm64} 36 | ports: 37 | - "80:80" 38 | depends_on: 39 | - owlistic 40 | 41 | postgres: 42 | image: postgres:15 43 | environment: 44 | POSTGRES_USER: admin 45 | POSTGRES_PASSWORD: admin 46 | ports: 47 | - "5432:5432" 48 | volumes: 49 | - postgres_data:/var/lib/postgresql/data 50 | networks: 51 | - db 52 | 53 | nats: 54 | image: nats 55 | command: 56 | - --http_port 57 | - "8222" 58 | - -js 59 | - -sd 60 | - /var/lib/nats/data 61 | ports: 62 | - "4222:4222" 63 | - "8222:8222" 64 | volumes: 65 | - nats_data:/var/lib/nats/data 66 | networks: 67 | - events 68 | 69 | volumes: 70 | postgres_data: 71 | nats_data: 72 | 73 | networks: 74 | server: 75 | driver: bridge 76 | events: 77 | driver: bridge 78 | db: 79 | driver: bridge -------------------------------------------------------------------------------- /docs/postman/environment.postman_environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "owlistic-env", 3 | "name": "Owlistic Environment", 4 | "values": [ 5 | { 6 | "key": "baseUrl", 7 | "value": "http://localhost:8080", 8 | "enabled": true 9 | }, 10 | { 11 | "key": "wsBaseUrl", 12 | "value": "ws://localhost:8080", 13 | "type": "default", 14 | "enabled": true 15 | }, 16 | { 17 | "key": "accessToken", 18 | "value": "", 19 | "type": "secret", 20 | "enabled": true 21 | }, 22 | { 23 | "key": "refreshToken", 24 | "value": "", 25 | "type": "secret", 26 | "enabled": true 27 | }, 28 | { 29 | "key": "userId", 30 | "value": "90a12345-f12a-98c4-a456-513432930000", 31 | "type": "default", 32 | "enabled": true 33 | }, 34 | { 35 | "key": "noteId", 36 | "value": "", 37 | "type": "default", 38 | "enabled": true 39 | }, 40 | { 41 | "key": "taskId", 42 | "value": "", 43 | "type": "default", 44 | "enabled": true 45 | }, 46 | { 47 | "key": "notebookId", 48 | "value": "", 49 | "type": "default", 50 | "enabled": true 51 | }, 52 | { 53 | "key": "blockId", 54 | "value": "", 55 | "type": "default", 56 | "enabled": true 57 | } 58 | ], 59 | "_postman_variable_scope": "environment" 60 | } 61 | -------------------------------------------------------------------------------- /docs/website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/website/docs/contributing/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Contributing", 3 | "position": 6, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/website/docs/contributing/pull-requests.md: -------------------------------------------------------------------------------- 1 | # Pull Requests 2 | 3 | Thank you for your interest in contributing to our Owlistic! This document will guide you through the process of submitting a pull request. 4 | 5 | ## How to Submit a Pull Request 6 | 7 | 1. **Fork the Repository**: Start by forking the repository to your own GitHub account. This allows you to make changes without affecting the original project. 8 | 9 | 2. **Clone Your Fork**: Clone your forked repository to your local machine using the following command: 10 | ```bash 11 | git clone https://github.com/owlistic-notes/owlistic.git 12 | ``` 13 | 14 | 3. **Create a New Branch**: Before making any changes, create a new branch for your feature or bug fix: 15 | ```bash 16 | git checkout -b my-feature-branch 17 | ``` 18 | 19 | 4. **Make Your Changes**: Implement your changes in the codebase. Ensure that your code adheres to the project's coding standards and guidelines. 20 | 21 | 5. **Commit Your Changes**: Once you are satisfied with your changes, commit them with a clear and descriptive message: 22 | ```bash 23 | git commit -m "Add a new feature" 24 | ``` 25 | 26 | 6. **Push Your Changes**: Push your changes to your forked repository: 27 | ```bash 28 | git push origin my-feature-branch 29 | ``` 30 | 31 | 7. **Open a Pull Request**: Go to the original repository on GitHub and click on the "Pull Requests" tab. Click on the "New Pull Request" button. Select your branch and submit the pull request. 32 | 33 | ## Pull Request Guidelines 34 | 35 | - **Descriptive Title**: Provide a clear and concise title for your pull request that summarizes the changes made. 36 | - **Detailed Description**: Include a detailed description of the changes, why they were made, and any relevant context. 37 | - **Link to Issues**: If your pull request addresses an existing issue, please reference it in the description (e.g., "Fixes #123"). 38 | - **Testing**: Ensure that your changes are tested and do not break existing functionality. Include tests if applicable. 39 | 40 | ## Review Process 41 | 42 | Once your pull request is submitted, the maintainers will review your changes. They may request modifications or provide feedback. Please be responsive to their comments and make necessary adjustments. 43 | 44 | Thank you for contributing to our project! Your efforts help improve the Owlistic for everyone. -------------------------------------------------------------------------------- /docs/website/docs/developers/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Developers", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/website/docs/developers/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Architecture 6 | 7 | Owlistic uses a traditional client-server design, with a dedicated database for data persistence. On top of that, it leverages an event streaming system to push real-time updates to clients. Clients communicate with server over HTTP using REST APIs and listen over a websocket connection for server events. Below is a high level diagram of the architecture. 8 | 9 | ## High Level Design 10 | 11 | ![Architecture](/img/developers/architecture.png) 12 | 13 | The diagram shows clients communicating with the server REST APIs for CRUD (Create, Read, Update, Delete) operations and listening to server events over a websocket connection. 14 | 15 | The server exposes a gateway layer for REST APIs and a websocket connection. The API gateway is responsible for routing requests to the appropriate service, while the WebSocket gateway is responsible for and handling events. 16 | 17 | Under the hood, the server communicates with downstream systems (i.e. Postgres, NATS) through data models, both for data persistence and event streaming. 18 | 19 | ## Technology Stack 20 | 21 | ### Server 22 | 23 | go logo 24 | 25 | postgresql logo 26 | 27 | nats logo 28 | 29 | 30 | Owlistic server is built using Go, a statically typed, compiled language. The main reason for choosing Go is its strong support for concurrency and efficient memory management, which are crucial for event-driven systems. It also uses the PostgreSQL database for data persistence and NATS as an event streaming system to push real-time updates to clients. 31 | 32 | Please refer to the [Server](developers/backend.md) section for more details. 33 | 34 | ### Client (Web/Mobile/Desktop App) 35 | 36 | flutter logo 37 | 38 | dart logo 39 | 40 | 41 | Owlistic client is built using Flutter, a popular open-source framework for building cross-platform applications. The client provides a user-friendly interface to interact with the server and access the features of Owlistic. The main reason for choosing Flutter is its ease of use and ability to create cross-platforms mobile apps using the same codebase with minimal code changes. 42 | 43 | Please refer to the [App](developers/frontend.md) section for more details. 44 | -------------------------------------------------------------------------------- /docs/website/docs/developers/frontend.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # App 6 | 7 | ## Design 8 | 9 | ### Data Models 10 | 11 | ### Views and View Models 12 | 13 | ### Providers (Controllers) 14 | -------------------------------------------------------------------------------- /docs/website/docs/developers/repo.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Repository Structure 6 | 7 | 8 | ## Directory Structure 9 | 10 | The repository is organized into several directories to facilitate code organization and maintainability. Here's a breakdown of the main directories: 11 | 12 | | Folder | Description | 13 | | :------------------ | :--------------------------------------------------------------------------------- | 14 | | `.github` | Github repo templates and action workflows | 15 | | `src/backend` | Source code for the mobile app | 16 | | `src/frontend` | Source code for the server app | 17 | | `docs/diagrams` | High level design diagrams for the project | 18 | | `docs/website` | Source code for the [Owlistic website](https://owlistic-notes.github.io/owlistic/) | 19 | 20 | ### Server (Backend) 21 | 22 | | Folder | Description | 23 | | :----------------------- | :--------------------------------------------------| 24 | | `src/backend/cmd` | Server main app | 25 | | `src/backend/config` | Configuration files for the server app | 26 | | `src/backend/database` | Database schema and operations | 27 | | `src/backend/models` | Data models used by the server app | 28 | | `src/backend/middleware` | Middlewares used in the server app | 29 | | `src/backend/routes` | API routes for the server app | 30 | | `src/backend/services` | Business logic services for the server app | 31 | | `src/backend/broker` | Message broker for handling requests and responses | 32 | | `src/backend/utils` | Utility functions for the server app | 33 | 34 | 35 | ### App (Frontend) 36 | 37 | | Folder | Description | 38 | | :--------------------------- | :--------------------------------------------- | 39 | | `src/frontend/lib/core` | Core functionality of the mobile app | 40 | | `src/frontend/lib/models` | Data models used by the mobile app | 41 | | `src/frontend/lib/providers` | Providers for services used in the mobile app | 42 | | `src/frontend/lib/screens` | Screens and layouts for the mobile app | 43 | | `src/frontend/lib/services` | Business logic services for the mobile app | 44 | | `src/frontend/lib/viewmodel` | View models for the mobile app | 45 | | `src/frontend/lib/widgets` | Custom widgets used in the mobile app | 46 | | `src/frontend/lib/utils` | Utility functions for the mobile app | 47 | -------------------------------------------------------------------------------- /docs/website/docs/developers/setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | Follow the steps below to set up your development environment. 4 | 5 | ## Prerequisites 6 | 7 | Before you begin, ensure you have the following installed: 8 | 9 | - **Go** (version 1.23 or above) for the server 10 | - **Flutter** (latest stable version) for the App 11 | - **Git** (for version control) 12 | - **Docker** and **Docker Compose** (for local deployment with dependencies) 13 | - A code editor of your choice (e.g., Visual Studio Code) 14 | 15 | ## Cloning the Repository 16 | 17 | First, clone the repository to your local machine: 18 | 19 | ```bash 20 | git clone https://github.com/owlistic-notes/owlistic.git 21 | cd owlistic 22 | ``` 23 | 24 | ## Development Workflow 25 | 26 | Owlistic consists of two main components: 27 | 28 | 1. **Server**: Written in Go 29 | 2. **App**: Flutter web application 30 | 31 | ### Setting Up the Server 32 | 33 | Navigate to the server directory and build the Go application: 34 | 35 | ```bash 36 | cd src/backend 37 | go mod download 38 | go build -o build/owlistic cmd/main.go 39 | ``` 40 | 41 | #### Running the Server 42 | 43 | Before running the server, ensure PostgreSQL and NATS are available. You can use Docker Compose for this: 44 | 45 | ```bash 46 | # From the project root directory 47 | docker-compose up -d postgres nats 48 | ``` 49 | 50 | Configure environment variables: 51 | 52 | ```bash 53 | export DB_HOST=localhost 54 | export DB_PORT=5432 55 | export DB_USER=admin 56 | export DB_PASSWORD=admin 57 | export DB_NAME=postgres 58 | export BROKER_ADDRESS=localhost:9092 59 | ``` 60 | 61 | Run the server: 62 | 63 | ```bash 64 | cd src/backend 65 | ./build/owlistic 66 | ``` 67 | 68 | The server should now be running on `http://localhost:8080`. 69 | 70 | ### Setting Up the App 71 | 72 | Navigate to the Flutter app directory: 73 | 74 | ```bash 75 | cd src/frontend 76 | ``` 77 | 78 | Install Flutter dependencies: 79 | 80 | ```bash 81 | flutter pub get 82 | ``` 83 | 84 | #### Running the App 85 | 86 | Start the Flutter web application in development mode: 87 | 88 | ```bash 89 | flutter run -d chrome 90 | ``` 91 | 92 | This will launch the application in Chrome. 93 | 94 | ## Making Changes 95 | 96 | You can now make changes to the codebase. Here are some guidelines: 97 | 98 | - Server (Go): Changes will require recompilation and restarting the server 99 | - App (Flutter): Many changes will automatically reload in the browser 100 | 101 | ## Testing Your Changes 102 | 103 | Before submitting your contributions, ensure that all tests pass: 104 | 105 | ```bash 106 | # For server tests 107 | cd src/backend 108 | go test ./... 109 | 110 | # For app tests 111 | cd src/frontend 112 | flutter test 113 | ``` 114 | 115 | ## Full Development Environment with Docker Compose 116 | 117 | For convenience, you can use Docker Compose to run the entire application stack: 118 | 119 | ```bash 120 | docker-compose up -d 121 | ``` 122 | 123 | This will start PostgreSQL, NATS, and the Owlistic server and app. 124 | 125 | ## Submitting Your Changes 126 | 127 | Once you are satisfied with your changes, commit them and push to your forked repository: 128 | 129 | ```bash 130 | git add . 131 | git commit -m "Your descriptive commit message" 132 | git push origin your-branch-name 133 | ``` 134 | 135 | Finally, create a pull request to the main repository for review. 136 | 137 | Thank you for contributing to Owlistic! 138 | -------------------------------------------------------------------------------- /docs/website/docs/features/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Features", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Explore the various features of the Owlistic." 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/features/creating-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Notes 6 | 7 | ## Intro 8 | 9 | Creating notes in Owlistic is a straightforward process. 10 | 11 | 12 | 13 | ## Creating notes 14 | 15 | To create a new notebook, press . You can then enter a title for your notebook and save it. 16 | 17 | 18 | 19 | ## Importing notes from Markdown files 20 | 21 | Owlistic also supports importing notes from existing Markdown (.md) files. To import a note, use the . 22 | 23 | After pressing the "Import" button, you will be prompted to select the Markdown file you want to import. Once selected, the note will be created in Owlistic with its title and content. 24 | 25 | 26 | 27 | ## Editing notes 28 | 29 | 30 | 31 | Once you created your note you can press on it to open the [note editor](./note-editor.md) and start typing your content. The editor supports Markdown, allowing you to format your text easily. 32 | 33 | 34 | 35 | ## Moving notes to notebooks 36 | 37 | Once you have created a note, you can move it to another notebook by tapping 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/website/docs/features/inline-tasks.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Tasks 6 | 7 | ## Overview 8 | 9 | Owlistic supports the creation and management of tasks. You can create a task by tapping on the "+" button in the top right corner of the screen. 10 | 11 | 12 | 13 | ## Creating tasks 14 | 15 | To create a new notebook, press . You can then enter a title for your notebook and save it. 16 | 17 | 18 | 19 | ## Editing tasks 20 | 21 | 22 | 23 | ## Inline tasks 24 | 25 | -------------------------------------------------------------------------------- /docs/website/docs/features/note-editor.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Rich Text Editor 6 | 7 | ## Overview 8 | 9 | Owlistic note editor is a powerful WYSIWYG (What You See Is What You Get) editor that enables you to create notes with ease. With its user-friendly interface and advanced features, Owlistic makes it easy for you to create notes that are both visually appealing and informative. 10 | 11 | 12 | 13 | Creating notes in the application is a straightforward process that allows you to capture your thoughts, ideas, and important information quickly. Follow the steps below to create your first note. 14 | 15 | ## Toolbar 16 | 17 | The popover toolbar provides quick access to common formatting options such as bold, italic, underline, and more. You can also use the toolbar to insert links, change block type and add new blocks to your notes. 18 | 19 | 20 | 21 | ## Inline Markdown Formatting 22 | 23 | The editor also supports a variety of Markdown features to enhance your note-taking experience. Below are the key Markdown functionalities available: 24 | 25 | ### Headings 26 | 27 | Use `# ` for headings. The number of `#` symbols indicates the heading level. 28 | 29 | ```markdown 30 | # Heading 1 31 | ## Heading 2 32 | ### Heading 3 33 | ``` 34 | 35 | ### Emphasis 36 | 37 | You can emphasize text using asterisks or underscores. 38 | 39 | ```markdown 40 | *italic* or _italic_ 41 | **bold** or __bold__ 42 | ``` 43 | 44 | :::tip 45 | If you are on desktop, you can also use keyboard shortcuts to format text. 46 | ::: 47 | 48 | ### Lists 49 | 50 | Create ordered and unordered lists easily. 51 | 52 | ```markdown 53 | - Item 1 54 | - Item 2 55 | - Subitem 1 56 | - Subitem 2 57 | 58 | 1. First item 59 | 2. Second item 60 | ``` 61 | 62 | ### Links 63 | 64 | ```markdown 65 | [Link text](http://example.com) 66 | ``` 67 | 68 | ## Scrolling 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/website/docs/features/organizing-notes.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Notebooks 6 | 7 | ## Overview 8 | 9 | Owlistic supports organizing notes in notebooks. A notebook is a collection of notes that you can organize and manage. Each note in a notebook has its own unique title, which makes it easy to find and access your notes quickly. 10 | 11 | 12 | 13 | ## Creating new notebooks 14 | 15 | To create a new notebook, press . You can then enter a title for your notebook and save it. 16 | 17 | 18 | 19 | ## Adding notes to notebooks 20 | 21 | Once you have created a notebook, you can add notes to it by pressing 22 | 23 | Then, you can specify the new note title. 24 | 25 | 26 | 27 | ### Notes screen 28 | 29 | Notes can be also created from Notes screen by pressing . You can then enter select a notebook for the note and a title, and save it. 30 | 31 | 32 | 33 | ## Editing notebooks 34 | 35 | 36 | 37 | ## Moving notes to notebooks 38 | 39 | Once you have created a note, you can move it to another notebook by pressing the "Move" button 40 | 41 | -------------------------------------------------------------------------------- /docs/website/docs/features/real-time-updates.md: -------------------------------------------------------------------------------- 1 | # Real Time Updates 2 | 3 | ## Intro 4 | 5 | Owlistic supports real-time updates allowing you to keep your knowledge synchronized and up-to-date across devices. 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/website/docs/features/trash.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Trash 6 | 7 | ## Overview 8 | 9 | Owlistic support "soft-deletion" model for notebooks and notes, allowing you to recover deleted items. You can find a list of all your deleted items in the trash. 10 | 11 | 12 | 13 | You can restore an item from the trash by pressing restore button. You can also permanently delete an item by pressing the delete button, or permanently delete all items in the trash by pressing the "Empty Trash" button. 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/website/docs/features/user-profile.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # User Profile 6 | 7 | ## Overview 8 | 9 | Owlistic supports basic user profile management, allowing you to customize your account settings (such as name, email address, display name) and authentication preferences. 10 | 11 | 12 | 13 | 14 | ## Editing your profile 15 | 16 | To edit your profile in Owlistic, press the "Profile" button located at the top right corner of the screen. This will open a dialog where you can update your user information such as name, email address, and display name. 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/website/docs/installation/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Installation", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn how to install Owlistic." 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/installation/binary.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Binary Installation 6 | 7 | ## Prerequisites 8 | 9 | Before installation, ensure you have: 10 | 11 | - Read the [System Requirements](system-requirements.md) 12 | - Set up PostgreSQL and NATS (required for storage and real-time synchronization) 13 | - [Flutter](https://flutter.dev/docs/get-started/install) installed on your system (required for building the Flutter web app) 14 | 15 | ## Steps 16 | 17 | ### Step 1: Download the Binary 18 | 19 | ```bash 20 | # For Linux (amd64) 21 | curl -LO https://github.com/owlistic-notes/owlistic/releases/latest/download/owlistic 22 | # Make owlistic executable 23 | chmod +x owlistic 24 | 25 | curl -L https://github.com/owlistic-notes/owlistic/releases/latest/download/owlistic-app.zip -o owlistic-app.zip 26 | # Extract the UI files 27 | unzip owlistic-app.zip -d owlistic-app 28 | ``` 29 | 30 | ### Step 2: Configure Environment Variables 31 | 32 | Set the required environment variables: 33 | 34 | ```bash 35 | export APP_ORIGINS=http://localhost* 36 | export DB_HOST=localhost 37 | export DB_PORT=5432 38 | export DB_USER=admin 39 | export DB_PASSWORD=admin 40 | export DB_NAME=postgres 41 | export BROKER_ADDRESS=localhost:9092 42 | ``` 43 | 44 | ### Step 3: Run the Application 45 | 46 | ```bash 47 | # Start the server application 48 | ./owlistic 49 | 50 | # Serve the UI using a simple HTTP server 51 | cd owlistic-app 52 | flutter run -d 53 | ``` 54 | 55 | ## Post-Installation 56 | 57 | After installation: 58 | - The server should be running on port 8080 59 | - The app should be accessible on port 80 60 | - Visit `http://your-server` to access the web interface 61 | 62 | ## Troubleshooting 63 | 64 | If you encounter any issues during installation, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for assistance. 65 | -------------------------------------------------------------------------------- /docs/website/docs/installation/docker-compose.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Docker Compose Installation (Recommended) 6 | 7 | ## Prerequisites 8 | 9 | Before installation, ensure you have: 10 | 11 | - Read the [System Requirements](system-requirements.md) 12 | - Docker installed on your system 13 | 14 | ## Steps 15 | 16 | ### Step 1: Create Docker Compose File 17 | 18 | Create a file named `docker-compose.yml`: 19 | 20 | ```yaml 21 | version: '3.8' 22 | 23 | services: 24 | owlistic: 25 | image: ghcr.io/owlistic-notes/owlistic:latest 26 | ports: 27 | - "8080:8080" 28 | depends_on: 29 | - postgres 30 | - nats 31 | environment: 32 | - APP_ORIGINS=http://owlistic*,http://owlistic-app* 33 | - DB_HOST=postgres 34 | - DB_PORT=5432 35 | - DB_USER=admin 36 | - DB_PASSWORD=admin 37 | - DB_NAME=postgres 38 | - BROKER_ADDRESS=nats:4222 39 | 40 | owlistic-app: 41 | image: ghcr.io/owlistic-notes/owlistic-app:latest 42 | ports: 43 | - "80:80" 44 | depends_on: 45 | - owlistic 46 | 47 | postgres: 48 | image: postgres:15 49 | environment: 50 | POSTGRES_USER: admin 51 | POSTGRES_PASSWORD: admin 52 | ports: 53 | - "5432:5432" 54 | volumes: 55 | - postgres_data:/var/lib/postgresql/data 56 | 57 | nats: 58 | image: nats 59 | command: 60 | - --http_port 61 | - "8222" 62 | - -js 63 | - -sd 64 | - /var/lib/nats/data 65 | ports: 66 | - "4222:4222" 67 | - "8222:8222" 68 | volumes: 69 | - nats_data:/var/lib/nats/data 70 | 71 | volumes: 72 | postgres_data: 73 | nats_data: 74 | ``` 75 | 76 | ### Step 2: Start the Services 77 | 78 | ```bash 79 | docker-compose up -d 80 | ``` 81 | 82 | ## Post-Installation 83 | 84 | After installation: 85 | - The server should be running on port 8080 86 | - The app should be accessible on port 80 87 | - Visit `http://your-server` to access the web interface 88 | 89 | ## Troubleshooting 90 | 91 | If you encounter any issues during installation, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for assistance. 92 | -------------------------------------------------------------------------------- /docs/website/docs/installation/docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Docker Installation 6 | 7 | ## Prerequisites 8 | 9 | Before installation, ensure you have: 10 | 11 | - Read the [System Requirements](system-requirements.md) 12 | - Docker installed on your system 13 | - Set up PostgreSQL and NATS (either in docker or on your local machine) 14 | 15 | ## Steps 16 | 17 | ### Step 1: Set up PostgreSQL and NATS containers in Docker (if not already running) 18 | 19 | ```yaml 20 | version: '3.8' 21 | 22 | services: 23 | postgres: 24 | image: postgres:15 25 | environment: 26 | POSTGRES_USER: admin 27 | POSTGRES_PASSWORD: admin 28 | ports: 29 | - "5432:5432" 30 | volumes: 31 | - postgres_data:/var/lib/postgresql/data 32 | 33 | nats: 34 | image: nats 35 | command: 36 | - --http_port 37 | - "8222" 38 | - -js 39 | - -sd 40 | - /var/lib/nats/data 41 | ports: 42 | - "4222:4222" 43 | - "8222:8222" 44 | volumes: 45 | - nats_data:/var/lib/nats/data 46 | 47 | volumes: 48 | postgres_data: 49 | nats_data: 50 | ``` 51 | 52 | ### Step 2.1 Using Pre-built Images 53 | 54 | ```bash 55 | # Pull the server image 56 | docker pull ghcr.io/owlistic-notes/owlistic:latest 57 | 58 | # Pull the app image 59 | docker pull ghcr.io/owlistic-notes/owlistic-app:latest 60 | ``` 61 | 62 | ```bash 63 | # Run the server 64 | docker run -d \ 65 | --name owlistic \ 66 | -p 8080:8080 \ 67 | -e APP_PORT=8080 \ 68 | -e DB_HOST=postgres \ 69 | -e DB_PORT=5432 \ 70 | -e DB_USER=admin \ 71 | -e DB_PASSWORD=admin \ 72 | -e DB_NAME=postgres \ 73 | -e BROKER_ADDRESS=nats:4222 \ 74 | ghcr.io/owlistic-notes/owlistic:latest 75 | 76 | # Run the app 77 | docker run -d \ 78 | --name owlistic-app \ 79 | -p 80:80 \ 80 | ghcr.io/owlistic-notes/owlistic-app:latest 81 | ``` 82 | 83 | Note: The above commands assume you have PostgreSQL and NATS running and accessible respectively at `postgres:5432` and `nats:4222`. 84 | 85 | ### Step 2.2: Building from Source 86 | 87 | ```bash 88 | 89 | # Build the server image 90 | docker build -t owlistic:latest . 91 | 92 | # Build the app image 93 | docker build -t owlistic-app:latest . 94 | 95 | # Run the server container 96 | docker run -d \ 97 | --name owlistic \ 98 | -p 8080:8080 \ 99 | -e APP_PORT=8080 \ 100 | -e DB_HOST=postgres \ 101 | -e DB_PORT=5432 \ 102 | -e DB_NAME=owlistic \ 103 | -e DB_USER=admin \ 104 | -e DB_PASSWORD=admin \ 105 | -e BROKER_ADDRESS=nats:4222 \ 106 | owlistic 107 | 108 | # Run the app container 109 | docker run -d \ 110 | --name owlistic-app \ 111 | -p 80:80 \ 112 | owlistic-app 113 | ``` 114 | 115 | Note: The above commands assume you have PostgreSQL and NATS running and accessible respectively at `postgres:5432` and `nats:4222`. 116 | 117 | ## Post-Installation 118 | 119 | After installation: 120 | - The server should be running on port 8080 121 | - The app should be accessible on port 80 122 | - Visit `http://your-server` to access the web interface 123 | 124 | ## Troubleshooting 125 | 126 | If you encounter any issues during installation, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for assistance. 127 | -------------------------------------------------------------------------------- /docs/website/docs/installation/kubernetes.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Kubernetes Installation (Recommended) 6 | 7 | ## Prerequisites 8 | 9 | Before installation, ensure you have: 10 | 11 | - Read the [System Requirements](system-requirements.md) 12 | - A running Kubernetes cluster 13 | - [Helm](https://helm.sh) installed locally 14 | 15 | ## Kubernetes/Helm Installation 16 | 17 | Owlistic supports deployment on Kubernetes using Helm. Follow these steps to install [Owlistic helm chart](https://github.com/owlistic-notes/helm-charts) on your Kubernetes cluster. 18 | 19 | ### Step 1: Add the Owlistic Helm Repository 20 | 21 | ```bash 22 | helm repo add owlistic https://owlistic-notes.github.io/helm-charts 23 | helm repo update 24 | ``` 25 | 26 | ### Step 2: Install Using Helm 27 | 28 | ```bash 29 | # Create a values file (values.yaml) for your configuration 30 | helm install owlistic owlistic/owlistic -f values.yaml 31 | ``` 32 | 33 | Example `values.yaml`: 34 | 35 | ```yaml 36 | server: 37 | enabled: true 38 | service: 39 | enabled: true 40 | type: ClusterIP 41 | port: 8080 42 | persistence: 43 | data: 44 | enabled: true 45 | existingClaim: 46 | env: 47 | DB_HOST: postgresql 48 | DB_PORT: 5432 49 | DB_NAME: owlistic 50 | DB_USER: owlistic 51 | DB_PASSWORD: owlistic 52 | BROKER_ADDRESS: nats:4222 53 | 54 | app: 55 | enabled: true 56 | service: 57 | enabled: true 58 | type: ClusterIP 59 | port: 80 60 | 61 | postgresql: 62 | enabled: true 63 | global: 64 | postgresql: 65 | auth: 66 | username: owlistic 67 | password: owlistic 68 | database: owlistic 69 | 70 | nats: 71 | enabled: true 72 | config: 73 | jetstream: 74 | enabled: true 75 | fileStore: 76 | enabled: true 77 | pvc: 78 | enabled: false 79 | size: 512Mi 80 | storageClassName: "-" 81 | ``` 82 | 83 | ### Step 3: Verify the Installation 84 | 85 | ```bash 86 | kubectl get pods -l app.kubernetes.io/name=owlistic 87 | kubectl get services -l app.kubernetes.io/name=owlistic 88 | ``` 89 | 90 | ## Post-Installation 91 | 92 | After installation: 93 | - The server should be running on port 8080 94 | - The app should be accessible on port 80 95 | - Visit `http://your-server` to access the web interface 96 | 97 | ## Troubleshooting 98 | 99 | If you encounter any issues during installation, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for assistance. 100 | -------------------------------------------------------------------------------- /docs/website/docs/installation/source.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Build From Source 6 | 7 | ## Prerequisites 8 | 9 | Before installation, ensure you have: 10 | 11 | - Read the [System Requirements](system-requirements.md) 12 | - Set up PostgreSQL and NATS (required for storage and real-time synchronization) 13 | - [Flutter](https://flutter.dev/docs/get-started/install) installed on your system (required for building the Flutter web app) 14 | 15 | ## Building from Source 16 | 17 | If you prefer to build from source: 18 | 19 | ```bash 20 | # Clone the repository 21 | git clone https://github.com/owlistic-notes/owlistic.git 22 | cd owlistic 23 | ``` 24 | 25 | ### Step 1: Building the server 26 | 27 | ``` 28 | # Build the server 29 | cd src/backend 30 | go build -o owlistic cmd/main.go 31 | ``` 32 | 33 | ### Step 2: Building the Flutter Web UI 34 | 35 | To build the Flutter web application: 36 | 37 | ```bash 38 | # Navigate to the app directory 39 | cd src/frontend 40 | 41 | # Ensure Flutter dependencies are installed 42 | flutter pub get 43 | ``` 44 | 45 | This will generate the web artifacts in the `build/web` directory, which can be deployed to any web server. 46 | 47 | 48 | ### Step 3: Configure Environment Variables 49 | 50 | Set the required environment variables: 51 | 52 | ```bash 53 | export APP_ORIGINS=http://localhost* 54 | export DB_HOST=localhost 55 | export DB_PORT=5432 56 | export DB_USER=admin 57 | export DB_PASSWORD=admin 58 | export DB_NAME=postgres 59 | export BROKER_ADDRESS=localhost:9092 60 | ``` 61 | 62 | ### Step 4: Run the Application 63 | 64 | ```bash 65 | # Start the server application 66 | cd src/backend 67 | ./owlistic 68 | 69 | # Run the flutter app 70 | cd src/frontend 71 | flutter run -d 72 | ``` 73 | 74 | ## Post-Installation 75 | 76 | After installation: 77 | - The server should be running on port 8080 78 | - The app should be accessible on port 80 79 | - Visit `http://your-server` to access the web interface 80 | 81 | ## Troubleshooting 82 | 83 | If you encounter any issues during installation, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for assistance. 84 | -------------------------------------------------------------------------------- /docs/website/docs/installation/system-requirements.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | 3 | To ensure optimal performance and functionality of the Owlistic, please ensure your system meets the following requirements: 4 | 5 | ## Minimum System Requirements 6 | 7 | - **Operating System**: Windows 10, macOS 10.14 (Mojave), or a recent version of Linux (Ubuntu 20.04 or later). 8 | - **Processor**: Dual-core CPU with a clock speed of 2.0 GHz or higher. 9 | - **RAM**: 4 GB of RAM. 10 | - **Storage**: At least 500 MB of free disk space. 11 | - **Node.js**: Version 14.x or later. 12 | - **Internet Connection**: Required for initial setup and updates. 13 | 14 | ## Recommended System Requirements 15 | 16 | - **Operating System**: Windows 10, macOS 11 (Big Sur), or a recent version of Linux (Ubuntu 22.04 or later). 17 | - **Processor**: Quad-core CPU with a clock speed of 2.5 GHz or higher. 18 | - **RAM**: 8 GB of RAM or more. 19 | - **Storage**: At least 1 GB of free disk space. 20 | - **Node.js**: Version 16.x or later. 21 | - **Internet Connection**: Stable broadband connection for optimal performance. 22 | 23 | ## Additional Notes 24 | 25 | - Ensure that your system has the latest updates installed for your operating system. 26 | - For the best experience, consider using a modern web browser such as Google Chrome, Mozilla Firefox, or Microsoft Edge. -------------------------------------------------------------------------------- /docs/website/docs/overview/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Overview", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Owlistic overview and quick start guides" 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/overview/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Introduction 6 | 7 | ![Owlistic Logo](/img/logo/owlistic-192x192.png) 8 | 9 | ## Welcome to Owlistic! 10 | 11 | Hi, I'm the creator of **Owlistic**, an open-source, event-driven note-taking app that I originally built for myself. 12 | 13 | Owlistic started out of a simple need: I have been a long time user the _Best Note Taking App_ that not-so-recently became almost unusable without paid subscription. Also, since I started my self-hosting journey, I was already looking for an alternative that was free and open-source, without loosing the user experience and performance of such _Best Note Taking App_. 14 | 15 | So that's where things paired up. I needed a simple-to-use note-taking app with a native desktop/mobile app, with support for inline tasks, that was blazing fast at synchronizing notes and tasks across my devices. And so, I started building Owlistic and took it as my personal deep dive into event-driven system design. 16 | 17 | By contributing Owlistic to the open-source community, I'd like to think I am also giving back to the open-source community from which I have greatly benefit. It's a small way for me to pay it forward and help others build their own solutions, free from the limitations of proprietary software. 18 | 19 | I'm excited to share this project with the community and invite you to join me in making it even better! 20 | 21 | ## Getting Started 22 | 23 | Ready to start using Owlistic? Check out the [Quick Start Guide](./quick-start) to get it up and running. 24 | 25 | ## Contributing 26 | 27 | Owlistic is developed by the community, for the community. We welcome contributions of all kinds - from code improvements to documentation updates. Check out our [Contributing Guide](/docs/category/contributing) to learn how you can help. 28 | -------------------------------------------------------------------------------- /docs/website/docs/overview/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Quick Start 6 | 7 | Welcome to the Quick Start Guide! This guide will help you get it up and running. 8 | 9 | ![Owlistic Demo](/img/screenshots/real_time_updates.gif) 10 | 11 | ## Installation 12 | 13 | Before you begin, ensure that you have completed the installation process using your preferred [install option](/docs/category/installation). 14 | 15 | ## Launch the Application 16 | 17 | Once installed, you can launch the application you can access Owlistic at `http://your-website` or using [Progrssive Web App](https://en.wikipedia.org/wiki/Progressive_web_app) (PWA). 18 | 19 | :::info 20 | 21 | Stay tuned for Mobile and Desktop apps soon! 22 | 23 | ::: 24 | 25 | ### Register into Owlistic 26 | 27 | ![Register screen](/img/screenshots/register.png) 28 | 29 | ### Login into Owlistic 30 | 31 | ![Login screen](/img/screenshots/login.png) 32 | 33 | ### Explore Owlistic 34 | 35 | After you login into the application, you will be redirected to the home screen. 36 | 37 | ![Home screen](/img/screenshots/home.png) 38 | 39 | :::tip 40 | Use sidebar for quick access to all the features of Owlistic. 41 | ::: 42 | 43 | ![Sidebar](/img/screenshots/sidebar.png) 44 | 45 | 46 | ## What's next? 47 | 48 | - Start exploring [Owlistic features](/docs/category/features) and keep your knowledge organized! 49 | -------------------------------------------------------------------------------- /docs/website/docs/troubleshooting/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Troubleshooting", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index" 6 | } 7 | } -------------------------------------------------------------------------------- /docs/website/docs/troubleshooting/common-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Common Issues 6 | 7 | This section addresses some of the most common issues users may encounter while using the Owlistic, along with their solutions. 8 | 9 | ## Issue 1: Application Won't Start 10 | 11 | **Symptoms:** The application fails to launch, and no error messages are displayed. 12 | 13 | **Solution:** 14 | - Ensure that all system requirements are met. 15 | - Check if the necessary dependencies are installed. Run the installation command again to ensure all packages are correctly set up. 16 | 17 | ## Issue 2: Unable to Save Notes 18 | 19 | **Symptoms:** Changes made to notes are not being saved. 20 | 21 | **Solution:** 22 | - Verify that you have write permissions for the directory where the notes are stored. 23 | - Check if the application has sufficient storage space available. 24 | 25 | ## Issue 3: Database Connection Errors 26 | 27 | **Symptoms:** The application displays errors related to database connectivity. 28 | 29 | **Solution:** 30 | - Ensure that the database server is running. 31 | - Check the database configuration settings in the `configuration/database-setup.md` file to ensure they are correct. 32 | 33 | ## Issue 4: Sync Issues Across Devices 34 | 35 | **Symptoms:** Notes are not syncing properly between devices. 36 | 37 | **Solution:** 38 | - Ensure that you are logged into the same account on all devices. 39 | - Check your internet connection and try refreshing the app. 40 | 41 | ## Issue 5: Performance Issues 42 | 43 | **Symptoms:** The application is running slowly or freezing. 44 | 45 | **Solution:** 46 | - Try closing other applications to free up system resources. 47 | - Refer to the `troubleshooting/performance.md` file for additional tips on improving performance. 48 | 49 | ## Additional Help 50 | 51 | If you continue to experience issues try the following: 52 | 53 | 1. Check the [FAQs](./faq.md). 54 | 2. Read through the [Release Notes](https://github.com/owlistic-notes/owlistic/releases). 55 | 3. Search through existing [GitHub Issues](https://github.com/owlistic-notes/owlistic/issues). 56 | -------------------------------------------------------------------------------- /docs/website/docs/troubleshooting/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # FAQ 6 | 7 | ## What is this Owlistic? 8 | 9 | Owlistic is a self-hosted solution designed to help users manage their notes and tasks efficiently. It offers features such as WYSIWYG editor, inline todo items, and synchronization across devices. 10 | 11 | ## How do I install the app? 12 | 13 | You can install the app using your preferred [installation method](/docs/category/installation). 14 | 15 | ## What are the system requirements? 16 | 17 | The system requirements for running the app can be found in the [System Requirements](../installation/system-requirements.md) section. 18 | 19 | ## How do I create notes? 20 | 21 | To create notes, refer to the [Creating Notes](../features/creating-notes.md) guide, which provides step-by-step instructions. 22 | 23 | ## Is there a mobile version of the app? 24 | 25 | Currently, the app only support web-based experience. However, you can access it through a mobile browser. 26 | 27 | ## How can I contribute to the project? 28 | 29 | We welcome contributions! Please read our [Contributing](../contributing/pull-requests.md) guidelines for more information on how to get involved. 30 | 31 | ## Where can I find help if I encounter issues? 32 | 33 | If you encounter any issues, please refer to the [Troubleshooting](../troubleshooting/common-issues.md) section for common problems and solutions. You can also reach out to the community through our GitHub discussions. 34 | 35 | ## How do I report a bug? 36 | 37 | To report a bug, please open an issue on our [GitHub repository](https://github.com/owlistic-notes/owlistic/issues) and provide as much detail as possible. 38 | -------------------------------------------------------------------------------- /docs/website/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type * as Preset from '@docusaurus/preset-classic'; 3 | 4 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 5 | 6 | export default { 7 | title: 'Owlistic', 8 | tagline: 'An Evernote-like application for managing notes and tasks', 9 | favicon: 'img/favicon.ico', 10 | 11 | // Set the production url of your site here 12 | url: 'https://owlistic-notes.github.io', 13 | // Set the // pathname under which your site is served 14 | // For GitHub pages deployment, it is often '//' 15 | baseUrl: '/owlistic/', 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: 'owlistic-notes', // Usually your GitHub org/user name. 20 | projectName: 'owlistic', // Usually your repo name. 21 | 22 | onBrokenLinks: 'throw', 23 | onBrokenMarkdownLinks: 'warn', 24 | 25 | // Even if you don't use internalization, you can use this field to set useful 26 | // metadata like html lang. For example, if your site is Chinese, you may want 27 | // to replace "en" with "zh-Hans". 28 | i18n: { 29 | defaultLocale: 'en', 30 | locales: ['en'], 31 | }, 32 | 33 | presets: [ 34 | [ 35 | 'classic', 36 | { 37 | docs: { 38 | sidebarPath: './sidebars.ts', 39 | // Please change this to your repo. 40 | // Remove this to remove the "edit this page" links. 41 | editUrl: 42 | 'https://github.com/owlistic-notes/owlistic/tree/main/docs/website/', 43 | }, 44 | theme: { 45 | customCss: './src/css/custom.css', 46 | }, 47 | } satisfies Preset.Options, 48 | ], 49 | ], 50 | 51 | themeConfig: { 52 | // Replace with your project's social card 53 | image: 'img/owlistic-social-card.jpg', 54 | navbar: { 55 | title: 'Owlistic', 56 | logo: { 57 | alt: 'Owlistic Logo', 58 | src: '/img/logo/owlistic.svg', 59 | }, 60 | items: [ 61 | { 62 | type: 'docSidebar', 63 | sidebarId: 'tutorialSidebar', 64 | position: 'left', 65 | label: 'Documentation', 66 | }, 67 | { 68 | to: '/roadmap', 69 | label: 'Roadmap', 70 | position: 'left', 71 | }, 72 | { 73 | href: 'https://github.com/owlistic-notes/owlistic', 74 | label: 'GitHub', 75 | position: 'right', 76 | }, 77 | ], 78 | }, 79 | footer: { 80 | style: 'dark', 81 | links: [ 82 | { 83 | title: 'Docs', 84 | items: [ 85 | { 86 | label: 'Introduction', 87 | to: '/docs/overview/intro', 88 | }, 89 | ], 90 | }, 91 | { 92 | title: 'Community', 93 | items: [ 94 | { 95 | label: 'GitHub Discussions', 96 | href: 'https://github.com/owlistic-notes/owlistic/discussions', 97 | }, 98 | { 99 | label: 'Issues', 100 | href: 'https://github.com/owlistic-notes/owlistic/issues', 101 | }, 102 | ], 103 | }, 104 | { 105 | title: 'More', 106 | items: [ 107 | { 108 | label: 'GitHub', 109 | href: 'https://github.com/owlistic-notes/owlistic', 110 | }, 111 | { 112 | label: 'Helm Charts', 113 | href: 'https://github.com/owlistic-notes/helm-charts', 114 | }, 115 | ], 116 | }, 117 | ], 118 | copyright: `Copyright © ${new Date().getFullYear()} Owlistic. Built with Docusaurus.`, 119 | }, 120 | prism: { 121 | theme: prismThemes.github, 122 | darkTheme: prismThemes.dracula, 123 | }, 124 | } satisfies Preset.ThemeConfig, 125 | }; 126 | -------------------------------------------------------------------------------- /docs/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.7.0", 19 | "@docusaurus/plugin-content-blog": "^3.7.0", 20 | "@docusaurus/preset-classic": "3.7.0", 21 | "@mdi/js": "^7.4.47", 22 | "@mdi/react": "^1.6.1", 23 | "@mdx-js/react": "^3.0.0", 24 | "clsx": "^2.0.0", 25 | "prism-react-renderer": "^2.3.0", 26 | "react": "^19.0.0", 27 | "react-dom": "^19.0.0" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "3.7.0", 31 | "@docusaurus/tsconfig": "3.7.0", 32 | "@docusaurus/types": "3.7.0", 33 | "typescript": "~5.6.2" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 3 chrome version", 43 | "last 3 firefox version", 44 | "last 5 safari version" 45 | ] 46 | }, 47 | "engines": { 48 | "node": ">=18.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/website/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /docs/website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Heading from '@theme/Heading'; 4 | import styles from './styles.module.css'; 5 | 6 | type FeatureItem = { 7 | title: string; 8 | Svg: React.ComponentType>; 9 | description: ReactNode; 10 | }; 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | // { 14 | // title: 'Easy to Use', 15 | // Svg: require('/img/undraw_docusaurus_mountain.svg').default, 16 | // description: ( 17 | // <> 18 | // Docusaurus was designed from the ground up to be easily installed and 19 | // used to get your website up and running quickly. 20 | // 21 | // ), 22 | // }, 23 | ]; 24 | 25 | function Feature({title, Svg, description}: FeatureItem) { 26 | return ( 27 |
28 |
29 | 30 |
31 |
32 | {title} 33 |

{description}

34 |
35 |
36 | ); 37 | } 38 | 39 | export default function HomepageFeatures(): ReactNode { 40 | return ( 41 |
42 |
43 |
44 | {FeatureList.map((props, idx) => ( 45 | 46 | ))} 47 |
48 |
49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /docs/website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/website/src/components/RoadmapFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .timeline { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | .timelineItem { 7 | display: flex; 8 | position: relative; 9 | min-height: 6rem; 10 | max-width: 90vw; 11 | width: 700px; 12 | margin-bottom: 2rem; 13 | } 14 | 15 | .date { 16 | width: 5rem; 17 | display: flex; 18 | align-items: center; 19 | justify-content: flex-start; 20 | color: #2196F3; 21 | font-weight: 500; 22 | } 23 | 24 | .lineWrapper { 25 | position: relative; 26 | width: 64px; 27 | display: flex; 28 | justify-content: center; 29 | } 30 | 31 | .line { 32 | position: absolute; 33 | width: 4px; 34 | background-color: #2196F3; 35 | top: 0; 36 | bottom: 0; 37 | z-index: 0; 38 | border-radius: 999px; 39 | } 40 | 41 | .lineTop { 42 | top: 50%; 43 | min-height: 100%; 44 | } 45 | 46 | .lineBottom { 47 | bottom: 50%; 48 | } 49 | 50 | .circle { 51 | z-index: 10; 52 | width: 32px; 53 | height: 32px; 54 | border: 3px solid white; 55 | box-shadow: 0 0 0 3px #2196F3; 56 | border-radius: 50%; 57 | position: relative; 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | margin-top: 50%; 62 | } 63 | 64 | .card { 65 | flex: 1; 66 | background: #f8f8f8; 67 | border-radius: 1rem; 68 | border: 1px solid #ddd; 69 | padding: 1rem; 70 | display: flex; 71 | justify-content: space-between; 72 | gap: 1rem; 73 | } 74 | 75 | .cardLeft { 76 | display: flex; 77 | flex-direction: column; 78 | justify-content: space-between; 79 | gap: 0.5rem; 80 | } 81 | 82 | .cardTitle { 83 | display: flex; 84 | align-items: center; 85 | gap: 0.5rem; 86 | font-size: 1.125rem; 87 | font-weight: 700; 88 | margin: 0; 89 | } 90 | 91 | .cardDesc { 92 | font-size: 0.875rem; 93 | color: #555; 94 | margin: 0; 95 | } 96 | 97 | .cardRight { 98 | display: flex; 99 | flex-direction: column; 100 | justify-content: space-between; 101 | align-items: flex-end; 102 | } 103 | 104 | .cardDateMobile { 105 | display: none; 106 | font-size: 0.875rem; 107 | } 108 | 109 | .link { 110 | font-size: 0.875rem; 111 | color: #2196F3; 112 | text-decoration: none; 113 | } 114 | 115 | .icon { 116 | background-color: white; 117 | color: #2196F3; 118 | border-radius: 50%; 119 | display: flex; 120 | align-items: center; 121 | justify-content: center; 122 | } 123 | 124 | @media (max-width: 768px) { 125 | .date { 126 | display: none; 127 | } 128 | 129 | .cardDateMobile { 130 | display: block; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /docs/website/src/components/RoadmapFeatures/timeline.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import * as mdiIcons from '@mdi/js'; 3 | import Icon from '@mdi/react'; 4 | 5 | import styles from './styles.module.css'; 6 | 7 | export type Item = { 8 | icon: string; 9 | iconColor: string; 10 | title: string; 11 | description?: string; 12 | link?: { url: string; text: string }; 13 | done?: false; 14 | getDateLabel?: (language: string) => string; 15 | }; 16 | 17 | interface Props { 18 | items: Item[]; 19 | } 20 | 21 | export function Timeline({ items }: Props): ReactNode { 22 | return ( 23 |
    24 | {items.map((item, index) => { 25 | const isFirst = index === 0; 26 | const isLast = index === items.length - 1; 27 | const done = item.done ?? true; 28 | const dateLabel = item.getDateLabel? item.getDateLabel('en-US') : ''; 29 | const timelineIcon = done ? mdiIcons.mdiCheckboxMarkedCircle : mdiIcons.mdiCheckboxBlankCircle; 30 | const cardIcon = item.icon; 31 | 32 | return ( 33 |
  • 34 | {dateLabel &&
    {dateLabel}
    } 35 | 36 |
    37 | {!isLast &&
    } 38 |
    39 | 40 |
    41 | {!isFirst &&
    } 42 |
    43 | 44 |
    45 |
    46 |
    47 | {cardIcon === 'owlistic' ? ( 48 | 49 | ) : ( 50 | 51 | )} 52 | {item.title} 53 |
    54 |

    {item.description}

    55 |
    56 | 57 |
    58 | {item.link && ( 59 | 60 | [{item.link.text}] 61 | 62 | )} 63 |
    {dateLabel}
    64 |
    65 |
    66 |
  • 67 | ); 68 | })} 69 |
70 | ); 71 | } -------------------------------------------------------------------------------- /docs/website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | 9 | :root { 10 | --ifm-color-primary: #2196F3; /* Flutter Colors.blue */ 11 | --ifm-color-primary-dark: #1976D2; /* Blues.700 */ 12 | --ifm-color-primary-darker: #1565C0; /* Blues.800 */ 13 | --ifm-color-primary-darkest: #0D47A1; /* Blues.900 */ 14 | --ifm-color-primary-light: #42A5F5; /* Blues.400 */ 15 | --ifm-color-primary-lighter: #64B5F6; /* Blues.300 */ 16 | --ifm-color-primary-lightest: #90CAF9; /* Blues.200 */ 17 | --ifm-code-font-size: 95%; 18 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 19 | 20 | /* Additional app colors */ 21 | --ifm-color-danger: #F44336; /* Colors.red */ 22 | --ifm-color-warning: #FF9800; /* Colors.orange */ 23 | --ifm-color-success: #4CAF50; /* Colors.green */ 24 | --ifm-background-color: #FFFFFF; /* Colors.white */ 25 | --ifm-card-background-color: #FFFFFF; /* cardLight */ 26 | } 27 | 28 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 29 | [data-theme='dark'] { 30 | --ifm-color-primary: #448AFF; /* Colors.blueAccent */ 31 | --ifm-color-primary-dark: #2979FF; /* BlueAccent.700 */ 32 | --ifm-color-primary-darker: #2962FF; /* BlueAccent.700 darker */ 33 | --ifm-color-primary-darkest: #0D47A1; /* Blues.900 */ 34 | --ifm-color-primary-light: #82B1FF; /* BlueAccent.100 */ 35 | --ifm-color-primary-lighter: #BBDEFB; /* Blues.100 */ 36 | --ifm-color-primary-lightest: #E3F2FD; /* Blues.50 */ 37 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 38 | 39 | /* Additional app colors */ 40 | --ifm-background-color: #121212; /* backgroundDark */ 41 | --ifm-card-background-color: #1E1E1E; /* cardDark */ 42 | } 43 | -------------------------------------------------------------------------------- /docs/website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | padding: 8px 14px; 24 | } 25 | 26 | .buttons > * { 27 | margin-right: 1rem; 28 | } 29 | 30 | .buttons > *:last-child { 31 | margin-right: 0; 32 | } 33 | -------------------------------------------------------------------------------- /docs/website/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import Link from '@docusaurus/Link'; 3 | import Layout from '@theme/Layout'; 4 | import Heading from '@theme/Heading'; 5 | import clsx from 'clsx'; 6 | 7 | import styles from './index.module.css'; 8 | 9 | function HomepageHeader() { 10 | return ( 11 |
12 |
13 | Owlistic logo 14 | 15 | An "Owlistic" notetaking app to easily organize and manage your notes and tasks! 16 | 17 |
18 | 21 | Get Started 22 | 23 | 26 | Roadmap 27 | 28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export default function Home(): ReactNode { 35 | return ( 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /docs/website/src/pages/roadmap.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import * as mdiIcons from '@mdi/js'; 3 | import { Item, Timeline } from '../components/RoadmapFeatures/timeline'; 4 | import Layout from '@theme/Layout'; 5 | import Heading from '@theme/Heading'; 6 | 7 | const releases = { 8 | 'v0.1.0': new Date(2025, 4, 19), 9 | } as const; 10 | 11 | const title = 'Roadmap'; 12 | const description = 'A list of future plans and goals, as well as past achievements and milestones.'; 13 | 14 | const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language); 15 | 16 | type Base = { 17 | icon: string; iconColor?: React.CSSProperties['color']; title: string; description: string 18 | }; 19 | 20 | const withRelease = ({ 21 | icon, 22 | iconColor, 23 | title, 24 | description, 25 | release: version, 26 | }: Base & { release: keyof typeof releases }) => { 27 | return { 28 | icon, 29 | iconColor: iconColor ?? 'gray', 30 | title, 31 | description, 32 | link: { 33 | url: `https://github.com/owlistic-notes/owlistic/releases/tag/${version}`, 34 | text: version, 35 | }, 36 | getDateLabel: withLanguage(releases[version]), 37 | }; 38 | }; 39 | 40 | const roadmap: Item[] = []; 41 | 42 | const milestones: Item[] = [ 43 | withRelease({ 44 | icon: mdiIcons.mdiRocketLaunch, 45 | iconColor: 'darkorange', 46 | title: 'First beta release', 47 | description: 'First Owlistic beta version.', 48 | release: 'v0.1.0', 49 | }), 50 | { 51 | icon: mdiIcons.mdiPartyPopper, 52 | iconColor: 'deeppink', 53 | title: 'First commit', 54 | description: 'First commit on GitHub, Owlistic is born.', 55 | getDateLabel: withLanguage(new Date(2025, 3, 14)), 56 | }, 57 | ]; 58 | 59 | function RoadmapHeader() { 60 | return ( 61 |
62 | 63 | Roadmap 64 | 65 |
66 | ); 67 | } 68 | 69 | function MilestonesHeader() { 70 | return ( 71 |
72 | 73 | Milestones 74 | 75 |
76 | ); 77 | } 78 | 79 | 80 | export default function RoadmapPage(): ReactNode { 81 | return ( 82 | 83 | {roadmap.length > 0 && } 84 | 85 |
86 | 87 |
88 | 89 | {milestones.length > 0 && } 90 | 91 |
92 | 93 |
94 |
95 | ); 96 | } -------------------------------------------------------------------------------- /docs/website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/.nojekyll -------------------------------------------------------------------------------- /docs/website/static/img/developers/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/developers/architecture.png -------------------------------------------------------------------------------- /docs/website/static/img/developers/event_outbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/developers/event_outbox.png -------------------------------------------------------------------------------- /docs/website/static/img/developers/server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/developers/server.png -------------------------------------------------------------------------------- /docs/website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/website/static/img/logo/owlistic-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/logo/owlistic-192x192.png -------------------------------------------------------------------------------- /docs/website/static/img/logo/owlistic-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/logo/owlistic-512x512.png -------------------------------------------------------------------------------- /docs/website/static/img/logo/owlistic-w-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/logo/owlistic-w-text.png -------------------------------------------------------------------------------- /docs/website/static/img/logo/owlistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/logo/owlistic.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/add_plus_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/add_plus_button.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/editor/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/editor/editor.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/editor/editor_toolbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/editor/editor_toolbar.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/editor/note_scrolling.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/editor/note_scrolling.gif -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/home.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/login.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notebooks/add_notebook_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notebooks/add_notebook_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notebooks/notebook_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notebooks/notebook_actions.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notebooks/notebook_add_note_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notebooks/notebook_add_note_button.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notebooks/notebook_add_note_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notebooks/notebook_add_note_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notebooks/notebooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notebooks/notebooks.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/add_note_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/add_note_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/import_markdown_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/import_markdown_button.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/import_markdown_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/import_markdown_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/move_note_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/move_note_button.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/move_note_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/move_note_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/note_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/note_actions.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/notes/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/notes/notes.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/profile/edit_profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/profile/edit_profile.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/profile/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/profile/profile.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/real_time_updates.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/real_time_updates.gif -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/register.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/sidebar.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/tasks/add_task_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/tasks/add_task_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/tasks/task_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/tasks/task_actions.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/tasks/tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/tasks/tasks.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/trash/delete_permanently_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/trash/delete_permanently_dialog.png -------------------------------------------------------------------------------- /docs/website/static/img/screenshots/trash/trash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/docs/website/static/img/screenshots/trash/trash.png -------------------------------------------------------------------------------- /docs/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /src/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Golang image as the base image 2 | FROM --platform=linux/$TARGETARCH golang:1.23.4-alpine AS builder 3 | 4 | # Set ARG for platform targeting 5 | ARG TARGETARCH 6 | 7 | # Install base compiler libraries 8 | RUN apk add --no-cache \ 9 | build-base \ 10 | gcc 11 | 12 | # Set the working directory inside the container 13 | WORKDIR /app 14 | 15 | # Copy go.mod and go.sum files first for better layer caching 16 | COPY ./go.mod ./go.sum ./ 17 | 18 | # Download dependencies 19 | RUN go mod download 20 | 21 | # Copy the rest of the source code 22 | COPY . . 23 | 24 | # Build the application 25 | RUN CGO_ENABLED=1 GO111MODULE=on \ 26 | GOOS=linux GOARCH=${TARGETARCH} \ 27 | go build -v \ 28 | -o /app/owlistic ./cmd/main.go 29 | 30 | # Use a minimal Alpine image for the final stage 31 | FROM --platform=linux/$TARGETARCH alpine:3.19 32 | 33 | # Set the working directory inside the container 34 | WORKDIR /app 35 | 36 | # Copy the built binary from the builder stage 37 | COPY --from=builder /app/owlistic ./ 38 | 39 | # Expose the application port 40 | EXPOSE 8080 41 | 42 | # Set the entrypoint to the binary 43 | ENTRYPOINT ["/app/owlistic"] 44 | -------------------------------------------------------------------------------- /src/backend/broker/consumer_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/nats-io/nats.go" 8 | ) 9 | 10 | // MockConsumer implements Consumer interface for testing 11 | type MockConsumer struct { 12 | messages []*nats.Msg 13 | closed bool 14 | } 15 | 16 | // NewMockConsumer creates a new mock consumer with optional test messages 17 | func NewMockConsumer() *MockConsumer { 18 | topic := "mock_topic" 19 | return &MockConsumer{ 20 | messages: []*nats.Msg{ 21 | { 22 | Subject: topic, 23 | Data: []byte("mock_value"), 24 | }, 25 | }, 26 | } 27 | } 28 | 29 | func (m *MockConsumer) ReadMessage(timeoutMs int) (*nats.Msg, error) { 30 | if m.closed { 31 | // Use a standard error code instead of ErrConsumerClosed which doesn't exist 32 | return nil, nats.ErrConnectionClosed 33 | } 34 | 35 | if len(m.messages) == 0 { 36 | return nil, nats.ErrConsumerNotActive 37 | } 38 | msg := m.messages[0] 39 | m.messages = m.messages[1:] 40 | return msg, nil 41 | } 42 | 43 | func (m *MockConsumer) Close() { 44 | m.closed = true 45 | } 46 | 47 | func TestStartConsumer(t *testing.T) { 48 | // Create a mock consumer with test messages 49 | mockConsumer := NewMockConsumer() 50 | 51 | // Create channel to receive messages 52 | messageChan := make(chan *nats.Msg, 1) 53 | 54 | // Simulate reading a message and sending it to the channel 55 | go func() { 56 | msg, err := mockConsumer.ReadMessage(-1) 57 | if err != nil { 58 | t.Errorf("Failed to read message: %v", err) 59 | return 60 | } 61 | 62 | messageChan <- msg 63 | }() 64 | 65 | // Receive the message from the channel 66 | var receivedMsg Message 67 | select { 68 | case receivedMsg = <-messageChan: 69 | // Message received successfully 70 | case <-time.After(1 * time.Second): 71 | t.Fatal("Timed out waiting for message") 72 | } 73 | 74 | // Verify the message content 75 | if receivedMsg.Subject != "mock_topic" || 76 | string(receivedMsg.Data) != "mock_value" { 77 | t.Errorf("Unexpected message content: %+v", receivedMsg) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/backend/broker/events.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | const ( 4 | UserSubject string = "user" 5 | NotebookSubject string = "notebook" 6 | NoteSubject string = "note" 7 | BlockSubject string = "block" 8 | TaskSubject string = "task" 9 | ) 10 | 11 | var SubjectNames = []string{ 12 | UserSubject, 13 | NotebookSubject, 14 | NoteSubject, 15 | BlockSubject, 16 | TaskSubject, 17 | } 18 | 19 | type EventType string 20 | 21 | const ( 22 | // Standardized event types in format: . 23 | NoteCreated EventType = "note.created" 24 | NoteUpdated EventType = "note.updated" 25 | NoteDeleted EventType = "note.deleted" 26 | NoteRestored EventType = "note.restored" 27 | 28 | NotebookCreated EventType = "notebook.created" 29 | NotebookUpdated EventType = "notebook.updated" 30 | NotebookDeleted EventType = "notebook.deleted" 31 | NotebookRestored EventType = "notebook.restored" 32 | 33 | BlockCreated EventType = "block.created" 34 | BlockUpdated EventType = "block.updated" 35 | BlockDeleted EventType = "block.deleted" 36 | 37 | TaskCreated EventType = "task.created" 38 | TaskUpdated EventType = "task.updated" 39 | TaskDeleted EventType = "task.deleted" 40 | 41 | // User events 42 | UserCreated EventType = "user.created" 43 | UserUpdated EventType = "user.updated" 44 | UserDeleted EventType = "user.deleted" 45 | 46 | // Trash events 47 | TrashEmptied EventType = "trash.emptied" 48 | ) 49 | -------------------------------------------------------------------------------- /src/backend/broker/producer_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type MockProducer struct { 8 | messages []struct { 9 | topic string 10 | key string 11 | value string 12 | } 13 | } 14 | 15 | func (m *MockProducer) Produce(topic, key, value string) { 16 | m.messages = append(m.messages, struct { 17 | topic string 18 | key string 19 | value string 20 | }{topic, key, value}) 21 | } 22 | 23 | func TestPublishMessage(t *testing.T) { 24 | mockProducer := &MockProducer{} 25 | 26 | mockProducer.Produce("test_topic", "test_key", "test_value") 27 | 28 | if len(mockProducer.messages) != 1 { 29 | t.Fatalf("Expected 1 message, got %d", len(mockProducer.messages)) 30 | } 31 | 32 | msg := mockProducer.messages[0] 33 | if msg.topic != "test_topic" || msg.key != "test_key" || msg.value != "test_value" { 34 | t.Errorf("Unexpected message content: %+v", msg) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | AppPort string 11 | AppOrigins string 12 | EventBroker string 13 | DBHost string 14 | DBPort string 15 | DBUser string 16 | DBPassword string 17 | DBName string 18 | JWTSecret string 19 | JWTExpirationHours int 20 | } 21 | 22 | func getEnv(key, defaultValue string) string { 23 | if value, exists := os.LookupEnv(key); exists { 24 | return value 25 | } 26 | log.Printf("%s not set, defaulting to %s", key, defaultValue) 27 | return defaultValue 28 | } 29 | 30 | func getEnvAsInt(key string, defaultValue int) int { 31 | if value, exists := os.LookupEnv(key); exists { 32 | if intVal, err := strconv.Atoi(value); err == nil { 33 | return intVal 34 | } 35 | log.Printf("Invalid integer value for %s, defaulting to %d", key, defaultValue) 36 | } 37 | return defaultValue 38 | } 39 | 40 | func Load() Config { 41 | log.Println("Loading configuration...") 42 | 43 | cfg := Config{ 44 | AppPort: getEnv("APP_PORT", "8080"), 45 | AppOrigins: getEnv("APP_ORIGINS", "*"), 46 | EventBroker: getEnv("BROKER_ADDRESS", "localhost:4222"), 47 | DBHost: getEnv("DB_HOST", "localhost"), 48 | DBPort: getEnv("DB_PORT", "5432"), 49 | DBUser: getEnv("DB_USER", "owlistic"), 50 | DBPassword: getEnv("DB_PASSWORD", "owlistic"), 51 | DBName: getEnv("DB_NAME", "owlistic"), 52 | JWTSecret: getEnv("JWT_SECRET", "your-super-secret-key-change-this-in-production"), 53 | JWTExpirationHours: getEnvAsInt("JWT_EXPIRATION_HOURS", 24), 54 | } 55 | Print(cfg) 56 | 57 | return cfg 58 | } 59 | 60 | func Print(cfg Config) { 61 | log.Printf("App Port: %s\n", cfg.AppPort) 62 | log.Printf("App Origins: %s\n", cfg.AppOrigins) 63 | log.Printf("Event Broker Address %s\n", cfg.EventBroker) 64 | log.Printf("DB Host: %s\n", cfg.DBHost) 65 | log.Printf("DB Port: %s\n", cfg.DBPort) 66 | log.Printf("DB Name: %s\n", cfg.DBName) 67 | log.Printf("DB User: %s\n", cfg.DBUser) 68 | log.Printf("DB Password: %s\n", cfg.DBPassword) 69 | log.Printf("JWT Secret: %s\n", cfg.JWTSecret) 70 | log.Printf("JWT Expiration Hours: %d\n", cfg.JWTExpirationHours) 71 | } 72 | -------------------------------------------------------------------------------- /src/backend/database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "owlistic-notes/owlistic/config" 8 | 9 | "gorm.io/driver/postgres" 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/logger" 12 | ) 13 | 14 | type Database struct { 15 | DB *gorm.DB 16 | } 17 | 18 | func Setup(cfg config.Config) (*Database, error) { 19 | 20 | dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", 21 | cfg.DBHost, 22 | cfg.DBPort, 23 | cfg.DBUser, 24 | cfg.DBPassword, 25 | cfg.DBName, 26 | ) 27 | 28 | // Configure GORM with performance settings for large datasets 29 | gormConfig := &gorm.Config{ 30 | Logger: logger.Default.LogMode(logger.Info), 31 | PrepareStmt: true, // Cache prepared statements for better performance 32 | AllowGlobalUpdate: false, // Prevent global updates without conditions 33 | SkipDefaultTransaction: true, // Skip default transaction for better performance 34 | } 35 | 36 | db, err := gorm.Open(postgres.Open(dsn), gormConfig) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to connect to database: %w", err) 39 | } 40 | 41 | // Run migrations to properly set up tables and constraints 42 | log.Println("Running database migrations...") 43 | if err := RunMigrations(db); err != nil { 44 | return nil, fmt.Errorf("failed to run migrations: %w", err) 45 | } 46 | log.Println("Database migrations completed successfully") 47 | 48 | return &Database{DB: db}, nil 49 | } 50 | 51 | func (d *Database) Close() { 52 | if d.DB == nil { 53 | log.Println("Database connection is nil, nothing to close.") 54 | return 55 | } 56 | sqlDB, err := d.DB.DB() 57 | if err != nil { 58 | log.Printf("Failed to get database connection: %v", err) 59 | return 60 | } 61 | if err := sqlDB.Close(); err != nil { 62 | log.Printf("Failed to close database connection: %v", err) 63 | } 64 | } 65 | 66 | func (d *Database) Query(query string, args ...interface{}) (*gorm.DB, error) { 67 | result := d.DB.Raw(query, args...) 68 | return result, result.Error 69 | } 70 | 71 | func (d *Database) Execute(query string, args ...interface{}) error { 72 | result := d.DB.Exec(query, args...) 73 | return result.Error 74 | } 75 | -------------------------------------------------------------------------------- /src/backend/database/migrations.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "log" 5 | 6 | "owlistic-notes/owlistic/models" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // RunMigrations runs database migrations to ensure tables are up to date 12 | func RunMigrations(db *gorm.DB) error { 13 | log.Println("Running database migrations...") 14 | 15 | // Add all models that should be migrated 16 | err := db.AutoMigrate( 17 | &models.User{}, 18 | &models.Role{}, 19 | &models.Notebook{}, 20 | &models.Note{}, 21 | &models.Block{}, 22 | &models.Task{}, 23 | &models.Event{}, 24 | ) 25 | 26 | if err != nil { 27 | log.Printf("Migration failed: %v", err) 28 | return err 29 | } 30 | 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /src/backend/go.mod: -------------------------------------------------------------------------------- 1 | module owlistic-notes/owlistic 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/DATA-DOG/go-sqlmock v1.5.2 7 | github.com/gin-contrib/cors v1.7.5 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/golang-jwt/jwt/v5 v5.2.2 10 | github.com/google/uuid v1.6.0 11 | github.com/gorilla/websocket v1.5.3 12 | github.com/nats-io/nats.go v1.42.0 13 | github.com/stretchr/testify v1.10.0 14 | golang.org/x/crypto v0.37.0 15 | gorm.io/driver/postgres v1.5.11 16 | gorm.io/driver/sqlite v1.5.7 17 | gorm.io/gorm v1.25.12 18 | ) 19 | 20 | require ( 21 | github.com/bytedance/sonic v1.13.2 // indirect 22 | github.com/bytedance/sonic/loader v0.2.4 // indirect 23 | github.com/cloudwego/base64x v0.1.5 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/gabriel-vasile/mimetype v1.4.8 // indirect 26 | github.com/gin-contrib/sse v1.0.0 // indirect 27 | github.com/go-playground/locales v0.14.1 // indirect 28 | github.com/go-playground/universal-translator v0.18.1 // indirect 29 | github.com/go-playground/validator/v10 v10.26.0 // indirect 30 | github.com/goccy/go-json v0.10.5 // indirect 31 | github.com/jackc/pgpassfile v1.0.0 // indirect 32 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 33 | github.com/jackc/pgx/v5 v5.5.5 // indirect 34 | github.com/jackc/puddle/v2 v2.2.1 // indirect 35 | github.com/jinzhu/inflection v1.0.0 // indirect 36 | github.com/jinzhu/now v1.1.5 // indirect 37 | github.com/json-iterator/go v1.1.12 // indirect 38 | github.com/klauspost/compress v1.18.0 // indirect 39 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 40 | github.com/kr/text v0.2.0 // indirect 41 | github.com/leodido/go-urn v1.4.0 // indirect 42 | github.com/mattn/go-isatty v0.0.20 // indirect 43 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/nats-io/nkeys v0.4.11 // indirect 47 | github.com/nats-io/nuid v1.0.1 // indirect 48 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/rogpeppe/go-internal v1.14.1 // indirect 51 | github.com/stretchr/objx v0.5.2 // indirect 52 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 53 | github.com/ugorji/go/codec v1.2.12 // indirect 54 | golang.org/x/arch v0.15.0 // indirect 55 | golang.org/x/net v0.38.0 // indirect 56 | golang.org/x/sync v0.13.0 // indirect 57 | golang.org/x/sys v0.32.0 // indirect 58 | golang.org/x/text v0.24.0 // indirect 59 | google.golang.org/protobuf v1.36.6 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | ) 62 | -------------------------------------------------------------------------------- /src/backend/middleware/auth_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | 6 | "owlistic-notes/owlistic/services" 7 | "owlistic-notes/owlistic/utils/token" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // ExtractAndValidateToken uses the token utility instead 13 | func ExtractAndValidateToken(c *gin.Context, authService services.AuthServiceInterface) (*token.JWTClaims, error) { 14 | // Extract token from query or header 15 | tokenString, err := token.ExtractToken(c) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | // Validate the token using the auth service (which now uses the token utility) 21 | return authService.ValidateToken(tokenString) 22 | } 23 | 24 | func AuthMiddleware(authService services.AuthServiceInterface) gin.HandlerFunc { 25 | return func(c *gin.Context) { 26 | // Skip authentication for OPTIONS requests (CORS preflight) 27 | if c.Request.Method == "OPTIONS" { 28 | c.Next() 29 | return 30 | } 31 | 32 | // Extract and validate token 33 | claims, err := ExtractAndValidateToken(c, authService) 34 | if err != nil { 35 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 36 | return 37 | } 38 | 39 | // Store user info in the context for later use 40 | c.Set("userID", claims.UserID) 41 | c.Set("email", claims.Email) 42 | c.Next() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/backend/middleware/cors_middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-contrib/cors" 7 | gin "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // CORSMiddleware adds the required headers to allow cross-origin requests 11 | func CORSMiddleware(AppOrigins string) gin.HandlerFunc { 12 | 13 | // Set up CORS configuration 14 | corsConfig := cors.DefaultConfig() 15 | corsConfig.AllowOrigins = strings.Split(AppOrigins, ",") 16 | corsConfig.AllowWildcard = true 17 | corsConfig.AllowWebSockets = true 18 | corsConfig.AllowCredentials = true 19 | corsConfig.AllowHeaders = append(corsConfig.AllowHeaders, []string{ 20 | "Accept", 21 | "Authorization", 22 | "Accept-Encoding", 23 | "X-CSRF-Token", 24 | "X-Requested-With", 25 | }...) 26 | 27 | return cors.New(corsConfig) 28 | } 29 | -------------------------------------------------------------------------------- /src/backend/models/block_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBlockJSON(t *testing.T) { 12 | block := Block{ 13 | ID: uuid.New(), 14 | NoteID: uuid.New(), 15 | Type: TextBlock, 16 | Content: BlockContent{"text": "Test Content"}, 17 | Metadata: BlockMetadata{"format": "markdown"}, 18 | Order: 1, 19 | } 20 | 21 | data, err := json.Marshal(block) 22 | assert.NoError(t, err) 23 | 24 | var result Block 25 | err = json.Unmarshal(data, &result) 26 | assert.NoError(t, err) 27 | assert.Equal(t, block.ID, result.ID) 28 | assert.Equal(t, block.Type, result.Type) 29 | assert.Equal(t, "Test Content", result.Content["text"]) 30 | assert.Equal(t, "markdown", result.Metadata["format"]) 31 | assert.Equal(t, block.Order, result.Order) 32 | } 33 | 34 | func TestBlockWithTasks(t *testing.T) { 35 | task := Task{ 36 | ID: uuid.New(), 37 | UserID: uuid.New(), 38 | NoteID: uuid.New(), 39 | Title: "Test Task", 40 | Description: "Test Description", 41 | } 42 | 43 | block := Block{ 44 | ID: uuid.New(), 45 | NoteID: uuid.New(), 46 | Type: TaskBlock, 47 | Content: BlockContent{"text": "Task Block"}, 48 | Metadata: BlockMetadata{"task_id": task.ID}, 49 | Order: 1, 50 | } 51 | 52 | data, err := json.Marshal(block) 53 | assert.NoError(t, err) 54 | 55 | var result Block 56 | err = json.Unmarshal(data, &result) 57 | assert.NoError(t, err) 58 | assert.Equal(t, task.ID, result.GetTaskID()) 59 | } 60 | 61 | func TestBlockContentSerialization(t *testing.T) { 62 | // Test empty BlockContent 63 | empty := BlockContent{} 64 | emptyVal, err := empty.Value() 65 | assert.NoError(t, err) 66 | assert.Equal(t, "{}", string(emptyVal.([]byte))) 67 | 68 | // Test complex BlockContent 69 | content := BlockContent{ 70 | "text": "Sample text", 71 | "format": "markdown", 72 | "nested": map[string]interface{}{ 73 | "key": "value", 74 | "list": []string{"item1", "item2"}, 75 | }, 76 | } 77 | 78 | val, err := content.Value() 79 | assert.NoError(t, err) 80 | 81 | var scanned BlockContent 82 | err = scanned.Scan(val) 83 | assert.NoError(t, err) 84 | 85 | assert.Equal(t, "Sample text", scanned["text"]) 86 | assert.Equal(t, "markdown", scanned["format"]) 87 | 88 | nested := scanned["nested"].(map[string]interface{}) 89 | assert.Equal(t, "value", nested["key"]) 90 | } 91 | -------------------------------------------------------------------------------- /src/backend/models/event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Event struct { 11 | ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` 12 | Event string `gorm:"not null" json:"event"` 13 | Version int `gorm:"not null" json:"version"` 14 | Entity string `gorm:"not null" json:"entity"` 15 | Timestamp time.Time `gorm:"not null" json:"timestamp"` 16 | Data json.RawMessage `gorm:"type:jsonb;not null" json:"data"` 17 | Status string `gorm:"not null;default:'pending'" json:"status"` 18 | Dispatched bool `gorm:"not null;default:false" json:"dispatched"` 19 | DispatchedAt *time.Time `json:"dispatched_at,omitempty"` 20 | } 21 | 22 | func NewEvent(event, entity string, data interface{}) (*Event, error) { 23 | dataBytes, err := json.Marshal(data) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | // Use standardized event name format 29 | return &Event{ 30 | ID: uuid.New(), 31 | Event: event, 32 | Version: 1, 33 | Entity: entity, 34 | Timestamp: time.Now().UTC(), 35 | Data: dataBytes, 36 | Status: "pending", 37 | }, nil 38 | } 39 | -------------------------------------------------------------------------------- /src/backend/models/event_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNewEvent(t *testing.T) { 10 | testCases := []struct { 11 | name string 12 | event string 13 | entity string 14 | operation string 15 | data interface{} 16 | wantErr bool 17 | }{ 18 | { 19 | name: "Valid event", 20 | event: "test.created", 21 | entity: "test", 22 | operation: "create", 23 | data: map[string]interface{}{"key": "value"}, 24 | wantErr: false, 25 | }, 26 | { 27 | name: "Invalid JSON data", 28 | event: "test.created", 29 | entity: "test", 30 | data: make(chan int), // Unmarshalable type 31 | wantErr: true, 32 | }, 33 | } 34 | 35 | for _, tc := range testCases { 36 | t.Run(tc.name, func(t *testing.T) { 37 | event, err := NewEvent(tc.event, tc.entity, tc.data) 38 | if tc.wantErr { 39 | assert.Error(t, err) 40 | return 41 | } 42 | 43 | assert.NoError(t, err) 44 | assert.NotNil(t, event) 45 | assert.Equal(t, tc.event, event.Event) 46 | assert.Equal(t, tc.entity, event.Entity) 47 | assert.Equal(t, "pending", event.Status) 48 | assert.False(t, event.Dispatched) 49 | assert.Nil(t, event.DispatchedAt) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/backend/models/note.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Note struct { 12 | ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` 13 | UserID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"user_id"` 14 | NotebookID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"notebook_id"` 15 | Title string `gorm:"not null" json:"title"` 16 | Blocks []Block `gorm:"foreignKey:NoteID" json:"blocks"` 17 | Tags []string `gorm:"type:text[]" json:"tags"` 18 | CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` 19 | UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` 20 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` 21 | } 22 | 23 | func (n *Note) FromJSON(data []byte) error { 24 | return json.Unmarshal(data, n) 25 | } 26 | 27 | func (n *Note) ToJSON() ([]byte, error) { 28 | return json.Marshal(n) 29 | } 30 | -------------------------------------------------------------------------------- /src/backend/models/note_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/google/uuid" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNoteToJSON(t *testing.T) { 12 | note := Note{ 13 | ID: uuid.New(), 14 | UserID: uuid.New(), 15 | NotebookID: uuid.New(), 16 | Title: "Test Title", 17 | Blocks: []Block{ 18 | { 19 | ID: uuid.New(), 20 | Type: TextBlock, 21 | Content: BlockContent{"text": "Test Content"}, 22 | Order: 1, 23 | }, 24 | }, 25 | } 26 | 27 | data, err := note.ToJSON() 28 | assert.NoError(t, err) 29 | 30 | var result Note 31 | err = json.Unmarshal(data, &result) 32 | assert.NoError(t, err) 33 | assert.Equal(t, note.ID, result.ID) 34 | assert.Equal(t, note.Title, result.Title) 35 | assert.Equal(t, len(note.Blocks), len(result.Blocks)) 36 | assert.Equal(t, "Test Content", result.Blocks[0].Content["text"]) 37 | } 38 | 39 | func TestNoteFromJSON(t *testing.T) { 40 | blockID := uuid.New() 41 | data := `{ 42 | "id": "550e8400-e29b-41d4-a716-446655440000", 43 | "user_id": "550e8400-e29b-41d4-a716-446655440001", 44 | "notebook_id": "550e8400-e29b-41d4-a716-446655440002", 45 | "title": "Test Title", 46 | "blocks": [{ 47 | "id": "` + blockID.String() + `", 48 | "type": "text", 49 | "content": {"text": "Test Content"}, 50 | "order": 1 51 | }], 52 | }` 53 | 54 | var note Note 55 | err := note.FromJSON([]byte(data)) 56 | assert.NoError(t, err) 57 | assert.Equal(t, "Test Title", note.Title) 58 | assert.Equal(t, 1, len(note.Blocks)) 59 | assert.Equal(t, "Test Content", note.Blocks[0].Content["text"]) 60 | } 61 | -------------------------------------------------------------------------------- /src/backend/models/notebook.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Notebook struct { 12 | ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` 13 | UserID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"user_id"` 14 | Name string `gorm:"not null" json:"name"` 15 | Description string `json:"description"` 16 | Notes []Note `gorm:"foreignKey:NotebookID" json:"notes"` 17 | CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` 18 | UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` 19 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` 20 | } 21 | 22 | func (nb *Notebook) FromJSON(data []byte) error { 23 | return json.Unmarshal(data, nb) 24 | } 25 | 26 | func (nb *Notebook) ToJSON() ([]byte, error) { 27 | return json.Marshal(nb) 28 | } 29 | -------------------------------------------------------------------------------- /src/backend/models/notebook_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/google/uuid" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestNotebookToJSON(t *testing.T) { 13 | notebook := Notebook{ 14 | ID: uuid.New(), 15 | UserID: uuid.New(), 16 | Name: "Test Notebook", 17 | Description: "Test Description", 18 | CreatedAt: time.Now(), 19 | UpdatedAt: time.Now(), 20 | Notes: []Note{}, 21 | } 22 | 23 | data, err := notebook.ToJSON() 24 | assert.NoError(t, err) 25 | 26 | var result Notebook 27 | err = json.Unmarshal(data, &result) 28 | assert.NoError(t, err) 29 | assert.Equal(t, notebook.ID, result.ID) 30 | assert.Equal(t, notebook.UserID, result.UserID) 31 | assert.Equal(t, notebook.Name, result.Name) 32 | assert.Equal(t, notebook.Description, result.Description) 33 | } 34 | 35 | func TestNotebookFromJSON(t *testing.T) { 36 | data := `{ 37 | "id": "550e8400-e89b-41d4-a716-446655440000", 38 | "user_id": "550e8400-e89b-41d4-a716-446655440001", 39 | "name": "Test Notebook", 40 | "description": "Test Description", 41 | "notes": [] 42 | }` 43 | 44 | var notebook Notebook 45 | err := notebook.FromJSON([]byte(data)) 46 | assert.NoError(t, err) 47 | assert.Equal(t, "Test Notebook", notebook.Name) 48 | assert.Equal(t, "Test Description", notebook.Description) 49 | assert.Equal(t, "550e8400-e89b-41d4-a716-446655440000", notebook.ID.String()) 50 | assert.Equal(t, "550e8400-e89b-41d4-a716-446655440001", notebook.UserID.String()) 51 | } 52 | 53 | func TestNotebookWithNotes(t *testing.T) { 54 | block := Block{ 55 | ID: uuid.New(), 56 | Type: TextBlock, 57 | Content: BlockContent{"text": "Test Content"}, 58 | Order: 1, 59 | } 60 | 61 | note := Note{ 62 | ID: uuid.New(), 63 | UserID: uuid.New(), 64 | NotebookID: uuid.New(), 65 | Title: "Test Note", 66 | Blocks: []Block{block}, 67 | CreatedAt: time.Now(), 68 | UpdatedAt: time.Now(), 69 | } 70 | 71 | notebook := Notebook{ 72 | ID: uuid.New(), 73 | UserID: uuid.New(), 74 | Name: "Test Notebook", 75 | Description: "Test Description", 76 | Notes: []Note{note}, 77 | CreatedAt: time.Now(), 78 | UpdatedAt: time.Now(), 79 | } 80 | 81 | data, err := notebook.ToJSON() 82 | assert.NoError(t, err) 83 | 84 | var result Notebook 85 | err = json.Unmarshal(data, &result) 86 | assert.NoError(t, err) 87 | assert.Equal(t, 1, len(result.Notes)) 88 | assert.Equal(t, note.Title, result.Notes[0].Title) 89 | assert.Equal(t, 1, len(result.Notes[0].Blocks)) 90 | assert.Equal(t, "Test Content", result.Notes[0].Blocks[0].Content["text"]) 91 | } 92 | -------------------------------------------------------------------------------- /src/backend/models/notification_event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type NotificationEvent struct { 6 | UserID string `json:"user_id"` 7 | EventType string `json:"event_type"` 8 | Message string `json:"message"` 9 | Timestamp string `json:"timestamp"` 10 | } 11 | 12 | func (n *NotificationEvent) FromJSON(data []byte) error { 13 | return json.Unmarshal(data, n) 14 | } 15 | 16 | func (n *NotificationEvent) ToJSON() ([]byte, error) { 17 | return json.Marshal(n) 18 | } 19 | -------------------------------------------------------------------------------- /src/backend/models/notification_event_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNotificationEventToJSON(t *testing.T) { 11 | event := NotificationEvent{ 12 | UserID: "user123", 13 | EventType: "info", 14 | Message: "Test message", 15 | Timestamp: "2023-01-01T00:00:00Z", 16 | } 17 | 18 | data, err := event.ToJSON() 19 | assert.NoError(t, err) 20 | 21 | var result NotificationEvent 22 | err = json.Unmarshal(data, &result) 23 | assert.NoError(t, err) 24 | assert.Equal(t, event, result) 25 | } 26 | 27 | func TestNotificationEventFromJSON(t *testing.T) { 28 | data := `{ 29 | "user_id": "user123", 30 | "event_type": "info", 31 | "message": "Test message", 32 | "timestamp": "2023-01-01T00:00:00Z" 33 | }` 34 | 35 | var event NotificationEvent 36 | err := event.FromJSON([]byte(data)) 37 | assert.NoError(t, err) 38 | assert.Equal(t, "user123", event.UserID) 39 | assert.Equal(t, "info", event.EventType) 40 | assert.Equal(t, "Test message", event.Message) 41 | assert.Equal(t, "2023-01-01T00:00:00Z", event.Timestamp) 42 | } 43 | -------------------------------------------------------------------------------- /src/backend/models/role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // RoleType represents the type of role (Admin, Owner, Editor, Viewer) 12 | type RoleType string 13 | 14 | // Role types 15 | const ( 16 | AdminRole RoleType = "admin" // Can access and modify everything 17 | OwnerRole RoleType = "owner" // Full access to a specific resource 18 | EditorRole RoleType = "editor" // Can edit but not delete 19 | ViewerRole RoleType = "viewer" // Read-only access 20 | ) 21 | 22 | // RoleTypeFromString converts a string to a RoleType 23 | func RoleTypeFromString(roleStr string) (RoleType, error) { 24 | switch roleStr { 25 | case "admin": 26 | return AdminRole, nil 27 | case "owner": 28 | return OwnerRole, nil 29 | case "editor": 30 | return EditorRole, nil 31 | case "viewer": 32 | return ViewerRole, nil 33 | default: 34 | return "", errors.New("invalid role type") 35 | } 36 | } 37 | 38 | // ResourceType represents the type of resource for RBAC 39 | type ResourceType string 40 | 41 | // ResourceTypes 42 | const ( 43 | UserResource ResourceType = "user" 44 | NoteResource ResourceType = "note" 45 | NotebookResource ResourceType = "notebook" 46 | BlockResource ResourceType = "block" 47 | TaskResource ResourceType = "task" 48 | ) 49 | 50 | // Role represents a role assignment for a user on a specific resource 51 | type Role struct { 52 | ID uuid.UUID `gorm:"type:uuid;primary_key" json:"id"` 53 | UserID uuid.UUID `gorm:"type:uuid;not null" json:"user_id"` 54 | User *User `gorm:"foreignKey:UserID" json:"-"` 55 | ResourceID uuid.UUID `gorm:"type:uuid;not null" json:"resource_id"` 56 | ResourceType ResourceType `gorm:"type:varchar(50);not null" json:"resource_type"` 57 | Role RoleType `gorm:"type:varchar(50);not null" json:"role"` 58 | CreatedAt time.Time `json:"created_at"` 59 | UpdatedAt time.Time `json:"updated_at"` 60 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` 61 | } 62 | 63 | // BeforeCreate is a GORM hook that runs before creating a new role 64 | func (r *Role) BeforeCreate(tx *gorm.DB) (err error) { 65 | if r.ID == uuid.Nil { 66 | r.ID = uuid.New() 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /src/backend/models/task.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // TaskMetadata stores additional information about a task 14 | type TaskMetadata map[string]interface{} 15 | 16 | // Value implements the driver.Valuer interface for JSONB storage 17 | func (tm TaskMetadata) Value() (driver.Value, error) { 18 | if tm == nil { 19 | return nil, nil 20 | } 21 | return json.Marshal(tm) 22 | } 23 | 24 | // Scan implements the sql.Scanner interface for JSONB retrieval 25 | func (tm *TaskMetadata) Scan(value interface{}) error { 26 | if value == nil { 27 | *tm = make(TaskMetadata) 28 | return nil 29 | } 30 | 31 | bytes, ok := value.([]byte) 32 | if !ok { 33 | return errors.New("type assertion to []byte failed") 34 | } 35 | 36 | return json.Unmarshal(bytes, tm) 37 | } 38 | 39 | type Task struct { 40 | ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` 41 | UserID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"user_id"` 42 | NoteID uuid.UUID `gorm:"type:uuid;not null;constraint:OnDelete:CASCADE;" json:"note_id"` // Direct note relationship 43 | Title string `gorm:"not null" json:"title"` 44 | Description string `json:"description"` 45 | IsCompleted bool `gorm:"default:false" json:"is_completed"` 46 | DueDate string `json:"due_date"` 47 | Metadata TaskMetadata `gorm:"type:jsonb;default:'{}'::jsonb" json:"metadata,omitempty"` 48 | CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` 49 | UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` 50 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` 51 | } 52 | -------------------------------------------------------------------------------- /src/backend/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // User represents the user entity stored in the database 11 | type User struct { 12 | ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"` 13 | Email string `gorm:"unique;not null" json:"email"` 14 | PasswordHash string `json:"-"` // Password hash is never exposed in JSON 15 | Username string `gorm:"unique" json:"username"` 16 | DisplayName string `json:"display_name"` 17 | ProfilePic string `json:"profile_pic"` 18 | Preferences map[string]interface{} `gorm:"type:jsonb" json:"preferences"` 19 | CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at"` 20 | UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at"` 21 | DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` 22 | } 23 | 24 | // UserRegistrationInput represents data needed for registration 25 | type UserRegistrationInput struct { 26 | Email string `json:"email" binding:"required,email"` 27 | Password string `json:"password" binding:"required"` 28 | Username string `json:"username"` 29 | DisplayName string `json:"display_name"` 30 | ProfilePic string `json:"profile_pic"` 31 | Preferences map[string]interface{} `json:"preferences"` 32 | } 33 | 34 | // UserLoginInput represents data needed for login 35 | type UserLoginInput struct { 36 | Email string `json:"email" binding:"required,email"` 37 | Password string `json:"password" binding:"required"` 38 | } 39 | 40 | // UserUpdateInput represents data for updating user account details 41 | type UserUpdateInput struct { 42 | Email string `json:"email"` 43 | Password string `json:"password"` 44 | Username string `json:"username"` 45 | DisplayName string `json:"display_name"` 46 | ProfilePic string `json:"profile_pic"` 47 | Preferences map[string]interface{} `json:"preferences"` 48 | } 49 | 50 | // UserPasswordUpdateInput is specifically for password changes 51 | type UserPasswordUpdateInput struct { 52 | CurrentPassword string `json:"current_password" binding:"required"` 53 | NewPassword string `json:"new_password" binding:"required"` 54 | } 55 | 56 | // UserProfile represents the subset of User information 57 | // that can be safely updated through the profile endpoints 58 | type UserProfile struct { 59 | Username string `json:"username"` 60 | DisplayName string `json:"display_name"` 61 | ProfilePic string `json:"profile_pic"` 62 | Preferences map[string]interface{} `json:"preferences"` 63 | } 64 | -------------------------------------------------------------------------------- /src/backend/models/websocket_message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | const ( 10 | // Message types 11 | EventMessage string = "event" 12 | SubscribeMessage string = "subscribe" 13 | UnsubscribeMessage string = "unsubscribe" 14 | ErrorMessage string = "error" 15 | ) 16 | 17 | // StandardMessage represents a standardized WebSocket message format 18 | type StandardMessage struct { 19 | ID string `json:"id"` 20 | Type string `json:"type"` 21 | Event string `json:"event,omitempty"` // For event messages 22 | Timestamp time.Time `json:"timestamp"` 23 | Payload map[string]interface{} `json:"payload"` 24 | ResourceID string `json:"resource_id,omitempty"` // Used for RBAC 25 | ResourceType string `json:"resource_type,omitempty"` // Used for RBAC 26 | } 27 | 28 | // NewStandardMessage creates a new standard message 29 | func NewStandardMessage(msgType string, event string, payload map[string]interface{}) *StandardMessage { 30 | return &StandardMessage{ 31 | ID: uuid.New().String(), 32 | Type: msgType, 33 | Event: event, 34 | Timestamp: time.Now(), 35 | Payload: payload, 36 | } 37 | } 38 | 39 | // WithResource adds resource information to the message 40 | func (m *StandardMessage) WithResource(resourceType string, resourceID string) *StandardMessage { 41 | m.ResourceType = resourceType 42 | m.ResourceID = resourceID 43 | return m 44 | } 45 | -------------------------------------------------------------------------------- /src/backend/routes/auth.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "owlistic-notes/owlistic/database" 7 | "owlistic-notes/owlistic/models" 8 | "owlistic-notes/owlistic/services" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func RegisterAuthRoutes(group *gin.RouterGroup, db *database.Database, authService services.AuthServiceInterface) { 14 | group.POST("/login", func(c *gin.Context) { Login(c, db, authService) }) 15 | } 16 | 17 | func Login(c *gin.Context, db *database.Database, authService services.AuthServiceInterface) { 18 | var loginInput models.UserLoginInput 19 | if err := c.ShouldBindJSON(&loginInput); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 21 | return 22 | } 23 | 24 | token, err := authService.Login(db, loginInput.Email, loginInput.Password) 25 | if err != nil { 26 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) 27 | return 28 | } 29 | 30 | c.JSON(http.StatusOK, gin.H{"token": token}) 31 | } 32 | -------------------------------------------------------------------------------- /src/backend/routes/debug.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "owlistic-notes/owlistic/database" 8 | "owlistic-notes/owlistic/models" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // SetupDebugRoutes sets up routes for debugging 14 | func SetupDebugRoutes(router *gin.Engine, db *database.Database) { 15 | debugGroup := router.Group("/api/v1/debug") 16 | { 17 | debugGroup.GET("/note-exists/:id", func(c *gin.Context) { 18 | id := c.Param("id") 19 | 20 | // Check if note exists 21 | var note models.Note 22 | result := db.DB.Where("id = ?", id).First(¬e) 23 | 24 | if result.Error != nil { 25 | c.JSON(http.StatusOK, gin.H{ 26 | "exists": false, 27 | "error": result.Error.Error(), 28 | "time": time.Now(), 29 | }) 30 | return 31 | } 32 | 33 | c.JSON(http.StatusOK, gin.H{ 34 | "exists": true, 35 | "id": note.ID, 36 | "title": note.Title, 37 | "notebook_id": note.NotebookID, 38 | "time": time.Now(), 39 | }) 40 | }) 41 | 42 | // Add route to check transaction processing queue 43 | debugGroup.GET("/event-queue", func(c *gin.Context) { 44 | var events []models.Event 45 | db.DB.Where("dispatched = ?", false).Find(&events) 46 | 47 | c.JSON(http.StatusOK, gin.H{ 48 | "pending_events": len(events), 49 | "events": events, 50 | "time": time.Now(), 51 | }) 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/backend/routes/websocket.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "owlistic-notes/owlistic/services" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // RegisterWebSocketRoutes sets up WebSocket endpoints with authentication 10 | func RegisterWebSocketRoutes(group *gin.RouterGroup, wsService services.WebSocketServiceInterface) { 11 | // Register the WebSocket route without middleware - auth happens in the handler 12 | // by extracting the token from query parameter 13 | group.GET("", func(c *gin.Context) { wsService.HandleConnection(c) }) 14 | } 15 | -------------------------------------------------------------------------------- /src/backend/services/auth_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "time" 5 | 6 | "owlistic-notes/owlistic/database" 7 | "owlistic-notes/owlistic/models" 8 | "owlistic-notes/owlistic/utils/token" 9 | 10 | "golang.org/x/crypto/bcrypt" 11 | ) 12 | 13 | // Use the JWTClaims from token package 14 | type JWTClaims = token.JWTClaims 15 | 16 | type AuthServiceInterface interface { 17 | Login(db *database.Database, email, password string) (string, error) 18 | ValidateToken(tokenString string) (*JWTClaims, error) 19 | HashPassword(password string) (string, error) 20 | ComparePasswords(hashedPassword, password string) error 21 | } 22 | 23 | type AuthService struct { 24 | jwtSecret []byte 25 | jwtExpiration time.Duration 26 | } 27 | 28 | func NewAuthService(jwtSecret string, jwtExpirationHours int) *AuthService { 29 | return &AuthService{ 30 | jwtSecret: []byte(jwtSecret), 31 | jwtExpiration: time.Duration(jwtExpirationHours) * time.Hour, 32 | } 33 | } 34 | 35 | func (s *AuthService) Login(db *database.Database, email, password string) (string, error) { 36 | var user models.User 37 | if err := db.DB.Where("email = ?", email).First(&user).Error; err != nil { 38 | return "", ErrInvalidCredentials 39 | } 40 | 41 | if err := s.ComparePasswords(user.PasswordHash, password); err != nil { 42 | return "", ErrInvalidCredentials 43 | } 44 | 45 | // Use the utility function instead 46 | tokenString, err := token.GenerateToken(user.ID, user.Email, s.jwtSecret, s.jwtExpiration) 47 | if err != nil { 48 | return "", err 49 | } 50 | 51 | return tokenString, nil 52 | } 53 | 54 | // ValidateToken uses the token utility to validate tokens 55 | func (s *AuthService) ValidateToken(tokenString string) (*JWTClaims, error) { 56 | return token.ValidateToken(tokenString, s.jwtSecret) 57 | } 58 | 59 | func (s *AuthService) HashPassword(password string) (string, error) { 60 | hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 61 | if err != nil { 62 | return "", err 63 | } 64 | return string(hashedBytes), nil 65 | } 66 | 67 | func (s *AuthService) ComparePasswords(hashedPassword, password string) error { 68 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 69 | } 70 | 71 | var AuthServiceInstance AuthServiceInterface 72 | -------------------------------------------------------------------------------- /src/backend/services/errors.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | // Common errors 8 | var ( 9 | // General errors 10 | ErrNotFound = errors.New("resource not found") 11 | ErrInvalidInput = errors.New("invalid input") 12 | ErrInternal = errors.New("internal server error") 13 | ErrResourceExists = errors.New("resource already exists") 14 | ErrValidation = errors.New("validation error") 15 | 16 | // Authentication/authorization errors 17 | ErrInvalidCredentials = errors.New("invalid email or password") 18 | ErrInvalidToken = errors.New("invalid or expired token") 19 | ErrUnauthorized = errors.New("unauthorized") 20 | 21 | // Resource-specific errors 22 | ErrUserNotFound = errors.New("user not found") 23 | ErrNoteNotFound = errors.New("note not found") 24 | ErrBlockNotFound = errors.New("block not found") 25 | ErrNotebookNotFound = errors.New("notebook not found") 26 | ErrTaskNotFound = errors.New("task not found") 27 | ErrEventNotFound = errors.New("event not found") 28 | ErrUserAlreadyExists = errors.New("user with that email already exists") 29 | 30 | // Type errors 31 | ErrInvalidBlockType = errors.New("invalid block type") 32 | 33 | // Connection errors 34 | ErrWebSocketConnection = errors.New("websocket connection error") 35 | ) 36 | -------------------------------------------------------------------------------- /src/backend/services/event_handler_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "owlistic-notes/owlistic/database" 8 | "owlistic-notes/owlistic/models" 9 | "owlistic-notes/owlistic/testutils" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/mock" 13 | ) 14 | 15 | // MockProducer implements the broker.Producer interface for testing 16 | type MockProducer struct { 17 | mock.Mock 18 | available bool 19 | } 20 | 21 | func NewMockProducer() *MockProducer { 22 | return &MockProducer{available: true} 23 | } 24 | 25 | func (m *MockProducer) PublishMessage(topic string, value string) error { 26 | args := m.Called(topic, value) 27 | return args.Error(0) 28 | } 29 | 30 | func (m *MockProducer) CreateTopics(streamName string, topics []string) error { 31 | args := m.Called(topics) 32 | return args.Error(0) 33 | } 34 | 35 | func (m *MockProducer) Close() { 36 | m.Called() 37 | } 38 | 39 | func (m *MockProducer) IsAvailable() bool { 40 | args := m.Called() 41 | return args.Bool(0) 42 | } 43 | 44 | func TestEventHandlerService_ProcessPendingEvents(t *testing.T) { 45 | db, dbMock, close := testutils.SetupMockDB() 46 | defer close() 47 | 48 | // Create a mock producer 49 | mockProducer := NewMockProducer() 50 | mockProducer.On("PublishMessage", mock.Anything, mock.Anything, mock.Anything).Return(nil) 51 | 52 | // Setup test data 53 | dbMock.ExpectQuery("SELECT \\* FROM \"events\" WHERE dispatched = \\$1"). 54 | WithArgs(false). 55 | WillReturnRows(testutils.MockEventRows([]models.Event{ 56 | { 57 | Event: "test.created", 58 | Entity: "test", 59 | }, 60 | })) 61 | 62 | // Expect update after processing 63 | dbMock.ExpectBegin() 64 | dbMock.ExpectExec("UPDATE \"events\" SET"). 65 | WillReturnResult(testutils.NewResult(1, 1)) 66 | dbMock.ExpectCommit() 67 | 68 | // Create service with our mock producer 69 | service := NewEventHandlerServiceWithProducer(db, mockProducer) 70 | 71 | service.Start() 72 | time.Sleep(2 * time.Second) // Allow some time for processing 73 | service.Stop() 74 | 75 | assert.NoError(t, dbMock.ExpectationsWereMet()) 76 | mockProducer.AssertExpectations(t) 77 | } 78 | 79 | func TestEventHandlerService_Lifecycle(t *testing.T) { 80 | db := &database.Database{} 81 | service := NewEventHandlerService(db) 82 | 83 | // Test Start 84 | service.Start() 85 | assert.True(t, service.(*EventHandlerService).isRunning) 86 | 87 | // Test double Start 88 | service.Start() // Should be no-op 89 | assert.True(t, service.(*EventHandlerService).isRunning) 90 | 91 | // Test Stop 92 | service.Stop() 93 | assert.False(t, service.(*EventHandlerService).isRunning) 94 | 95 | // Test double Stop 96 | service.Stop() // Should be no-op 97 | assert.False(t, service.(*EventHandlerService).isRunning) 98 | } 99 | -------------------------------------------------------------------------------- /src/backend/services/notification_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | // "owlistic-notes/owlistic/broker" 5 | "owlistic-notes/owlistic/models" 6 | ) 7 | 8 | type NotificationServiceInterface interface { 9 | PublishNotification(userID, eventType, message, timestamp string) error 10 | } 11 | 12 | type NotificationService struct{} 13 | 14 | func (s *NotificationService) PublishNotification(userID, eventType, message, timestamp string) error { 15 | event := models.NotificationEvent{ 16 | UserID: userID, 17 | EventType: eventType, 18 | Message: message, 19 | Timestamp: timestamp, 20 | } 21 | 22 | _, err := event.ToJSON() 23 | if err != nil { 24 | return err 25 | } 26 | 27 | // Always use the DefaultProducer directly - it will never be null 28 | // return broker.DefaultProducer.PublishMessage(broker.NotificationTopic, string(eventJSON)) 29 | return nil 30 | } 31 | 32 | var NotificationServiceInstance NotificationServiceInterface = &NotificationService{} 33 | -------------------------------------------------------------------------------- /src/backend/services/notification_service_test.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPublishNotification_Success(t *testing.T) { 10 | notificationService := &NotificationService{} 11 | err := notificationService.PublishNotification("user-id", "event-type", "message", "timestamp") 12 | assert.NoError(t, err) 13 | } 14 | -------------------------------------------------------------------------------- /src/backend/testutils/event_utils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "time" 7 | 8 | "owlistic-notes/owlistic/models" 9 | 10 | sqlmock "github.com/DATA-DOG/go-sqlmock" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | // MockEventRows creates mock SQL rows for events testing 15 | func MockEventRows(events []models.Event) *sqlmock.Rows { 16 | rows := sqlmock.NewRows([]string{ 17 | "id", "event", "version", "entity", "operation", 18 | "timestamp", "actor_id", "data", "status", 19 | "dispatched", "dispatched_at", 20 | }) 21 | 22 | for _, event := range events { 23 | if event.ID == uuid.Nil { 24 | event.ID = uuid.New() 25 | } 26 | if event.Timestamp.IsZero() { 27 | event.Timestamp = time.Now() 28 | } 29 | if event.Data == nil { 30 | event.Data = json.RawMessage(`{}`) 31 | } 32 | if event.Status == "" { 33 | event.Status = "pending" 34 | } 35 | 36 | rows.AddRow( 37 | event.ID, 38 | event.Event, 39 | event.Version, 40 | event.Entity, 41 | event.Timestamp, 42 | event.Data, 43 | event.Status, 44 | event.Dispatched, 45 | event.DispatchedAt, 46 | ) 47 | } 48 | 49 | return rows 50 | } 51 | 52 | func MockEventInsert() *sqlmock.Rows { 53 | return sqlmock.NewRows([]string{"id"}).AddRow(uuid.New()) 54 | } 55 | 56 | func NewResult(lastInsertID, rowsAffected int64) driver.Result { 57 | return sqlmock.NewResult(lastInsertID, rowsAffected) 58 | } 59 | -------------------------------------------------------------------------------- /src/backend/testutils/mock_context.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import( 4 | "net/http" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func GetTestGinContext(w http.ResponseWriter, req *http.Request) *gin.Context { 9 | gin.SetMode(gin.TestMode) 10 | c, _ := gin.CreateTestContext(w) 11 | c.Request = req 12 | return c 13 | } -------------------------------------------------------------------------------- /src/backend/testutils/mockdb.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "database/sql" 5 | 6 | "owlistic-notes/owlistic/database" 7 | 8 | sqlmock "github.com/DATA-DOG/go-sqlmock" 9 | "gorm.io/driver/postgres" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | // SetupMockDB sets up a mock database connection 14 | func SetupMockDB() (*database.Database, sqlmock.Sqlmock, func()) { 15 | var db *sql.DB 16 | var mock sqlmock.Sqlmock 17 | var err error 18 | 19 | db, mock, err = sqlmock.New() 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | dialector := postgres.New(postgres.Config{ 25 | DSN: "sqlmock_db_0", 26 | DriverName: "postgres", 27 | Conn: db, 28 | PreferSimpleProtocol: true, 29 | }) 30 | 31 | gormDB, err := gorm.Open(dialector, &gorm.Config{}) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | mockDB := &database.Database{ 37 | DB: gormDB, 38 | } 39 | 40 | close := func() { 41 | db.Close() 42 | } 43 | 44 | return mockDB, mock, close 45 | } 46 | -------------------------------------------------------------------------------- /src/backend/utils/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt/v5" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | // Common auth errors 15 | var ( 16 | ErrAuthHeaderMissing = errors.New("Authentication required") 17 | ErrInvalidAuthFormat = errors.New("Authorization header format must be Bearer {token}") 18 | ErrInvalidToken = errors.New("Invalid or expired token") 19 | ) 20 | 21 | // JWTClaims holds the standard JWT claims plus our custom claims 22 | type JWTClaims struct { 23 | UserID uuid.UUID `json:"user_id"` 24 | Email string `json:"email"` 25 | jwt.RegisteredClaims 26 | } 27 | 28 | // ValidateToken validates a JWT token string and returns the claims 29 | func ValidateToken(tokenString string, secret []byte) (*JWTClaims, error) { 30 | token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) { 31 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 32 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 33 | } 34 | return secret, nil 35 | }) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid { 42 | return claims, nil 43 | } 44 | 45 | return nil, ErrInvalidToken 46 | } 47 | 48 | // GenerateToken creates a new JWT token for a user 49 | func GenerateToken(userID uuid.UUID, email string, secret []byte, expiration time.Duration) (string, error) { 50 | claims := JWTClaims{ 51 | UserID: userID, 52 | Email: email, 53 | RegisteredClaims: jwt.RegisteredClaims{ 54 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)), 55 | IssuedAt: jwt.NewNumericDate(time.Now()), 56 | NotBefore: jwt.NewNumericDate(time.Now()), 57 | }, 58 | } 59 | 60 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 61 | signedToken, err := token.SignedString(secret) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | return signedToken, nil 67 | } 68 | 69 | // ExtractToken extracts a token from query parameters or authorization header 70 | func ExtractToken(c *gin.Context) (string, error) { 71 | // First try to get token from query parameter (common for WebSocket connections) 72 | token := c.Query("token") 73 | 74 | // If not in query, try header (for REST API) 75 | if token == "" { 76 | authHeader := c.GetHeader("Authorization") 77 | if authHeader == "" { 78 | return "", ErrAuthHeaderMissing 79 | } 80 | 81 | // Extract token from Bearer schema 82 | parts := strings.Split(authHeader, " ") 83 | if len(parts) != 2 || parts[0] != "Bearer" { 84 | return "", ErrInvalidAuthFormat 85 | } 86 | token = parts[1] 87 | } 88 | 89 | return token, nil 90 | } 91 | 92 | // ExtractAndValidateToken combines extraction and validation 93 | func ExtractAndValidateToken(c *gin.Context, secret []byte) (*JWTClaims, error) { 94 | tokenString, err := ExtractToken(c) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | return ValidateToken(tokenString, secret) 100 | } 101 | -------------------------------------------------------------------------------- /src/frontend/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "ea121f8859e4b13e47a8f845e4586164519588bc" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 17 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 18 | - platform: macos 19 | create_revision: ea121f8859e4b13e47a8f845e4586164519588bc 20 | base_revision: ea121f8859e4b13e47a8f845e4586164519588bc 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /src/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # First stage: Build the Flutter web app 2 | FROM --platform=linux/$TARGETARCH debian:bullseye-slim AS build 3 | 4 | # Set ARG for platform targeting 5 | ARG TARGETARCH 6 | 7 | # Set environment variables 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Install dependencies 11 | RUN apt-get update && apt-get install -y --no-install-recommends \ 12 | curl \ 13 | git \ 14 | unzip \ 15 | xz-utils \ 16 | zip \ 17 | libglu1-mesa \ 18 | ca-certificates \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Set up Flutter 22 | RUN git clone https://github.com/flutter/flutter.git -b stable /flutter 23 | ENV PATH="/flutter/bin:$PATH" 24 | 25 | # Verify flutter installation 26 | RUN flutter doctor 27 | 28 | # Set working directory 29 | WORKDIR /app 30 | 31 | # Copy the Flutter project files 32 | COPY . . 33 | 34 | # Configure Flutter for web 35 | RUN flutter config --enable-web 36 | 37 | # Get Flutter packages 38 | RUN flutter pub get 39 | 40 | # Initialize web project (creates index.html if missing) 41 | RUN flutter create --platforms=web . 42 | 43 | # Build the web app with warning suppressed 44 | RUN flutter build web --release 45 | 46 | # Second stage: Serve the app with Nginx 47 | FROM --platform=linux/$TARGETARCH nginx:alpine 48 | 49 | # Copy the build output to replace the default nginx contents 50 | COPY --from=build /app/build/web /usr/share/nginx/html 51 | 52 | # Expose port 80 53 | EXPOSE 80 54 | 55 | # Start Nginx server 56 | CMD ["nginx", "-g", "daemon off;"] 57 | -------------------------------------------------------------------------------- /src/frontend/README.md: -------------------------------------------------------------------------------- 1 | # owlistic 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /src/frontend/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | analyzer: 11 | errors: 12 | unused_field: ignore 13 | include: package:flutter_lints/flutter.yaml 14 | 15 | linter: 16 | # The lint rules applied to this project can be customized in the 17 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 18 | # included above or to enable additional rules. A list of all available lints 19 | # and their documentation is published at https://dart.dev/lints. 20 | # 21 | # Instead of disabling a lint rule for the entire project in the 22 | # section below, it can also be suppressed for a single line of code 23 | # or a specific dart file by using the `// ignore: name_of_lint` and 24 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 25 | # producing the lint. 26 | rules: 27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 28 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options 32 | -------------------------------------------------------------------------------- /src/frontend/assets/icon/owlistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/src/frontend/assets/icon/owlistic.png -------------------------------------------------------------------------------- /src/frontend/assets/logo/owlistic-w-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/src/frontend/assets/logo/owlistic-w-text.png -------------------------------------------------------------------------------- /src/frontend/assets/logo/owlistic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owlistic-notes/owlistic/982178f3e13eb23b413d6ea841f9a7415400758b/src/frontend/assets/logo/owlistic.png -------------------------------------------------------------------------------- /src/frontend/devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | - provider: true 5 | - shared_preferences: true -------------------------------------------------------------------------------- /src/frontend/docs/architecture.md: -------------------------------------------------------------------------------- 1 | # Owlistic App Architecture 2 | 3 | ## Model-View-Presenter (MVP) Pattern 4 | 5 | Owlistic uses the Model-View-Presenter (MVP) pattern with a few adaptations for Flutter: 6 | 7 | ### Components 8 | 9 | 1. **Models**: Plain Dart classes that represent data entities (Notebook, Note, Block, Task) 10 | 2. **Views**: Flutter widgets (screens) that display UI and forward user actions to presenters 11 | 3. **Presenters**: Providers that handle business logic and data manipulation 12 | 13 | ### Implementation Details 14 | 15 | - **Providers as Presenters**: Instead of creating separate presenter classes, we leverage Flutter's Provider system to act as presenters. This avoids duplication of functionality. 16 | - **Screens as Views**: Flutter widgets (screens) act as passive views that simply display data and handle user input. 17 | - **ChangeNotifier**: Presenters extend ChangeNotifier to notify views of data changes. 18 | 19 | ### Benefits of This Approach 20 | 21 | 1. **Simplified Architecture**: Using providers directly as presenters reduces boilerplate code. 22 | 2. **Better State Management**: Leverages the Provider package's strengths for state management. 23 | 3. **Testability**: Business logic is isolated in presenters (providers), making it easier to test. 24 | 4. **Separation of Concerns**: UI logic is separated from business logic. 25 | 26 | ### Interaction Flow 27 | 28 | 1. User interacts with the View (Screen) 29 | 2. View calls methods on the Presenter (Provider) 30 | 3. Presenter updates the Model or performs business logic 31 | 4. Presenter notifies the View of changes via ChangeNotifier 32 | 5. View rebuilds to display updated data 33 | 34 | ### Example 35 | 36 | ```dart 37 | // View (Screen) 38 | class NotesScreen extends StatelessWidget { 39 | @override 40 | Widget build(BuildContext context) { 41 | // Get the presenter 42 | final presenter = context.notesPresenter(listen: true); 43 | 44 | return Scaffold( 45 | body: ListView.builder( 46 | itemCount: presenter.notes.length, 47 | itemBuilder: (ctx, index) => NoteItem(presenter.notes[index]), 48 | ), 49 | floatingActionButton: FloatingActionButton( 50 | onPressed: () => presenter.createNote(...), 51 | child: Icon(Icons.add), 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | // Presenter (Provider) 58 | class NotesProvider with ChangeNotifier { 59 | List _notes = []; 60 | List get notes => [..._notes]; 61 | 62 | Future createNote(...) async { 63 | // Business logic 64 | final note = await ApiService.createNote(...); 65 | _notes.add(note); 66 | notifyListeners(); // Notify view to rebuild 67 | } 68 | } 69 | ``` 70 | 71 | ### Real-time Updates with WebSockets 72 | 73 | The MVP pattern is extended to handle real-time updates: 74 | 75 | 1. WebSocketProvider acts as a central presenter for real-time events 76 | 2. Domain-specific presenters (NotesProvider, NotebooksProvider, etc.) subscribe to relevant events 77 | 3. When WebSocket events arrive, the appropriate presenter updates its model and notifies views 78 | 79 | This architecture provides a clean separation of concerns while efficiently handling real-time data updates. 80 | -------------------------------------------------------------------------------- /src/frontend/lib/models/block.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/utils/data_converter.dart'; 2 | 3 | class Block { 4 | final String id; 5 | final String noteId; 6 | final Map content; // Changed from dynamic to Map 7 | final Map? metadata; // Keep metadata as optional Map 8 | final String type; 9 | final double order; 10 | final DateTime createdAt; 11 | final DateTime updatedAt; 12 | 13 | Block({ 14 | required this.id, 15 | required this.noteId, 16 | required this.content, 17 | required this.type, 18 | required this.order, 19 | this.metadata, 20 | DateTime? createdAt, 21 | DateTime? updatedAt, 22 | }) : createdAt = createdAt ?? DateTime.now(), 23 | updatedAt = updatedAt ?? DateTime.now(); 24 | 25 | factory Block.fromJson(Map json) { 26 | // Parse order as double 27 | final double orderValue = DataConverter.parseDoubleSafely(json['order']); 28 | 29 | // Parse datetime fields 30 | DateTime? createdAt; 31 | if (json['created_at'] != null) { 32 | try { 33 | createdAt = DateTime.parse(json['created_at']); 34 | } catch (e) { 35 | createdAt = DateTime.now(); 36 | } 37 | } 38 | 39 | DateTime? updatedAt; 40 | if (json['updated_at'] != null) { 41 | try { 42 | updatedAt = DateTime.parse(json['updated_at']); 43 | } catch (e) { 44 | updatedAt = DateTime.now(); 45 | } 46 | } 47 | 48 | // Ensure content is a map 49 | Map contentMap = {}; 50 | if (json['content'] is Map) { 51 | contentMap = Map.from(json['content']); 52 | } else if (json['content'] is String) { 53 | contentMap = {'text': json['content']}; 54 | } 55 | 56 | // Parse metadata if available 57 | Map? metadata; 58 | if (json['metadata'] != null && json['metadata'] is Map) { 59 | metadata = Map.from(json['metadata']); 60 | } 61 | 62 | return Block( 63 | id: json['id'] ?? '', 64 | content: contentMap, 65 | type: json['type'] ?? 'text', 66 | noteId: json['note_id'] ?? '', 67 | order: orderValue, 68 | metadata: metadata, 69 | createdAt: createdAt, 70 | updatedAt: updatedAt, 71 | ); 72 | } 73 | 74 | Map toJson() { 75 | return { 76 | 'id': id, 77 | 'note_id': noteId, 78 | 'content': content, 79 | 'type': type, 80 | 'metadata': metadata, 81 | 'order': order, 82 | 'created_at': createdAt.toIso8601String(), 83 | 'updated_at': updatedAt.toIso8601String(), 84 | }; 85 | } 86 | 87 | // Helper method to extract text content 88 | String getTextContent() { 89 | if (content.containsKey('text') && content['text'] is String) { 90 | return content['text'] as String; 91 | } 92 | return ''; 93 | } 94 | 95 | // Creates update data with new text 96 | Map createUpdateWithText(String text) { 97 | final updatedContent = Map.from(content); 98 | updatedContent['text'] = text; 99 | 100 | return { 101 | 'note_id': noteId, 102 | 'type': type, 103 | 'content': updatedContent, 104 | 'order': order, 105 | }; 106 | } 107 | 108 | Block copyWith({ 109 | String? id, 110 | String? noteId, 111 | String? userId, 112 | String? type, 113 | Map? content, 114 | Map? metadata, 115 | double? order, 116 | DateTime? createdAt, 117 | DateTime? updatedAt, 118 | }) { 119 | return Block( 120 | id: id ?? this.id, 121 | noteId: noteId ?? this.noteId, 122 | type: type ?? this.type, 123 | content: content ?? Map.from(this.content), 124 | metadata: metadata ?? (this.metadata != null ? Map.from(this.metadata!) : null), 125 | order: order ?? this.order, 126 | createdAt: createdAt ?? this.createdAt, 127 | updatedAt: updatedAt ?? DateTime.now(), 128 | ); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/frontend/lib/models/note.dart: -------------------------------------------------------------------------------- 1 | import 'block.dart'; 2 | 3 | class Note { 4 | final String id; 5 | final String title; 6 | final String notebookId; 7 | final String userId; 8 | final List blocks; 9 | final DateTime? createdAt; 10 | final DateTime? updatedAt; 11 | final DateTime? deletedAt; 12 | 13 | const Note({ 14 | required this.id, 15 | required this.title, 16 | required this.notebookId, 17 | required this.userId, 18 | this.blocks = const [], 19 | this.createdAt, 20 | this.updatedAt, 21 | this.deletedAt, 22 | }); 23 | 24 | String get content { 25 | final contents = blocks.map((b) => b.content).where((c) => c.isNotEmpty); 26 | return contents.isEmpty ? '' : contents.join('\n'); 27 | } 28 | 29 | factory Note.fromJson(Map json) { 30 | List blocksList = []; 31 | 32 | // Parse blocks if available 33 | if (json.containsKey('blocks')) { 34 | final blocksJson = json['blocks']; 35 | if (blocksJson != null) { 36 | try { 37 | if (blocksJson is List) { 38 | blocksList = blocksJson 39 | .where((blockJson) => blockJson != null) 40 | .map((blockJson) => Block.fromJson(blockJson)) 41 | .toList(); 42 | } 43 | } catch (e) { 44 | print('Error parsing blocks in note: $e'); 45 | } 46 | } 47 | } 48 | 49 | // Parse dates 50 | DateTime? createdAt; 51 | if (json['created_at'] != null) { 52 | try { 53 | createdAt = DateTime.parse(json['created_at']); 54 | } catch (e) { 55 | print('Error parsing created_at: $e'); 56 | } 57 | } 58 | 59 | DateTime? updatedAt; 60 | if (json['updated_at'] != null) { 61 | try { 62 | updatedAt = DateTime.parse(json['updated_at']); 63 | } catch (e) { 64 | print('Error parsing updated_at: $e'); 65 | } 66 | } 67 | 68 | DateTime? deletedAt; 69 | if (json['deleted_at'] != null) { 70 | try { 71 | deletedAt = DateTime.parse(json['deleted_at']); 72 | } catch (e) { 73 | print('Error parsing deleted_at: $e'); 74 | } 75 | } 76 | 77 | return Note( 78 | id: json['id']?.toString() ?? '', 79 | title: json['title']?.toString() ?? '', 80 | notebookId: json['notebook_id']?.toString() ?? '', 81 | userId: json['user_id']?.toString() ?? '', 82 | blocks: blocksList, 83 | createdAt: createdAt, 84 | updatedAt: updatedAt, 85 | deletedAt: deletedAt, 86 | ); 87 | } 88 | 89 | Map toJson() { 90 | return { 91 | 'id': id, 92 | 'title': title, 93 | 'notebook_id': notebookId, 94 | 'user_id': userId, 95 | 'blocks': blocks.map((block) => block.toJson()).toList(), 96 | if (createdAt != null) 'created_at': createdAt!.toIso8601String(), 97 | if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(), 98 | if (deletedAt != null) 'deleted_at': deletedAt!.toIso8601String(), 99 | }; 100 | } 101 | 102 | /// Creates a copy of this note with the given fields replaced with the new values 103 | Note copyWith({ 104 | String? id, 105 | String? title, 106 | String? notebookId, 107 | String? userId, 108 | List? blocks, 109 | DateTime? createdAt, 110 | DateTime? updatedAt, 111 | DateTime? deletedAt, 112 | }) { 113 | return Note( 114 | id: id ?? this.id, 115 | title: title ?? this.title, 116 | notebookId: notebookId ?? this.notebookId, 117 | userId: userId ?? this.userId, 118 | blocks: blocks ?? this.blocks, 119 | createdAt: createdAt ?? this.createdAt, 120 | updatedAt: updatedAt ?? this.updatedAt, 121 | deletedAt: deletedAt ?? this.deletedAt, 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/frontend/lib/models/notebook.dart: -------------------------------------------------------------------------------- 1 | import 'note.dart'; 2 | 3 | class Notebook { 4 | final String id; 5 | final String name; 6 | final String description; 7 | final String userId; 8 | final List notes; 9 | final DateTime? createdAt; 10 | final DateTime? deletedAt; 11 | 12 | const Notebook({ 13 | required this.id, 14 | required this.name, 15 | required this.description, 16 | required this.userId, 17 | this.notes = const [], 18 | this.createdAt, 19 | this.deletedAt, 20 | }); 21 | 22 | factory Notebook.fromJson(Map json) { 23 | List notesList = []; 24 | 25 | // Correctly handle notes data with proper logging 26 | if (json.containsKey('notes')) { 27 | final notesJson = json['notes']; 28 | if (notesJson != null) { 29 | try { 30 | if (notesJson is List) { 31 | notesList = notesJson 32 | .where((noteJson) => noteJson != null) 33 | .map((noteJson) => Note.fromJson(noteJson)) 34 | .toList(); 35 | } 36 | } catch (e) { 37 | print('Error parsing notes in notebook: $e'); 38 | } 39 | } 40 | } 41 | 42 | // Parse createdAt and deletedAt 43 | DateTime? createdAt; 44 | if (json['created_at'] != null) { 45 | try { 46 | createdAt = DateTime.parse(json['created_at']); 47 | } catch (e) { 48 | print('Error parsing created_at: $e'); 49 | } 50 | } 51 | 52 | DateTime? deletedAt; 53 | if (json['deleted_at'] != null) { 54 | try { 55 | deletedAt = DateTime.parse(json['deleted_at']); 56 | } catch (e) { 57 | print('Error parsing deleted_at: $e'); 58 | } 59 | } 60 | 61 | return Notebook( 62 | id: json['id']?.toString() ?? '', 63 | name: json['name']?.toString() ?? '', 64 | description: json['description']?.toString() ?? '', 65 | userId: json['user_id']?.toString() ?? '', 66 | notes: notesList, 67 | createdAt: createdAt, 68 | deletedAt: deletedAt, 69 | ); 70 | } 71 | 72 | Map toJson() { 73 | return { 74 | 'id': id, 75 | 'name': name, 76 | 'description': description, 77 | 'user_id': userId, 78 | 'notes': notes.map((note) => note.toJson()).toList(), 79 | if (createdAt != null) 'created_at': createdAt!.toIso8601String(), 80 | if (deletedAt != null) 'deleted_at': deletedAt!.toIso8601String(), 81 | }; 82 | } 83 | 84 | /// Creates a copy of this notebook with the given fields replaced with the new values 85 | Notebook copyWith({ 86 | String? id, 87 | String? name, 88 | String? description, 89 | String? userId, 90 | List? notes, 91 | DateTime? createdAt, 92 | DateTime? deletedAt, 93 | }) { 94 | return Notebook( 95 | id: id ?? this.id, 96 | name: name ?? this.name, 97 | description: description ?? this.description, 98 | userId: userId ?? this.userId, 99 | notes: notes ?? this.notes, 100 | createdAt: createdAt ?? this.createdAt, 101 | deletedAt: deletedAt ?? this.deletedAt, 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/frontend/lib/models/subscription.dart: -------------------------------------------------------------------------------- 1 | /// Represents a WebSocket subscription to a resource 2 | class Subscription { 3 | /// The resource type (e.g., "note", "block", "notebook") 4 | final String resource; 5 | 6 | /// Optional ID for the specific resource instance 7 | final String? id; 8 | 9 | /// Create a new subscription 10 | Subscription({ 11 | required this.resource, 12 | this.id, 13 | }); 14 | 15 | /// Create a subscription from a string key (format: "resource:id" or just "resource") 16 | factory Subscription.fromKey(String key) { 17 | final parts = key.split(':'); 18 | if (parts.length > 1) { 19 | return Subscription( 20 | resource: parts[0], 21 | id: parts[1], 22 | ); 23 | } else { 24 | return Subscription(resource: key); 25 | } 26 | } 27 | 28 | /// Convert to a string key format 29 | String toKey() { 30 | return id != null && id!.isNotEmpty ? '$resource:$id' : resource; 31 | } 32 | 33 | @override 34 | String toString() { 35 | return id != null ? 'Subscription($resource:$id)' : 'Subscription($resource)'; 36 | } 37 | 38 | @override 39 | bool operator ==(Object other) { 40 | if (identical(this, other)) return true; 41 | if (other is! Subscription) return false; 42 | return resource == other.resource && id == other.id; 43 | } 44 | 45 | @override 46 | int get hashCode => resource.hashCode ^ (id?.hashCode ?? 0); 47 | } 48 | -------------------------------------------------------------------------------- /src/frontend/lib/models/task.dart: -------------------------------------------------------------------------------- 1 | class Task { 2 | final String id; 3 | final String title; 4 | final bool isCompleted; 5 | final String userId; 6 | final String noteId; 7 | final String? description; 8 | final String? dueDate; 9 | final Map? metadata; 10 | final DateTime? createdAt; 11 | final DateTime? updatedAt; 12 | final DateTime? deletedAt; 13 | 14 | const Task({ 15 | required this.id, 16 | required this.title, 17 | required this.isCompleted, 18 | required this.userId, 19 | required this.noteId, 20 | this.description, 21 | this.dueDate, 22 | this.metadata, 23 | this.createdAt, 24 | this.updatedAt, 25 | this.deletedAt, 26 | }); 27 | 28 | factory Task.fromJson(Map json) { 29 | // Parse datetime fields 30 | DateTime? createdAt; 31 | if (json['created_at'] != null) { 32 | try { 33 | createdAt = DateTime.parse(json['created_at']); 34 | } catch (e) { 35 | createdAt = DateTime.now(); 36 | } 37 | } 38 | 39 | DateTime? updatedAt; 40 | if (json['updated_at'] != null) { 41 | try { 42 | updatedAt = DateTime.parse(json['updated_at']); 43 | } catch (e) { 44 | updatedAt = DateTime.now(); 45 | } 46 | } 47 | 48 | DateTime? deletedAt; 49 | if (json['deleted_at'] != null) { 50 | try { 51 | deletedAt = DateTime.parse(json['deleted_at']); 52 | } catch (e) { 53 | updatedAt = DateTime.now(); 54 | } 55 | } 56 | 57 | // Parse metadata if available 58 | Map? metadata; 59 | if (json['metadata'] != null && json['metadata'] is Map) { 60 | metadata = Map.from(json['metadata']); 61 | } 62 | 63 | return Task( 64 | id: json['id'] ?? '', 65 | title: json['title'] ?? '', 66 | isCompleted: json['is_completed'] ?? false, 67 | userId: json['user_id'] ?? '', 68 | noteId: json['note_id'] ?? '', 69 | description: json['description'], 70 | dueDate: json['due_date'], 71 | metadata: metadata, 72 | createdAt: createdAt, 73 | updatedAt: updatedAt, 74 | deletedAt: deletedAt, 75 | ); 76 | } 77 | 78 | // Helper method to get blockId from metadata 79 | String? getBlockId() { 80 | if (metadata != null && metadata!.containsKey('block_id')) { 81 | return metadata!['block_id']; 82 | } 83 | return null; 84 | } 85 | 86 | Map toJson() { 87 | return { 88 | 'id': id, 89 | 'title': title, 90 | 'is_completed': isCompleted, 91 | 'user_id': userId, 92 | 'note_id': noteId, 93 | if (description != null) 'description': description, 94 | if (dueDate != null) 'due_date': dueDate, 95 | if (metadata != null) 'metadata': metadata, 96 | if (createdAt != null) 'created_at': createdAt!.toIso8601String(), 97 | if (updatedAt != null) 'updated_at': updatedAt!.toIso8601String(), 98 | if (deletedAt != null) 'deleted_at': deletedAt!.toIso8601String(), 99 | }; 100 | } 101 | 102 | Task copyWith({ 103 | String? id, 104 | String? title, 105 | bool? isCompleted, 106 | String? userId, 107 | String? description, 108 | String? dueDate, 109 | String? noteId, 110 | Map? metadata, 111 | DateTime? createdAt, 112 | DateTime? updatedAt, 113 | DateTime? deletedAt, 114 | }) { 115 | return Task( 116 | id: id ?? this.id, 117 | title: title ?? this.title, 118 | isCompleted: isCompleted ?? this.isCompleted, 119 | userId: userId ?? this.userId, 120 | description: description ?? this.description, 121 | dueDate: dueDate ?? this.dueDate, 122 | noteId: noteId ?? this.noteId, 123 | metadata: metadata ?? this.metadata, 124 | createdAt: createdAt ?? this.createdAt, 125 | updatedAt: updatedAt ?? this.updatedAt, 126 | deletedAt: deletedAt ?? this.deletedAt, 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/frontend/lib/models/user.dart: -------------------------------------------------------------------------------- 1 | class User { 2 | final String id; 3 | final String email; 4 | final String username; 5 | final String displayName; 6 | final String profilePic; 7 | final Map? preferences; 8 | final DateTime createdAt; 9 | final DateTime updatedAt; 10 | 11 | User({ 12 | required this.id, 13 | required this.email, 14 | this.username = '', 15 | this.displayName = '', 16 | this.profilePic = '', 17 | this.preferences, 18 | required this.createdAt, 19 | required this.updatedAt, 20 | }); 21 | 22 | factory User.fromJson(Map json) { 23 | return User( 24 | id: json['id'] ?? '', 25 | email: json['email'] ?? '', 26 | username: json['username'] ?? '', 27 | displayName: json['display_name'] ?? '', 28 | profilePic: json['profile_pic'] ?? '', 29 | preferences: json['preferences'] != null 30 | ? Map.from(json['preferences']) 31 | : null, 32 | createdAt: json['created_at'] != null 33 | ? DateTime.parse(json['created_at']) 34 | : DateTime.now(), 35 | updatedAt: json['updated_at'] != null 36 | ? DateTime.parse(json['updated_at']) 37 | : DateTime.now(), 38 | ); 39 | } 40 | 41 | Map toJson() { 42 | return { 43 | 'id': id, 44 | 'email': email, 45 | 'username': username, 46 | 'display_name': displayName, 47 | 'profile_pic': profilePic, 48 | 'preferences': preferences, 49 | 'created_at': createdAt.toIso8601String(), 50 | 'updated_at': updatedAt.toIso8601String(), 51 | }; 52 | } 53 | 54 | User copyWith({ 55 | String? id, 56 | String? email, 57 | String? username, 58 | String? displayName, 59 | String? profilePic, 60 | Map? preferences, 61 | DateTime? createdAt, 62 | DateTime? updatedAt, 63 | }) { 64 | return User( 65 | id: id ?? this.id, 66 | email: email ?? this.email, 67 | username: username ?? this.username, 68 | displayName: displayName ?? this.displayName, 69 | profilePic: profilePic ?? this.profilePic, 70 | preferences: preferences ?? this.preferences, 71 | createdAt: createdAt ?? this.createdAt, 72 | updatedAt: updatedAt ?? this.updatedAt, 73 | ); 74 | } 75 | } 76 | 77 | // UserProfile model for profile operations 78 | class UserProfile { 79 | final String username; 80 | final String displayName; 81 | final String profilePic; 82 | final Map? preferences; 83 | 84 | UserProfile({ 85 | this.username = '', 86 | this.displayName = '', 87 | this.profilePic = '', 88 | this.preferences, 89 | }); 90 | 91 | factory UserProfile.fromJson(Map json) { 92 | return UserProfile( 93 | username: json['username'] ?? '', 94 | displayName: json['display_name'] ?? '', 95 | profilePic: json['profile_pic'] ?? '', 96 | preferences: json['preferences'] != null 97 | ? Map.from(json['preferences']) 98 | : null, 99 | ); 100 | } 101 | 102 | Map toJson() { 103 | final data = {}; 104 | if (username.isNotEmpty) data['username'] = username; 105 | if (displayName.isNotEmpty) data['display_name'] = displayName; 106 | if (profilePic.isNotEmpty) data['profile_pic'] = profilePic; 107 | if (preferences != null) data['preferences'] = preferences; 108 | return data; 109 | } 110 | 111 | factory UserProfile.fromUser(User user) { 112 | return UserProfile( 113 | username: user.username, 114 | displayName: user.displayName, 115 | profilePic: user.profilePic, 116 | preferences: user.preferences, 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/frontend/lib/services/app_state_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:owlistic/utils/logger.dart'; 3 | 4 | /// Service for handling application-wide state changes and cross-provider communication 5 | class AppStateService { 6 | static final AppStateService _instance = AppStateService._internal(); 7 | final Logger _logger = Logger('AppStateService'); 8 | 9 | // Stream controllers for different app events 10 | final StreamController _resetStateController = StreamController.broadcast(); 11 | final StreamController _authStateController = StreamController.broadcast(); 12 | 13 | // Private constructor - initialize everything here 14 | AppStateService._internal() { 15 | _logger.info('AppStateService initialized immediately in constructor'); 16 | } 17 | 18 | // Factory constructor to return the same instance 19 | factory AppStateService() { 20 | return _instance; 21 | } 22 | 23 | // Streams that providers can listen to 24 | Stream get onResetState => _resetStateController.stream; 25 | Stream get onAuthStateChanged => _authStateController.stream; 26 | 27 | // Trigger app reset (when user logs out) 28 | void resetAppState() { 29 | _logger.info('Broadcasting app state reset event'); 30 | _resetStateController.add(null); 31 | } 32 | 33 | // Broadcast auth state changes 34 | void setAuthState(bool isLoggedIn) { 35 | _logger.info('Broadcasting auth state change: isLoggedIn=$isLoggedIn'); 36 | _authStateController.add(isLoggedIn); 37 | } 38 | 39 | // Clean up resources 40 | void dispose() { 41 | _resetStateController.close(); 42 | _authStateController.close(); 43 | _logger.info('AppStateService disposed'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/frontend/lib/services/theme_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:shared_preferences/shared_preferences.dart'; 4 | import 'package:owlistic/utils/logger.dart'; 5 | 6 | class ThemeService { 7 | final Logger _logger = Logger('ThemeService'); 8 | static const String _themeKey = 'app_theme_mode'; 9 | 10 | // Get theme mode from preferences 11 | Future getThemeMode() async { 12 | try { 13 | final prefs = await SharedPreferences.getInstance(); 14 | final themeModeString = prefs.getString(_themeKey); 15 | 16 | if (themeModeString == null) { 17 | return ThemeMode.system; 18 | } 19 | 20 | switch (themeModeString) { 21 | case 'light': 22 | return ThemeMode.light; 23 | case 'dark': 24 | return ThemeMode.dark; 25 | default: 26 | return ThemeMode.system; 27 | } 28 | } catch (e) { 29 | _logger.error('Error getting theme mode', e); 30 | return ThemeMode.system; 31 | } 32 | } 33 | 34 | // Save theme mode preference 35 | Future setThemeMode(ThemeMode mode) async { 36 | try { 37 | final prefs = await SharedPreferences.getInstance(); 38 | String themeModeString; 39 | 40 | switch (mode) { 41 | case ThemeMode.light: 42 | themeModeString = 'light'; 43 | break; 44 | case ThemeMode.dark: 45 | themeModeString = 'dark'; 46 | break; 47 | default: 48 | themeModeString = 'system'; 49 | } 50 | 51 | await prefs.setString(_themeKey, themeModeString); 52 | _logger.debug('Theme mode set to: $themeModeString'); 53 | } catch (e) { 54 | _logger.error('Error saving theme mode', e); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/frontend/lib/services/trash_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:owlistic/models/note.dart'; 3 | import 'package:owlistic/models/notebook.dart'; 4 | import 'package:owlistic/utils/logger.dart'; 5 | import 'base_service.dart'; 6 | 7 | class TrashService extends BaseService { 8 | final Logger _logger = Logger('TrashService'); 9 | 10 | Future> fetchTrashedItems({ 11 | Map? queryParams 12 | }) async { 13 | try { 14 | // Build base query parameters 15 | Map params = {}; 16 | 17 | // Add any additional query parameters 18 | if (queryParams != null) { 19 | params.addAll(queryParams); 20 | } 21 | 22 | // Use authenticatedGet from BaseService with auth headers 23 | final response = await authenticatedGet( 24 | '/api/v1/trash', 25 | queryParameters: params 26 | ); 27 | 28 | if (response.statusCode == 200) { 29 | final data = json.decode(response.body); 30 | 31 | final List notesList = data['notes'] ?? []; 32 | final List notebooksList = data['notebooks'] ?? []; 33 | 34 | final parsedNotes = notesList.map((n) => Note.fromJson(n)).toList(); 35 | final parsedNotebooks = notebooksList.map((nb) => Notebook.fromJson(nb)).toList(); 36 | 37 | return { 38 | 'notes': parsedNotes, 39 | 'notebooks': parsedNotebooks, 40 | }; 41 | } else { 42 | throw Exception('Failed to load trashed items: ${response.statusCode}'); 43 | } 44 | } catch (e) { 45 | _logger.error('Error in fetchTrashedItems', e); 46 | rethrow; 47 | } 48 | } 49 | 50 | Future restoreItem(String type, String id) async { 51 | try { 52 | // Use authenticatedPost from BaseService with auth headers 53 | final response = await authenticatedPost( 54 | '/api/v1/trash/restore/$type/$id', 55 | {}, 56 | queryParameters: {} 57 | ); 58 | 59 | if (response.statusCode != 200) { 60 | throw Exception('Failed to restore item: ${response.statusCode}'); 61 | } 62 | } catch (e) { 63 | _logger.error('Error in restoreItem', e); 64 | rethrow; 65 | } 66 | } 67 | 68 | Future permanentlyDeleteItem(String type, String id) async { 69 | try { 70 | // Use authenticatedDelete from BaseService with auth headers 71 | final response = await authenticatedDelete( 72 | '/api/v1/trash/$type/$id', 73 | queryParameters: {} 74 | ); 75 | 76 | // Server returns 200 for success, not 204 77 | if (response.statusCode != 200) { 78 | throw Exception('Failed to permanently delete item: ${response.statusCode}'); 79 | } 80 | } catch (e) { 81 | _logger.error('Error in permanentlyDeleteItem', e); 82 | rethrow; 83 | } 84 | } 85 | 86 | Future emptyTrash() async { 87 | try { 88 | // Use authenticatedDelete from BaseService with auth headers 89 | final response = await authenticatedDelete( 90 | '/api/v1/trash', 91 | queryParameters: {} 92 | ); 93 | 94 | if (response.statusCode != 200) { 95 | throw Exception('Failed to empty trash: ${response.statusCode}'); 96 | } 97 | } catch (e) { 98 | _logger.error('Error in emptyTrash', e); 99 | rethrow; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/frontend/lib/services/user_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:owlistic/models/user.dart'; 3 | import 'base_service.dart'; 4 | import 'package:owlistic/utils/logger.dart'; 5 | 6 | class UserService extends BaseService { 7 | final Logger _logger = Logger('UserService'); 8 | 9 | // Get user by ID - this is the primary method that should be used 10 | Future getUserById(String id) async { 11 | try { 12 | final response = await authenticatedGet('/api/v1/users/$id'); 13 | 14 | if (response.statusCode == 200) { 15 | return User.fromJson(json.decode(response.body)); 16 | } else { 17 | _logger.error('Failed to fetch user: ${response.statusCode}, ${response.body}'); 18 | throw Exception('Failed to fetch user: ${response.body}'); 19 | } 20 | } catch (e) { 21 | _logger.error('Error fetching user by ID', e); 22 | rethrow; 23 | } 24 | } 25 | 26 | // Update user profile 27 | Future updateUserProfile(String userId, UserProfile profile) async { 28 | try { 29 | final response = await authenticatedPut( 30 | '/api/v1/users/$userId', 31 | profile.toJson(), 32 | ); 33 | 34 | if (response.statusCode == 200) { 35 | _logger.info('User profile updated successfully'); 36 | return User.fromJson(json.decode(response.body)); 37 | } else { 38 | _logger.error('Failed to update profile: ${response.statusCode}, ${response.body}'); 39 | throw Exception('Failed to update profile: ${response.body}'); 40 | } 41 | } catch (e) { 42 | _logger.error('Error updating user profile', e); 43 | rethrow; 44 | } 45 | } 46 | 47 | // Update password 48 | Future updatePassword(String userId, String currentPassword, String newPassword) async { 49 | try { 50 | final response = await authenticatedPut( 51 | '/api/v1/users/$userId/password', 52 | { 53 | 'current_password': currentPassword, 54 | 'new_password': newPassword, 55 | }, 56 | ); 57 | 58 | if (response.statusCode == 200) { 59 | _logger.info('Password updated successfully'); 60 | return true; 61 | } else { 62 | _logger.error('Failed to update password: ${response.statusCode}, ${response.body}'); 63 | throw Exception('Failed to update password: ${response.body}'); 64 | } 65 | } catch (e) { 66 | _logger.error('Error updating password', e); 67 | rethrow; 68 | } 69 | } 70 | 71 | // Delete user account 72 | Future deleteUserAccount(String userId) async { 73 | try { 74 | final response = await authenticatedDelete('/api/v1/users/$userId'); 75 | 76 | if (response.statusCode == 204) { 77 | _logger.info('User account deleted successfully'); 78 | return true; 79 | } else { 80 | _logger.error('Failed to delete account: ${response.statusCode}, ${response.body}'); 81 | throw Exception('Failed to delete account: ${response.body}'); 82 | } 83 | } catch (e) { 84 | _logger.error('Error deleting account', e); 85 | rethrow; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/frontend/lib/utils/data_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/utils/logger.dart'; 2 | 3 | /// DataConverter provides utility methods for converting between different data formats 4 | /// throughout the application to ensure consistency and reduce code duplication. 5 | class DataConverter { 6 | static final Logger _logger = Logger('DataConverter'); 7 | 8 | /// Format date for display in the UI 9 | static String formatDate(DateTime? date, {bool includeTime = false}) { 10 | if (date == null) { 11 | return ''; 12 | } 13 | 14 | final now = DateTime.now(); 15 | final difference = now.difference(date); 16 | 17 | if (difference.inSeconds < 60) { 18 | return 'Just now'; 19 | } else if (difference.inMinutes < 60) { 20 | return '${difference.inMinutes} ${difference.inMinutes == 1 ? 'minute' : 'minutes'} ago'; 21 | } else if (difference.inHours < 24) { 22 | return '${difference.inHours} ${difference.inHours == 1 ? 'hour' : 'hours'} ago'; 23 | } else if (difference.inDays < 30) { 24 | return '${difference.inDays} ${difference.inDays == 1 ? 'day' : 'days'} ago'; 25 | } else { 26 | return formatDate(date); 27 | } 28 | } 29 | 30 | /// Handle numeric values in JSON that might be strings or numbers 31 | static int parseIntSafely(dynamic value, {int defaultValue = 1}) { 32 | try { 33 | if (value == null) { 34 | return defaultValue; 35 | } else if (value is int) { 36 | return value; 37 | } else if (value is String) { 38 | return int.tryParse(value) ?? defaultValue; 39 | } else { 40 | return defaultValue; 41 | } 42 | } catch (e) { 43 | _logger.error('Error parsing int value', e); 44 | return defaultValue; 45 | } 46 | } 47 | 48 | /// Parse a double value safely from various input types 49 | static double parseDoubleSafely(dynamic value, {double defaultValue = 0.0}) { 50 | if (value == null) return defaultValue; 51 | 52 | if (value is double) return value; 53 | 54 | if (value is int) return value.toDouble(); 55 | 56 | if (value is String) { 57 | try { 58 | return double.parse(value); 59 | } catch (e) { 60 | return defaultValue; 61 | } 62 | } 63 | 64 | return defaultValue; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/frontend/lib/utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | 3 | /// Simple logger class for consistent logging across the app 4 | class Logger { 5 | final String _tag; 6 | 7 | /// Create a logger with a tag (usually the class name) 8 | Logger(this._tag); 9 | 10 | /// Log an info message 11 | void info(String message) { 12 | _log('INFO', message); 13 | } 14 | 15 | /// Log a debug message 16 | void debug(String message) { 17 | _log('DEBUG', message); 18 | } 19 | 20 | /// Log a warning message 21 | void warning(String message) { 22 | _log('WARNING', message); 23 | } 24 | 25 | /// Log an error message with optional stack trace 26 | void error(String message, [dynamic error, StackTrace? stackTrace]) { 27 | _log('ERROR', message); 28 | if (error != null) { 29 | _log('ERROR', ' Cause: $error'); 30 | } 31 | if (stackTrace != null) { 32 | _log('ERROR', ' Stack: $stackTrace'); 33 | } 34 | } 35 | 36 | /// Internal log method 37 | void _log(String level, String message) { 38 | if (kDebugMode) { 39 | print('[$level] $_tag: $message'); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/base_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | /// Base ViewModel interface that all ViewModels must implement 4 | /// In MVVM, ViewModels expose state that Views observe and react to 5 | abstract class BaseViewModel with ChangeNotifier { 6 | /// Indicates if the ViewModel is currently loading data 7 | bool get isLoading; 8 | 9 | /// Indicates if the ViewModel has been initialized 10 | bool get isInitialized; 11 | 12 | /// Indicates if the ViewModel is currently active 13 | bool get isActive; 14 | 15 | /// Current error message, if any 16 | String? get errorMessage; 17 | 18 | /// Clear current error message 19 | void clearError(); 20 | 21 | /// Activate the ViewModel when its view becomes visible 22 | /// This is used to manage resources and subscriptions 23 | void activate(); 24 | 25 | /// Deactivate the ViewModel when its view is no longer visible 26 | /// This is used to release resources and pause subscriptions 27 | void deactivate(); 28 | 29 | /// Reset internal state 30 | /// Used when logging out or clearing application state 31 | void resetState(); 32 | } 33 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/home_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/note.dart'; 2 | import 'package:owlistic/models/notebook.dart'; 3 | import 'package:owlistic/models/task.dart'; 4 | import 'package:owlistic/models/user.dart'; 5 | import 'base_viewmodel.dart'; 6 | 7 | /// Interface for home screen functionality. 8 | /// This represents a consolidated API for the home screen to interact with all services. 9 | abstract class HomeViewModel extends BaseViewModel { 10 | // User information 11 | Future get currentUser; 12 | bool get isLoggedIn; 13 | Stream get authStateChanges; 14 | 15 | // Authentication methods 16 | Future logout(); 17 | 18 | // Notebook functionality 19 | List get recentNotebooks; 20 | Future fetchRecentNotebooks(); 21 | Future createNotebook(String name, String description); 22 | bool get hasNotebooks; 23 | Notebook? getNotebook(String notebookId); 24 | 25 | // Notes functionality 26 | List get recentNotes; 27 | Future fetchRecentNotes(); 28 | Future createNote(String title, String notebookId); 29 | 30 | // Tasks functionality 31 | List get recentTasks; 32 | Future fetchRecentTasks(); 33 | Future createTask(String title, String noteId); 34 | Future toggleTaskCompletion(String taskId, bool isCompleted); 35 | 36 | // WebSocket connection 37 | Future ensureConnected(); 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/login_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:owlistic/models/user.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | /// Interface for login functionality 6 | abstract class LoginViewModel extends BaseViewModel { 7 | /// Login with email and password 8 | Future login(String email, String password, bool rememberMe); 9 | 10 | /// Get user email if it was previously saved 11 | Future getSavedEmail(); 12 | 13 | /// Save email for future logins 14 | Future saveEmail(String email); 15 | 16 | /// Clear saved email 17 | Future clearSavedEmail(); 18 | 19 | /// Check if a login attempt is in progress 20 | bool get isLoggingIn; 21 | 22 | /// Auth state properties - needed for router redirects 23 | bool get isLoggedIn; 24 | Future get currentUser; 25 | Stream get authStateChanges; 26 | 27 | /// Navigation methods 28 | void navigateToRegister(BuildContext context); 29 | 30 | /// Navigate after successful login - allows screens to navigate properly 31 | void navigateAfterSuccessfulLogin(BuildContext context); 32 | 33 | /// Handle successful login - perform any additional actions needed 34 | void onLoginSuccess(BuildContext context); 35 | 36 | /// Save server URL 37 | Future saveServerUrl(String url); 38 | 39 | /// Get server URL 40 | String? getServerUrl(); 41 | } 42 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/note_editor_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:owlistic/models/block.dart'; 3 | import 'package:owlistic/models/note.dart'; 4 | import 'package:owlistic/utils/document_builder.dart'; 5 | import 'base_viewmodel.dart'; 6 | 7 | abstract class NoteEditorViewModel extends BaseViewModel { 8 | // Note properties 9 | String? get noteId; 10 | set noteId(String? value); 11 | List get blocks; 12 | int get updateCount; 13 | 14 | // Note object 15 | Note? get currentNote; 16 | 17 | // Note operations (moved from NotesViewModel) 18 | Future fetchNoteById(String id); 19 | Future updateNoteTitle(String id, String title); 20 | 21 | // Block operations 22 | Block? getBlock(String id); 23 | List getBlocksForNote(String noteId); 24 | Future> fetchBlocksForNote(String noteId, { 25 | int page = 1, 26 | int pageSize = 100, 27 | bool append = false, 28 | bool refresh = false 29 | }); 30 | Future fetchBlockById(String blockId); 31 | Future deleteBlock(String blockId); 32 | void updateBlock(String id, Map content, { 33 | String? type, 34 | double? order, 35 | }); 36 | 37 | // Document builder access 38 | DocumentBuilder get documentBuilder; 39 | FocusNode get focusNode; 40 | 41 | // Document management 42 | void setBlocks(List blocks); 43 | void addBlocks(List blocks); 44 | 45 | // Active note management 46 | void activateNote(String noteId); 47 | void deactivateNote(String noteId); 48 | 49 | // Block visibility and pagination 50 | 51 | /// Subscribe to receive updates for specific block IDs that are visible in the UI. 52 | /// This optimizes websocket subscriptions to focus on currently visible blocks. 53 | void subscribeToVisibleBlocks(String noteId, List visibleBlockIds); 54 | 55 | /// Check if there are more blocks available to load for the given note. 56 | bool hasMoreBlocks(String noteId); 57 | 58 | /// Get current pagination information for the specified note. 59 | Map getPaginationInfo(String noteId); 60 | 61 | // Server sync and events 62 | void commitAllNodes(); 63 | Future fetchBlockFromEvent(String blockId); 64 | 65 | // Focus handling 66 | void requestFocus(); 67 | void setFocusToBlock(String blockId); 68 | String? consumeFocusRequest(); 69 | 70 | // User modified blocks tracking 71 | Set get userModifiedBlockIds; 72 | 73 | // Pagination scroll handling 74 | 75 | /// Initializes the scroll listener for pagination. 76 | /// This should be called when the note editor is initialized. 77 | /// The provided ScrollController will be used to detect when the user 78 | /// scrolls near the bottom to automatically load more blocks. 79 | void initScrollListener(ScrollController scrollController); 80 | } 81 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/notebooks_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/notebook.dart'; 2 | import 'package:owlistic/models/note.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | /// Interface for notebook management functionality 6 | abstract class NotebooksViewModel extends BaseViewModel { 7 | /// All notebooks 8 | List get notebooks; 9 | 10 | /// Get notebooks with optional filtering 11 | Future fetchNotebooks({ 12 | String? name, 13 | int page = 1, 14 | int pageSize = 20, 15 | List? excludeIds, 16 | }); 17 | 18 | /// Get a specific notebook by ID with its notes 19 | Future fetchNotebookById(String id, { 20 | List? excludeIds, 21 | bool addToExistingList = false, 22 | bool updateExisting = false 23 | }); 24 | 25 | /// Create a new notebook 26 | Future createNotebook(String name, String description); 27 | 28 | /// Update a notebook 29 | Future updateNotebook(String id, String name, String description); 30 | 31 | /// Delete a notebook 32 | Future deleteNotebook(String id); 33 | 34 | /// Add a note to a notebook 35 | Future addNoteToNotebook(String notebookId, String title); 36 | 37 | /// Delete a note from a notebook 38 | Future deleteNote(String notebookId, String noteId); 39 | 40 | /// Get a notebook by ID from cache 41 | Notebook? getNotebook(String id); 42 | 43 | /// Update the notebooks list directly 44 | void updateNotebooksList(List updatedNotebooks); 45 | 46 | /// Update just the notes collection of a specific notebook 47 | void updateNotebookNotes(String notebookId, List updatedNotes); 48 | 49 | /// Remove a notebook by ID 50 | void removeNotebookById(String notebookId); 51 | } 52 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/notes_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/note.dart'; 2 | import 'package:owlistic/models/notebook.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | abstract class NotesViewModel extends BaseViewModel { 6 | // Getters 7 | List get notes; 8 | List get notebooks; 9 | bool get isEmpty; 10 | int get updateCount; 11 | List get recentNotes; 12 | 13 | // Fetch operations 14 | Future> fetchNotes({ 15 | String? notebookId, 16 | int page = 1, 17 | int pageSize = 20, 18 | List? excludeIds 19 | }); 20 | 21 | Future fetchNoteById(String id); 22 | 23 | // Note grouping/filtering 24 | List getNotesByNotebookId(String notebookId); 25 | 26 | // CRUD operations with proper typing 27 | Future createNote(String title, String? notebookId); 28 | Future deleteNote(String id); 29 | Future updateNote(String id, String title, {String? notebookId}); 30 | 31 | // Note activation methods for real-time editing 32 | void activateNote(String noteId); 33 | void deactivateNote(String noteId); 34 | 35 | // Event handling method for consistency 36 | void handleNoteDeleted(String id); 37 | 38 | /// Move a note from one notebook to another 39 | Future moveNote(String noteId, String newNotebookId); 40 | 41 | /// Get a notebook by ID from cache 42 | Future addNoteToNotebook(String notebookId, String title); 43 | 44 | /// Import a markdown file and create a new note with its content 45 | Future importMarkdownFile(String content, String fileName, String notebookId); 46 | 47 | /// Export a note to markdown format 48 | Future exportNoteToMarkdown(String noteId); 49 | } 50 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/register_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:owlistic/models/user.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | /// Interface for user registration functionality 6 | abstract class RegisterViewModel extends BaseViewModel { 7 | /// Register a new user 8 | Future register(String email, String password); 9 | 10 | /// Check if a registration attempt is in progress 11 | bool get isRegistering; 12 | 13 | /// Auth state properties - needed for router redirects 14 | bool get isLoggedIn; 15 | Future get currentUser; 16 | Stream get authStateChanges; 17 | 18 | /// Navigation helper to login screen 19 | void navigateToLogin(BuildContext context); 20 | 21 | /// Navigate after successful registration - allows screens to navigate properly 22 | void navigateAfterSuccessfulRegistration(BuildContext context); 23 | 24 | /// Handle successful registration - perform any additional actions needed 25 | void onRegistrationSuccess(BuildContext context); 26 | 27 | /// Validate email format 28 | bool isValidEmail(String email); 29 | 30 | /// Validate password strength 31 | bool isValidPassword(String password); 32 | 33 | /// Check if passwords match 34 | bool doPasswordsMatch(String password, String confirmPassword); 35 | } 36 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/tasks_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/task.dart'; 2 | import 'package:owlistic/models/note.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | /// Interface for task management functionality 6 | abstract class TasksViewModel extends BaseViewModel { 7 | /// All tasks 8 | List get tasks; 9 | 10 | /// Recent tasks (limited set) 11 | List get recentTasks; 12 | 13 | /// Available notes for task creation 14 | List get availableNotes; 15 | 16 | /// Fetch tasks with filtering 17 | Future fetchTasks({String? completed, String? noteId}); 18 | 19 | /// Load available notes for task creation 20 | Future loadAvailableNotes(); 21 | 22 | /// Create a new task 23 | Future createTask(String title, String noteId); 24 | 25 | /// Delete a task 26 | Future deleteTask(String id); 27 | 28 | /// Update a task's title 29 | Future updateTaskTitle(String id, String title); 30 | 31 | /// Toggle completion status 32 | Future toggleTaskCompletion(String id, bool isCompleted); 33 | 34 | /// Fetch a task from a WebSocket event 35 | Future fetchTaskFromEvent(String taskId); 36 | 37 | /// Add a task from a WebSocket event 38 | Future addTaskFromEvent(String taskId); 39 | 40 | /// Handle task deletion events 41 | void handleTaskDeleted(String taskId); 42 | } 43 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/theme_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'base_viewmodel.dart'; 3 | 4 | /// Interface for theme management functionality 5 | abstract class ThemeViewModel extends BaseViewModel { 6 | /// Dark mode state 7 | bool get isDarkMode; 8 | 9 | /// Get the current theme data 10 | ThemeData get theme; 11 | 12 | /// Get the theme mode 13 | ThemeMode get themeMode; 14 | 15 | /// Toggle between light and dark theme 16 | Future toggleTheme(); 17 | 18 | /// Set a specific theme mode 19 | Future setThemeMode(ThemeMode mode); 20 | 21 | /// Set theme by dark mode value 22 | Future setTheme(bool darkMode); 23 | 24 | /// Initialize theme settings 25 | Future initialize(); 26 | } 27 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/trash_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/note.dart'; 2 | import 'package:owlistic/models/notebook.dart'; 3 | import 'base_viewmodel.dart'; 4 | 5 | /// Interface for trash management functionality 6 | abstract class TrashViewModel extends BaseViewModel { 7 | /// Trashed notes 8 | List get trashedNotes; 9 | 10 | /// Trashed notebooks 11 | List get trashedNotebooks; 12 | 13 | /// Fetch all trashed items 14 | Future fetchTrashedItems(); 15 | 16 | /// Restore an item from trash 17 | Future restoreItem(String type, String id); 18 | 19 | /// Permanently delete an item from trash 20 | Future permanentlyDeleteItem(String type, String id); 21 | 22 | /// Empty the entire trash 23 | Future emptyTrash(); 24 | } 25 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/user_profile_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'package:owlistic/models/user.dart'; 2 | import 'base_viewmodel.dart'; 3 | 4 | /// Interface for user profile management functionality 5 | abstract class UserProfileViewModel extends BaseViewModel { 6 | // Current user data 7 | User? get currentUser; 8 | 9 | // Loading states for different operations 10 | bool get isLoadingProfile; 11 | bool get isUpdatingProfile; 12 | bool get isUpdatingPassword; 13 | 14 | // Error handling for profile actions 15 | String? get profileError; 16 | String? get passwordError; 17 | 18 | // Clear specific errors 19 | void clearProfileError(); 20 | void clearPasswordError(); 21 | 22 | // User profile operations 23 | Future loadUserProfile(); 24 | 25 | Future updateUserProfile({ 26 | String? username, 27 | String? displayName, 28 | String? profilePic, 29 | Map? preferences, 30 | }); 31 | 32 | Future updatePassword(String currentPassword, String newPassword); 33 | Future deleteAccount(); 34 | 35 | // Validation methods 36 | bool validateUsername(String username); 37 | bool validatePassword(String password); 38 | bool passwordsMatch(String password, String confirmPassword); 39 | 40 | // State management 41 | @override 42 | void resetState(); 43 | } 44 | -------------------------------------------------------------------------------- /src/frontend/lib/viewmodel/websocket_viewmodel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:owlistic/models/subscription.dart'; 3 | import 'package:owlistic/models/user.dart'; 4 | import 'base_viewmodel.dart'; 5 | 6 | abstract class WebSocketViewModel extends BaseViewModel { 7 | // Connection state 8 | bool get isConnected; 9 | Future ensureConnected(); 10 | Future reconnect(); 11 | void disconnect(); 12 | 13 | // Subscription management 14 | Future subscribe(String resource, {String? id}); 15 | Future subscribeToEvent(String eventType); 16 | void unsubscribe(String resource, {String? id}); 17 | void unsubscribeFromEvent(String eventType); 18 | Future batchSubscribe(List subscriptions); 19 | bool isSubscribed(String resource, {String? id}); 20 | bool isSubscribedToEvent(String eventType); 21 | 22 | // Event registration 23 | void addEventListener(String type, String event, Function(Map) handler); 24 | void removeEventListener(String type, String event); 25 | 26 | // Simple event listeners 27 | void on(String eventName, Function(dynamic) callback); 28 | void off(String eventName, [Function(dynamic)? callback]); 29 | 30 | // Stream access for reactive components 31 | Stream> get messageStream; 32 | 33 | // Subscription tracking 34 | Set get confirmedSubscriptions; 35 | Set get pendingSubscriptions; 36 | int get totalSubscriptions; 37 | 38 | // Debug info 39 | User? get currentUser; 40 | String get lastEventType; 41 | String get lastEventAction; 42 | DateTime? get lastEventTime; 43 | int get messageCount; 44 | Map getDebugInfo(); 45 | 46 | // Auth cleanup 47 | void clearAllSubscriptions(); 48 | void clearOnLogout(); 49 | } 50 | -------------------------------------------------------------------------------- /src/frontend/lib/widgets/app_bar_common.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | class AppBarCommon extends StatelessWidget implements PreferredSizeWidget { 5 | final String? title; 6 | final List? actions; 7 | final Widget? leading; 8 | final bool showBackButton; 9 | final VoidCallback? onBackPressed; 10 | final VoidCallback? onMenuPressed; 11 | final double elevation; 12 | final bool centerTitle; 13 | final Color? backgroundColor; 14 | final Color? foregroundColor; 15 | final bool showProfileButton; 16 | 17 | const AppBarCommon({ 18 | Key? key, 19 | this.title, 20 | this.actions, 21 | this.leading, 22 | this.showBackButton = true, 23 | this.onBackPressed, 24 | this.onMenuPressed, 25 | this.elevation = 0, 26 | this.centerTitle = true, 27 | this.backgroundColor, 28 | this.foregroundColor, 29 | this.showProfileButton = true, 30 | }) : super(key: key); 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | Widget? leadingWidget = leading; 35 | 36 | // If leading is not provided and showBackButton is true, show back button 37 | if (leading == null) { 38 | if (showBackButton) { 39 | leadingWidget = IconButton( 40 | icon: const Icon(Icons.arrow_back), 41 | onPressed: onBackPressed ?? () => Navigator.of(context).pop(), 42 | ); 43 | } else if (onMenuPressed != null) { 44 | // Show menu button if onMenuPressed callback is provided 45 | leadingWidget = IconButton( 46 | icon: const Icon(Icons.menu), 47 | onPressed: onMenuPressed, 48 | ); 49 | } 50 | } 51 | 52 | // Create a copy of the actions list to modify if needed 53 | List? actionWidgets = actions != null ? List.from(actions!) : []; 54 | 55 | // Add profile button to actions if showProfileButton is true 56 | if (showProfileButton) { 57 | actionWidgets.add( 58 | IconButton( 59 | icon: const Icon(Icons.account_circle), 60 | tooltip: 'Profile', 61 | onPressed: () { 62 | // Navigate to profile page 63 | context.go('/profile'); 64 | }, 65 | ), 66 | ); 67 | } 68 | 69 | return AppBar( 70 | title: title != null ? Text(title!) : null, 71 | centerTitle: centerTitle, 72 | elevation: elevation, 73 | backgroundColor: backgroundColor, 74 | foregroundColor: foregroundColor, 75 | leading: leadingWidget, 76 | actions: actionWidgets, 77 | ); 78 | } 79 | 80 | @override 81 | Size get preferredSize => const Size.fromHeight(kToolbarHeight); 82 | } 83 | -------------------------------------------------------------------------------- /src/frontend/lib/widgets/app_logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppLogo extends StatelessWidget { 4 | final double size; 5 | final bool useBackground; 6 | final EdgeInsetsGeometry padding; 7 | final bool forceTransparent; 8 | 9 | const AppLogo({ 10 | Key? key, 11 | this.size = 60.0, 12 | this.useBackground = false, 13 | this.padding = EdgeInsets.zero, 14 | this.forceTransparent = false, 15 | }) : super(key: key); 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | // Use transparent logo if in dark mode or if explicitly requested 20 | const String logoAsset = 'assets/logo/owlistic.png'; 21 | final logoWidget = Image.asset( 22 | logoAsset, 23 | width: size, 24 | height: size, 25 | ); 26 | 27 | if (!useBackground) { 28 | return Padding( 29 | padding: padding, 30 | child: logoWidget, 31 | ); 32 | } 33 | 34 | // With circular background 35 | return Padding( 36 | padding: padding, 37 | child: Container( 38 | width: size + 20, 39 | height: size + 20, 40 | decoration: BoxDecoration( 41 | color: Theme.of(context).primaryColor.withOpacity(0.1), 42 | shape: BoxShape.circle, 43 | ), 44 | child: Center(child: logoWidget), 45 | ), 46 | ); 47 | } 48 | } -------------------------------------------------------------------------------- /src/frontend/lib/widgets/card_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CardContainer extends StatelessWidget { 4 | final Widget? child; 5 | final VoidCallback? onTap; 6 | final VoidCallback? onLongPress; 7 | final Widget? trailing; 8 | final Widget? leading; 9 | final String? title; 10 | final String? subtitle; 11 | final EdgeInsetsGeometry? padding; 12 | 13 | const CardContainer({ 14 | Key? key, 15 | this.child, 16 | this.onTap, 17 | this.onLongPress, 18 | this.trailing, 19 | this.leading, 20 | this.title, 21 | this.subtitle, 22 | this.padding, 23 | }) : super(key: key); 24 | 25 | @override 26 | Widget build(BuildContext context) { 27 | return Card( 28 | margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 29 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), 30 | child: InkWell( 31 | onTap: onTap, 32 | onLongPress: onLongPress, 33 | borderRadius: BorderRadius.circular(12), 34 | child: Padding( 35 | padding: padding ?? const EdgeInsets.all(16.0), 36 | child: Column( 37 | crossAxisAlignment: CrossAxisAlignment.start, 38 | children: [ 39 | if (title != null || subtitle != null || leading != null || trailing != null) 40 | ListTile( 41 | contentPadding: EdgeInsets.zero, 42 | leading: leading, 43 | title: title != null 44 | ? Text( 45 | title!, 46 | style: Theme.of(context).textTheme.titleMedium, 47 | overflow: TextOverflow.ellipsis, 48 | ) 49 | : null, 50 | subtitle: subtitle != null 51 | ? Text( 52 | subtitle!, 53 | style: Theme.of(context).textTheme.bodySmall, 54 | ) 55 | : null, 56 | trailing: trailing, 57 | ), 58 | if (child != null) child!, 59 | ], 60 | ), 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/frontend/lib/widgets/empty_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class EmptyState extends StatelessWidget { 4 | final String title; 5 | final String message; 6 | final IconData icon; 7 | final VoidCallback? onAction; 8 | final String? actionLabel; 9 | 10 | const EmptyState({ 11 | Key? key, 12 | required this.title, 13 | required this.message, 14 | required this.icon, 15 | this.onAction, 16 | this.actionLabel, 17 | }) : super(key: key); 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return Center( 22 | child: Padding( 23 | padding: const EdgeInsets.all(16.0), 24 | child: Column( 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | children: [ 27 | Icon( 28 | icon, 29 | size: 80, 30 | color: Theme.of(context).primaryColor.withOpacity(0.5), 31 | ), 32 | const SizedBox(height: 24), 33 | Text( 34 | title, 35 | style: Theme.of(context).textTheme.headlineSmall, 36 | textAlign: TextAlign.center, 37 | ), 38 | const SizedBox(height: 16), 39 | Text( 40 | message, 41 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 42 | color: Theme.of(context).textTheme.bodySmall?.color, 43 | ), 44 | textAlign: TextAlign.center, 45 | ), 46 | if (onAction != null && actionLabel != null) ...[ 47 | const SizedBox(height: 24), 48 | ElevatedButton.icon( 49 | onPressed: onAction, 50 | icon: const Icon(Icons.add), 51 | label: Text(actionLabel!), 52 | style: ElevatedButton.styleFrom( 53 | padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), 54 | ), 55 | ), 56 | ], 57 | ], 58 | ), 59 | ), 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/frontend/lib/widgets/theme_switcher.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:owlistic/viewmodel/theme_viewmodel.dart'; 4 | 5 | /// A widget that provides a button to toggle between light and dark themes 6 | class ThemeSwitcher extends StatelessWidget { 7 | final double? size; 8 | final EdgeInsets padding; 9 | 10 | const ThemeSwitcher({ 11 | Key? key, 12 | this.size, 13 | this.padding = const EdgeInsets.all(8.0), 14 | }) : super(key: key); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Consumer( 19 | builder: (context, themeViewModel, _) { 20 | return IconButton( 21 | padding: padding, 22 | icon: Icon( 23 | themeViewModel.isDarkMode 24 | ? Icons.light_mode 25 | : Icons.nightlight_round, 26 | size: size, 27 | ), 28 | onPressed: () { 29 | themeViewModel.toggleTheme(); 30 | }, 31 | tooltip: themeViewModel.isDarkMode 32 | ? 'Switch to Light Theme' 33 | : 'Switch to Dark Theme', 34 | ); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/frontend/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: owlistic 2 | description: A productivity app built with Flutter 3 | publish_to: 'none' 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=2.19.0 <4.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | provider: ^6.0.5 14 | http: ^1.1.0 15 | go_router: ^13.1.0 16 | flutter_markdown: ^0.6.18 17 | sqflite: ^2.3.2 18 | path: ^1.8.3 19 | json_annotation: ^4.8.1 20 | freezed_annotation: ^2.4.1 21 | logging: ^1.3.0 22 | web_socket_channel: ^3.0.2 23 | super_editor: ^0.3.0-dev.23 24 | super_editor_markdown: ^0.1.6-dev.7 25 | attributed_text: ^0.4.4 26 | shared_preferences: ^2.5.3 27 | intl: ^0.18.1 28 | flutter_secure_storage: ^8.0.0 29 | jwt_decoder: ^2.0.1 30 | flutter_launcher_icons: ^0.13.1 31 | file_picker: ^8.0.7 32 | path_provider: ^2.1.0 33 | share_plus: ^7.0.2 34 | overlord: ^0.0.3+5 35 | follow_the_leader: ^0.0.4+8 36 | ionicons: ^0.2.2 37 | 38 | dev_dependencies: 39 | flutter_test: 40 | sdk: flutter 41 | flutter_lints: ^2.0.0 42 | build_runner: ^2.4.8 43 | freezed: ^2.4.7 44 | json_serializable: ^6.7.1 45 | 46 | flutter: 47 | uses-material-design: true 48 | assets: 49 | - assets/icon/ 50 | - assets/logo/ 51 | 52 | flutter_launcher_icons: 53 | image_path: "assets/icon/owlistic.png" 54 | web: 55 | generate: true 56 | image_path: "assets/icon/owlistic.png" 57 | # windows: 58 | # generate: true 59 | # image_path: "assets/icon/owlistic.png" 60 | # icon_size: 48 61 | # macos: 62 | # generate: true 63 | # image_path: "assets/icon/owlistic.png" 64 | # android: "launcher_icon" 65 | # min_sdk_android: 21 66 | # ios: true --------------------------------------------------------------------------------