├── .npmrc ├── .prettierignore ├── vault ├── .obsidian │ ├── app.json │ ├── plugins │ │ └── journal-review │ ├── appearance.json │ ├── community-plugins.json │ ├── daily-notes.json │ ├── hotkeys.json │ ├── graph.json │ ├── core-plugins-migration.json │ └── core-plugins.json └── Welcome.md ├── .github ├── FUNDING.yml ├── workflows │ ├── build.yml │ ├── lint.yml │ └── release.yml └── dependabot.yml ├── .editorconfig ├── src ├── hooks │ └── useContext.ts ├── components │ ├── context.ts │ ├── Main.tsx │ ├── TimeSpan.tsx │ └── NotePreview.tsx ├── styles.css ├── styles-settings.css ├── view.tsx ├── main.ts ├── constants.ts └── settingsTab.ts ├── manifest.json ├── eslint.config.mjs ├── .gitignore ├── versions.json ├── tsconfig.json ├── version-bump.mjs ├── scripts └── generateNotes.ts ├── LICENSE ├── package.json ├── esbuild.config.mjs └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | vault 2 | -------------------------------------------------------------------------------- /vault/.obsidian/app.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: kageetai 2 | -------------------------------------------------------------------------------- /vault/.obsidian/plugins/journal-review: -------------------------------------------------------------------------------- 1 | ../../../dist -------------------------------------------------------------------------------- /vault/.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "accentColor": "" 3 | } -------------------------------------------------------------------------------- /vault/.obsidian/community-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "periodic-notes", 3 | "journal-review" 4 | ] -------------------------------------------------------------------------------- /vault/.obsidian/daily-notes.json: -------------------------------------------------------------------------------- 1 | { 2 | "format": "YYYY-MM-DDTHH:mm:ssZ", 3 | "folder": "/" 4 | } -------------------------------------------------------------------------------- /vault/.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | { 2 | "app:reload": [ 3 | { 4 | "modifiers": [ 5 | "Mod" 6 | ], 7 | "key": "R" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 2 10 | tab_width = 2 11 | -------------------------------------------------------------------------------- /vault/Welcome.md: -------------------------------------------------------------------------------- 1 | This is your new *vault*. 2 | 3 | Make a note of something, [[create a link]], or try [the Importer](https://help.obsidian.md/Plugins/Importer)! 4 | 5 | When you're ready, delete this note and make the vault your own. -------------------------------------------------------------------------------- /src/hooks/useContext.ts: -------------------------------------------------------------------------------- 1 | import { useContext as PreUseContext } from "preact/hooks"; 2 | 3 | import AppContext from "../components/context"; 4 | 5 | const useContext = () => PreUseContext(AppContext); 6 | 7 | export default useContext; 8 | -------------------------------------------------------------------------------- /src/components/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "preact"; 2 | import { App, View } from "obsidian"; 3 | 4 | import { Settings } from "../constants"; 5 | 6 | type Context = { 7 | view: View; 8 | app: App; 9 | settings: Settings; 10 | }; 11 | 12 | const AppContext = createContext({} as Context); 13 | 14 | export default AppContext; 15 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "journal-review", 3 | "name": "Journal Review", 4 | "version": "2.7.0", 5 | "minAppVersion": "0.15.0", 6 | "description": "Review your daily notes on their anniversaries, like \"what happened today last year\".", 7 | "author": "Kageetai", 8 | "authorUrl": "https://kageetai.net", 9 | "isDesktopOnly": false, 10 | "fundingUrl": { 11 | "Ko-Fi": "https://ko-fi.com/kageetai" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import tseslint from "typescript-eslint"; 3 | import globals from "globals"; 4 | 5 | export default tseslint.config( 6 | eslint.configs.recommended, 7 | ...tseslint.configs.recommended, 8 | { 9 | ignores: ["**/node_modules/", "**/dist/", "**/main.js"], 10 | }, 11 | { 12 | languageOptions: { 13 | globals: { 14 | ...globals.node, 15 | }, 16 | }, 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | @import "styles-settings.css"; 2 | 3 | #journal-review > h2 { 4 | margin-top: 0; 5 | } 6 | 7 | #journal-review .list { 8 | list-style: none; 9 | padding: 0; 10 | 11 | &:empty:before { 12 | content: "nothing to show"; 13 | } 14 | 15 | &.notes > li { 16 | cursor: pointer; 17 | 18 | &:has(.callout) { 19 | transition: transform 0.5s ease-out; 20 | 21 | &:hover { 22 | transform: translateY(-0.25rem); 23 | transition: transform 0.5s ease-out; 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | dist 14 | main.js 15 | 16 | # Exclude sourcemaps 17 | *.map 18 | 19 | # Obsidian 20 | workspace.json 21 | vault/**/*.md 22 | vault/.obsidian/plugins/ 23 | vault/.obsidian/plugins/periodic-notes/data.json 24 | 25 | # Exclude macOS Finder (System Explorer) View States 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: ["master"] 9 | # schedule: 10 | # - cron: '37 9 * * 4' 11 | 12 | jobs: 13 | build: 14 | name: Run Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Install 21 | run: npm ci 22 | 23 | - name: Run Build 24 | run: npm run build 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: ["master"] 9 | # schedule: 10 | # - cron: "37 9 * * 4" 11 | 12 | jobs: 13 | Lint: 14 | name: Run Linting 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Install 21 | run: npm ci 22 | 23 | - name: Run Lint 24 | run: npm run lint 25 | -------------------------------------------------------------------------------- /vault/.obsidian/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapse-filter": true, 3 | "search": "", 4 | "showTags": false, 5 | "showAttachments": false, 6 | "hideUnresolved": false, 7 | "showOrphans": true, 8 | "collapse-color-groups": true, 9 | "colorGroups": [], 10 | "collapse-display": true, 11 | "showArrow": false, 12 | "textFadeMultiplier": 0, 13 | "nodeSizeMultiplier": 1, 14 | "lineSizeMultiplier": 1, 15 | "collapse-forces": true, 16 | "centerStrength": 0.518713248970312, 17 | "repelStrength": 10, 18 | "linkStrength": 1, 19 | "linkDistance": 250, 20 | "scale": 1, 21 | "close": true 22 | } -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.4": "0.15.0", 6 | "1.1.0": "0.15.0", 7 | "2.0.0": "0.15.0", 8 | "2.0.1": "0.15.0", 9 | "2.0.3": "0.15.0", 10 | "2.0.4": "0.15.0", 11 | "2.0.5": "0.15.0", 12 | "2.0.6": "0.15.0", 13 | "2.0.7": "0.15.0", 14 | "2.0.8": "0.15.0", 15 | "2.1.0": "0.15.0", 16 | "2.1.1": "0.15.0", 17 | "2.2.0": "0.15.0", 18 | "2.3.0": "0.15.0", 19 | "2.3.1": "0.15.0", 20 | "2.3.2": "0.15.0", 21 | "2.4.0": "0.15.0", 22 | "2.4.1": "0.15.0", 23 | "2.4.2": "0.15.0", 24 | "2.4.3": "0.15.0", 25 | "2.4.4": "0.15.0", 26 | "2.5.0": "0.15.0", 27 | "2.6.0": "0.15.0", 28 | "2.7.0": "0.15.0" 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "skipLibCheck": true, 5 | "inlineSourceMap": true, 6 | "inlineSources": true, 7 | "module": "ESNext", 8 | "target": "es2018", 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "preact", 11 | "allowJs": true, 12 | "noImplicitAny": true, 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | "isolatedModules": true, 16 | "strictNullChecks": true, 17 | "lib": ["DOM", "ES5", "ES6", "ES7"] 18 | }, 19 | "include": ["**/*.ts"], 20 | "ts-node": { 21 | "transpileOnly": true, 22 | "files": true, 23 | "moduleTypes": { 24 | "scripts/generateNotes.ts": "cjs" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/SilentVoid13/Templater/blob/master/.github/workflows/release.yml 2 | name: Plugin release 3 | on: 4 | push: 5 | tags: 6 | - "[0-9]+.[0-9]+.[0-9]+" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: npm build 16 | run: | 17 | npm ci 18 | npm run build --if-present 19 | - name: Plugin release 20 | uses: ncipollo/release-action@v1.13.0 21 | with: 22 | artifacts: "dist/**/*" 23 | generateReleaseNotes: true 24 | draft: true 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /src/styles-settings.css: -------------------------------------------------------------------------------- 1 | .journal-review-settings { 2 | .time-spans-container { 3 | display: block; 4 | margin: 0; 5 | } 6 | 7 | .time-spans-container > li { 8 | list-style: none; 9 | margin: 0; 10 | 11 | &:not(:last-child) { 12 | margin-block-end: 1rem; 13 | } 14 | 15 | & .setting-item { 16 | & .setting-item-info { 17 | display: flex; 18 | flex-direction: row; 19 | align-items: baseline; 20 | gap: var(--size-4-4); 21 | margin-inline-end: var(--size-4-8); 22 | 23 | & .setting-item-description { 24 | line-height: var(--line-height-tight); 25 | padding-top: 0; 26 | } 27 | } 28 | } 29 | } 30 | 31 | .setting-item { 32 | h2 + & { 33 | border-top: none; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | groups: 13 | production-dependencies: 14 | dependency-type: "production" 15 | development-dependencies: 16 | dependency-type: "development" 17 | -------------------------------------------------------------------------------- /vault/.obsidian/core-plugins-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": true, 6 | "backlink": true, 7 | "canvas": true, 8 | "outgoing-link": true, 9 | "tag-pane": true, 10 | "properties": false, 11 | "page-preview": true, 12 | "daily-notes": true, 13 | "templates": true, 14 | "note-composer": true, 15 | "command-palette": true, 16 | "slash-command": false, 17 | "editor-status": true, 18 | "bookmarks": true, 19 | "markdown-importer": false, 20 | "zk-prefixer": false, 21 | "random-note": false, 22 | "outline": true, 23 | "word-count": true, 24 | "slides": false, 25 | "audio-recorder": false, 26 | "workspaces": false, 27 | "file-recovery": true, 28 | "publish": false, 29 | "sync": false 30 | } -------------------------------------------------------------------------------- /src/components/Main.tsx: -------------------------------------------------------------------------------- 1 | import TimeSpan from "./TimeSpan"; 2 | import { RenderedTimeSpan } from "../constants"; 3 | import useContext from "../hooks/useContext"; 4 | import { moment } from "obsidian"; 5 | 6 | interface Props { 7 | timeSpans: RenderedTimeSpan[]; 8 | startDate?: moment.Moment; 9 | } 10 | 11 | const Main = ({ timeSpans, startDate }: Props) => { 12 | const { settings } = useContext(); 13 | 14 | return ( 15 |
16 | {settings.renderOnFileSwitch && startDate ? ( 17 |

On {startDate.format("ll")}...

18 | ) : ( 19 |

On today...

20 | )} 21 | 22 | 27 |
28 | ); 29 | }; 30 | 31 | export default Main; 32 | -------------------------------------------------------------------------------- /vault/.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": false, 6 | "backlink": false, 7 | "canvas": false, 8 | "outgoing-link": false, 9 | "tag-pane": false, 10 | "properties": false, 11 | "page-preview": false, 12 | "daily-notes": true, 13 | "templates": true, 14 | "note-composer": false, 15 | "command-palette": true, 16 | "slash-command": false, 17 | "editor-status": true, 18 | "bookmarks": false, 19 | "markdown-importer": false, 20 | "zk-prefixer": false, 21 | "random-note": false, 22 | "outline": false, 23 | "word-count": false, 24 | "slides": false, 25 | "audio-recorder": false, 26 | "workspaces": false, 27 | "file-recovery": false, 28 | "publish": false, 29 | "sync": false, 30 | "webviewer": false, 31 | "footnotes": false, 32 | "bases": false 33 | } -------------------------------------------------------------------------------- /src/components/TimeSpan.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "preact"; 2 | import { TFile } from "obsidian"; 3 | import useContext from "../hooks/useContext"; 4 | import NotePreview from "./NotePreview"; 5 | 6 | interface Props { 7 | title: string; 8 | notes: TFile[]; 9 | wrapper?: React.JSX.Element; 10 | } 11 | 12 | const TimeSpan = ({ title, notes, wrapper }: Props) => { 13 | const { 14 | settings: { dayMargin }, 15 | } = useContext(); 16 | 17 | if (!notes.length) { 18 | return null; 19 | } 20 | 21 | const component = ( 22 | <> 23 |

24 | {title} 25 | {dayMargin ? (+/- {dayMargin} day(s)) : ""}: 26 |

27 | 28 | 35 | 36 | ); 37 | 38 | if (wrapper) { 39 | return React.cloneElement(wrapper, {}, component); 40 | } 41 | 42 | return component; 43 | }; 44 | 45 | export default TimeSpan; 46 | -------------------------------------------------------------------------------- /scripts/generateNotes.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync, readFileSync, writeFileSync } from "fs"; 2 | import * as moment from "moment"; 3 | 4 | interface PeriodicNotesSettings { 5 | daily: { 6 | format: string; 7 | folder: string; 8 | template: string; 9 | enabled: boolean; 10 | }; 11 | } 12 | 13 | const vaultPath = `${process.cwd()}/${process.argv[2]}`; 14 | const years = process.argv[3]; 15 | const today = moment(); 16 | const yearsAgo = moment().subtract(years, "years"); 17 | const settingsPath = `${vaultPath}/.obsidian/plugins/periodic-notes/data.json`; 18 | const settings = JSON.parse( 19 | readFileSync(settingsPath).toString(), 20 | ) as PeriodicNotesSettings; 21 | const dailyPath = `${vaultPath}${settings.daily.folder || "/"}`; 22 | 23 | for (let m = yearsAgo; m.diff(today, "days") <= 0; m.add(1, "days")) { 24 | const dailyNoteFormat = m.format(settings.daily.format); 25 | mkdirSync( 26 | `${dailyPath}${dailyNoteFormat.split("/").slice(0, -1).join("/")}`, 27 | { recursive: true }, 28 | ); 29 | writeFileSync( 30 | `${dailyPath}${dailyNoteFormat}.md`, 31 | `daily Note for ${dailyNoteFormat}`, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Liam Cain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-plugin-journal-review", 3 | "version": "2.7.0", 4 | "description": "Review your daily notes on their anniversaries, like \"what happened today last year\"\n\n", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && npm run format && git add manifest.json versions.json", 10 | "lint": "eslint . && knip && prettier --check .", 11 | "format": "prettier --write .", 12 | "generateNotes": "ts-node ./scripts/generateNotes.ts vault 5", 13 | "knip": "knip" 14 | }, 15 | "keywords": [], 16 | "author": "Kageetai", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "@eslint/js": "^9.39.0", 20 | "@types/node": "^24.10.0", 21 | "builtin-modules": "^5.0.0", 22 | "esbuild": "^0.25.12", 23 | "esbuild-plugin-copy": "^2.1.1", 24 | "eslint": "^9.39.0", 25 | "globals": "^16.5.0", 26 | "knip": "^5.67.1", 27 | "moment": "^2.30.1", 28 | "obsidian": "latest", 29 | "prettier": "^3.6.2", 30 | "ts-node": "^10.9.2", 31 | "tslib": "^2.8.1", 32 | "typescript": "^5.9.3", 33 | "typescript-eslint": "^8.46.2" 34 | }, 35 | "dependencies": { 36 | "obsidian-daily-notes-interface": "^0.9.4", 37 | "preact": "^10.27.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | import { copy } from "esbuild-plugin-copy"; 5 | 6 | const banner = `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = process.argv[2] === "production"; 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts", "src/styles.css"], 19 | bundle: true, 20 | minify: true, 21 | external: [ 22 | "obsidian", 23 | "electron", 24 | "@codemirror/autocomplete", 25 | "@codemirror/collab", 26 | "@codemirror/commands", 27 | "@codemirror/language", 28 | "@codemirror/lint", 29 | "@codemirror/search", 30 | "@codemirror/state", 31 | "@codemirror/view", 32 | "@lezer/common", 33 | "@lezer/highlight", 34 | "@lezer/lr", 35 | ...builtins, 36 | ], 37 | format: "cjs", 38 | target: "es2018", 39 | logLevel: "info", 40 | sourcemap: prod ? false : "inline", 41 | treeShaking: true, 42 | outdir: "dist", 43 | plugins: [ 44 | copy({ 45 | // this is equal to process.cwd(), which means we use cwd path as base path to resolve `to` path 46 | // if not specified, this plugin uses ESBuild.build outdir/outfile options as base path. 47 | // resolveFrom: 'cwd', 48 | assets: { 49 | from: ["./manifest.json"], 50 | to: ["./manifest.json"], 51 | }, 52 | watch: true, 53 | }), 54 | ], 55 | }); 56 | 57 | if (prod) { 58 | await context.rebuild(); 59 | process.exit(0); 60 | } else { 61 | await context.watch(); 62 | } 63 | -------------------------------------------------------------------------------- /src/components/NotePreview.tsx: -------------------------------------------------------------------------------- 1 | import { Keymap, MarkdownRenderer, TFile } from "obsidian"; 2 | import { Ref } from "preact"; 3 | import { useRef } from "preact/hooks"; 4 | import useContext from "../hooks/useContext"; 5 | 6 | interface Props { 7 | note: TFile; 8 | } 9 | 10 | const NotePreview = ({ note }: Props) => { 11 | const { app, view, settings } = useContext(); 12 | const ref = useRef(null); 13 | 14 | void (async () => { 15 | const slicedContent = (await app.vault.cachedRead(note)) 16 | // remove frontmatter 17 | .replace(/---.*?---/s, "") 18 | // remove custom regex 19 | .replace(new RegExp(settings.noteMarkdownRegex, "sg"), "") 20 | // restrict to chosen preview length 21 | .substring(0, settings.previewLength); 22 | 23 | if (ref.current) { 24 | // clear the element before rendering, otherwise it will append 25 | ref.current.innerHTML = ""; 26 | 27 | await MarkdownRenderer.render( 28 | app, 29 | slicedContent, 30 | ref.current, 31 | note.path, 32 | view, 33 | ); 34 | } 35 | })(); 36 | 37 | const onClick = (evt: MouseEvent) => { 38 | const isMiddleButton = evt.button === 1; 39 | const newLeaf = 40 | Keymap.isModEvent(evt) || isMiddleButton || settings.openInNewPane; 41 | 42 | void app.workspace.getLeaf(newLeaf).openFile(note); 43 | }; 44 | 45 | if (settings.useCallout) { 46 | return ( 47 |
48 | {settings.showNoteTitle && ( 49 |
50 |
{note.basename}
51 |
52 | )} 53 | 54 |
} /> 55 |
56 | ); 57 | } 58 | 59 | return ( 60 |
61 | {settings.showNoteTitle &&

{note.basename}

} 62 | 63 | 64 | {settings.useQuote ? ( 65 |
} /> 66 | ) : ( 67 |
} /> 68 | )} 69 | 70 |
71 | ); 72 | }; 73 | 74 | export default NotePreview; 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian Plugin Journal Review 2 | 3 | This is a plugin helping you "review" your daily notes on their anniversaries. 4 | 5 | It pulls from all your daily notes (also using setting from the Periodic Notes plugin if present) and present notes from 6 | the same day in previous time spans in a view, that can be activated via ribbon button or command palette. 7 | 8 | ## Installation 9 | 10 | 1. Find the under "Journal Review" in the community plugins list in Obsidian 11 | 2. Click "Install" and "Enable" 12 | 3. Configure "Options" to your liking 13 | 14 | ## Usage 15 | 16 | Open the "On this day" view via the ribbon icon or the command palette, and it will display excerpts from previous daily 17 | notes. 18 | The view will refresh when a new daily note is created or around midnight to reflect the changes. 19 | 20 | ## Release 21 | 22 | 1. `npm run version` to update necessary manifest files 23 | 2. Push a new tag to GitHub with the corresponding version number 24 | 3. This automatically triggers a GitHub action that builds the plugin, creates a release and uploads the artifacts to 25 | the release 26 | 27 | ## Options 28 | 29 | ### Time Spans 30 | 31 | Time spans to review, can be defined via three values: `number`, `unit` and `recurring`, meaning how many times of the 32 | given unit to look back to, either once or recurring. 33 | 34 | **Default (as configured via UI elements):** 35 | 36 | ```js 37 | [ 38 | { number: 1, unit: Unit.month, recurring: false }, 39 | { number: 6, unit: Unit.month, recurring: false }, 40 | { number: 1, unit: Unit.year, recurring: true }, 41 | ]; 42 | ``` 43 | 44 | ### Show Note Title with previews 45 | 46 | Render the note title above the preview text, when showing note previews. 47 | 48 | ### Humanize Time Spans 49 | 50 | Use the 'humanization' feature from moment.js, when rendering the time spans titles. 51 | 52 | ### Use Obsidian callouts for note previews 53 | 54 | Use callouts to render note previews, using their styles based on current theme. 55 | 56 | ### Use quote element for note previews 57 | 58 | Format note previews using the HTML quote element 59 | 60 | ### Lookup Margin 61 | 62 | The number of days to include before and after the date being checked 63 | 64 | **Default:** `0` 65 | 66 | ### Preview Length 67 | 68 | Length of the preview text to show for each note 69 | 70 | **Default:** `200` 71 | 72 | ### Open in new pane 73 | 74 | Open the notes in a new pane/tab by default 75 | 76 | ### Use notifications 77 | 78 | Use notifications (inside Obsidian) to let you know, when there are new journal entries to review. This will happen when Obsidian is focused and it's a new day. 79 | 80 | ### Show Random Daily Note 81 | 82 | Enable/disable the random daily note feature. 83 | 84 | ### Random Note Position 85 | 86 | Choose whether to show the random daily note on top or bottom. 87 | -------------------------------------------------------------------------------- /src/view.tsx: -------------------------------------------------------------------------------- 1 | import { debounce, ItemView, moment, TFile, WorkspaceLeaf } from "obsidian"; 2 | import { 3 | appHasDailyNotesPluginLoaded, 4 | getAllDailyNotes, 5 | getDateFromFile, 6 | } from "obsidian-daily-notes-interface"; 7 | import { render } from "preact"; 8 | import Main from "./components/Main"; 9 | import AppContext from "./components/context"; 10 | import { icon } from "./main"; 11 | import { reduceTimeSpans, Settings, VIEW_TYPE } from "./constants"; 12 | 13 | export default class OnThisDayView extends ItemView { 14 | private readonly settings: Settings; 15 | 16 | icon = icon; 17 | 18 | constructor(leaf: WorkspaceLeaf, settings: Settings) { 19 | super(leaf); 20 | 21 | this.settings = settings; 22 | 23 | this.app.workspace.onLayoutReady(() => { 24 | this.registerEvent( 25 | this.app.vault.on("create", (file: TFile) => { 26 | if (getDateFromFile(file, "day")) { 27 | this.debouncedRenderView(); 28 | } 29 | }), 30 | ); 31 | this.registerEvent( 32 | this.app.vault.on("delete", (file: TFile) => { 33 | if (getDateFromFile(file, "day")) { 34 | this.debouncedRenderView(); 35 | } 36 | }), 37 | ); 38 | this.registerEvent( 39 | this.app.vault.on("rename", (file: TFile) => { 40 | if (getDateFromFile(file, "day")) { 41 | this.debouncedRenderView(); 42 | } 43 | }), 44 | ); 45 | 46 | this.registerEvent( 47 | this.app.workspace.on("file-open", (file) => { 48 | const dateFromFile = file && getDateFromFile(file, "day"); 49 | 50 | if (this.settings.renderOnFileSwitch && dateFromFile) { 51 | this.debouncedRenderView(dateFromFile); 52 | } 53 | }), 54 | ); 55 | 56 | // rerender at midnight 57 | this.registerInterval( 58 | window.setInterval( 59 | () => { 60 | if (new Date().getHours() === 0) { 61 | this.renderView(); 62 | } 63 | }, 64 | 60 * 60 * 1000, 65 | ), 66 | ); 67 | }); 68 | } 69 | 70 | getViewType() { 71 | return VIEW_TYPE; 72 | } 73 | 74 | getDisplayText() { 75 | return "On this day"; 76 | } 77 | 78 | renderView(startDate?: moment.Moment) { 79 | const container = this.containerEl.children[1]; 80 | const hasDailyNotesPluginLoaded = appHasDailyNotesPluginLoaded(); 81 | 82 | if (!hasDailyNotesPluginLoaded) { 83 | container.createEl("b", { 84 | text: "Daily notes plugin not loaded", 85 | }); 86 | 87 | return; 88 | } 89 | 90 | const timeSpans = reduceTimeSpans( 91 | getAllDailyNotes(), 92 | this.settings, 93 | startDate, 94 | ); 95 | 96 | render( 97 | 104 |
105 | , 106 | container, 107 | ); 108 | } 109 | 110 | debouncedRenderView = debounce(this.renderView.bind(this), 500); 111 | 112 | async onOpen() { 113 | this.debouncedRenderView(); 114 | } 115 | 116 | async onClose() {} 117 | } 118 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { moment, Notice, Plugin, WorkspaceLeaf } from "obsidian"; 2 | import { getAllDailyNotes } from "obsidian-daily-notes-interface"; 3 | 4 | import OnThisDayView from "./view"; 5 | import { 6 | DEFAULT_SETTINGS, 7 | reduceTimeSpans, 8 | Settings, 9 | TimeSpan, 10 | Unit, 11 | VIEW_TYPE, 12 | } from "./constants"; 13 | import { SettingsTab } from "./settingsTab"; 14 | 15 | export const icon = "calendar-clock"; 16 | const label = "Open 'On this day' view"; 17 | 18 | export default class JournalReviewPlugin extends Plugin { 19 | settings: Settings; 20 | 21 | checkIsNewDay = () => { 22 | if ( 23 | !this.settings.date || 24 | moment(new Date()).isAfter(this.settings.date, "day") 25 | ) { 26 | this.settings.date = new Date().toISOString(); 27 | void this.saveSettings(); 28 | 29 | const noteCount = reduceTimeSpans( 30 | getAllDailyNotes(), 31 | this.settings, 32 | ).reduce((count, timeSpan) => count + timeSpan.notes.length, 0); 33 | 34 | if (noteCount) { 35 | new Notice( 36 | `It's a new day! You have ${noteCount} journal entries to review. Open the "On this day" view to see them.`, 37 | 0, 38 | ); 39 | } 40 | } 41 | }; 42 | 43 | setupFocusListener = () => { 44 | if (this.settings.useNotifications) { 45 | // setup event listener to check if it's a new day and fire notification if so 46 | // need to wait for notes to be loaded 47 | setTimeout(this.checkIsNewDay, 1000); 48 | addEventListener("focus", this.checkIsNewDay); 49 | } else { 50 | removeEventListener("focus", this.checkIsNewDay); 51 | } 52 | }; 53 | 54 | async onload() { 55 | await this.loadSettings(); 56 | 57 | // This creates an icon in the left ribbon. 58 | this.addRibbonIcon(icon, label, () => { 59 | void this.activateView(); 60 | }); 61 | 62 | // This adds a simple command that can be triggered anywhere 63 | this.addCommand({ 64 | id: "open-on-this-day", 65 | name: label, 66 | callback: () => this.activateView(), 67 | }); 68 | 69 | // This adds a settings tab so the user can configure various aspects of the plugin 70 | this.addSettingTab(new SettingsTab(this.app, this)); 71 | 72 | this.registerView( 73 | VIEW_TYPE, 74 | (leaf) => new OnThisDayView(leaf, this.settings), 75 | ); 76 | 77 | this.setupFocusListener(); 78 | } 79 | 80 | async activateView() { 81 | const { workspace } = this.app; 82 | 83 | let leaf: WorkspaceLeaf | null; 84 | const leaves = workspace.getLeavesOfType(VIEW_TYPE); 85 | 86 | if (leaves.length > 0) { 87 | // A leaf with our view already exists, use that 88 | leaf = leaves[0]; 89 | } else { 90 | // Our view could not be found in the workspace, create a new leaf 91 | // in the right sidebar for it 92 | leaf = workspace.getRightLeaf(false); 93 | await leaf?.setViewState({ type: VIEW_TYPE, active: true }); 94 | } 95 | 96 | // "Reveal" the leaf in case it is in a collapsed sidebar 97 | await workspace.revealLeaf(leaf!); 98 | 99 | this.renderView(); 100 | } 101 | 102 | onunload() { 103 | removeEventListener("focus", this.checkIsNewDay); 104 | } 105 | 106 | async loadSettings() { 107 | // the settings could be in an outdated format 108 | const loadedData = (await this.loadData()) as Settings & { 109 | timeSpans: [number, string][]; 110 | }; 111 | const parsedData: Settings = loadedData; 112 | 113 | // check if v1 settings are loaded and convert them to v2 114 | if ( 115 | loadedData?.timeSpans?.length && 116 | Object.prototype.hasOwnProperty.call(loadedData.timeSpans[0], "length") 117 | ) { 118 | parsedData.timeSpans = loadedData.timeSpans.map( 119 | ([number, unit]: [number, string]) => ({ 120 | number, 121 | unit: (unit.endsWith("s") ? unit.slice(0, -1) : unit) as Unit, 122 | recurring: false, 123 | }), 124 | ) as TimeSpan[]; 125 | } 126 | 127 | this.settings = Object.assign({}, DEFAULT_SETTINGS, parsedData); 128 | } 129 | 130 | renderView() { 131 | const leaf = this.app.workspace.getLeavesOfType(VIEW_TYPE).first(); 132 | 133 | if (leaf && leaf.view instanceof OnThisDayView) { 134 | leaf.view.renderView(); 135 | } 136 | } 137 | 138 | async saveSettings() { 139 | await this.saveData(this.settings); 140 | this.renderView(); 141 | this.setupFocusListener(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import { moment, TFile } from "obsidian"; 2 | import { 3 | type getAllDailyNotes, 4 | getDailyNote, 5 | getDateFromFile, 6 | } from "obsidian-daily-notes-interface"; 7 | 8 | export const DEBOUNCE_DELAY = 1000; 9 | export const VIEW_TYPE = "on-this-day-view"; 10 | 11 | export enum Unit { 12 | day = "day", 13 | week = "week", 14 | month = "month", 15 | year = "year", 16 | } 17 | 18 | type AllDailyNotes = ReturnType; 19 | 20 | /** 21 | * TimeSpan type to define possible time span user can define 22 | * consisting of a number, e.g. 6, a unit, e.g. months, and whether it's recurring 23 | * @example {number: 1, unit: Unit.month, recurring: false} 24 | */ 25 | export type TimeSpan = { 26 | number: number; 27 | unit: Unit; 28 | recurring?: boolean; 29 | }; 30 | 31 | export type RenderedTimeSpan = { 32 | title: string; 33 | notes: TFile[]; 34 | moment: moment.Moment; 35 | }; 36 | 37 | export type RandomNotePosition = "top" | "bottom"; 38 | 39 | export type SortOrder = "asc" | "desc"; 40 | 41 | export const defaultTimeSpans: TimeSpan[] = [ 42 | { number: 1, unit: Unit.month, recurring: false }, 43 | { number: 6, unit: Unit.month, recurring: false }, 44 | { number: 1, unit: Unit.year, recurring: true }, 45 | ]; 46 | 47 | export interface Settings { 48 | timeSpans: TimeSpan[]; 49 | dayMargin: number; 50 | sortOrder: SortOrder; 51 | previewLength: number; 52 | useHumanize: boolean; 53 | useCallout: boolean; 54 | useQuote: boolean; 55 | openInNewPane: boolean; 56 | showNoteTitle: boolean; 57 | useNotifications: boolean; 58 | renderOnFileSwitch: boolean; 59 | date: string; 60 | noteMarkdownRegex: string; 61 | showRandomNote: boolean; 62 | randomNotePosition: RandomNotePosition; 63 | } 64 | 65 | export const DEFAULT_SETTINGS: Settings = { 66 | timeSpans: defaultTimeSpans, 67 | dayMargin: 0, 68 | sortOrder: "desc", 69 | previewLength: 100, 70 | useHumanize: true, 71 | useCallout: true, 72 | useQuote: true, 73 | openInNewPane: false, 74 | showNoteTitle: true, 75 | useNotifications: true, 76 | renderOnFileSwitch: false, 77 | date: "", 78 | noteMarkdownRegex: "", 79 | showRandomNote: false, 80 | randomNotePosition: "bottom", 81 | }; 82 | 83 | export const getTimeSpanTitle = ({ number, unit, recurring }: TimeSpan) => 84 | `${recurring ? "every" : ""} ${number} ${unit}${number > 1 ? "s" : ""}`; 85 | 86 | const reduceToOldestNote = (oldestDate: moment.Moment, currentNote: TFile) => { 87 | const currentDate = getDateFromFile(currentNote, "day"); 88 | return currentDate?.isBefore(oldestDate) ? currentDate : oldestDate; 89 | }; 90 | 91 | const getTitle = ( 92 | useHumanize: boolean, 93 | now: moment.Moment, 94 | startDate: moment.Moment, 95 | unit: Unit, 96 | ) => 97 | useHumanize 98 | ? now.from(startDate) 99 | : `${getTimeSpanTitle({ number: startDate.diff(now, unit), unit })} ago`; 100 | 101 | const getNotesOverMargins = ( 102 | dayMargin: number, 103 | mom: moment.Moment, 104 | allDailyNotes: AllDailyNotes, 105 | ) => 106 | Array.from({ length: dayMargin * 2 + 1 }, (_, i) => 107 | getDailyNote(moment(mom).add(i - dayMargin, "days"), allDailyNotes), 108 | ).filter(Boolean); 109 | 110 | const sortRenderedTimeSpanByDateAsc = ( 111 | a: RenderedTimeSpan, 112 | b: RenderedTimeSpan, 113 | ) => a.moment.valueOf() - b.moment.valueOf(); 114 | 115 | const sortRenderedTimeSpanByDateDesc = ( 116 | a: RenderedTimeSpan, 117 | b: RenderedTimeSpan, 118 | ) => b.moment.valueOf() - a.moment.valueOf(); 119 | 120 | const sortTFileByCtimeAsc = (a: TFile, b: TFile) => a.stat.ctime - b.stat.ctime; 121 | 122 | const sortTFileByCtimeDesc = (a: TFile, b: TFile) => 123 | b.stat.ctime - a.stat.ctime; 124 | 125 | const isDuplicateNote = (note: TFile, notes: TFile[]) => 126 | notes.some((existingNote) => existingNote.path === note.path); 127 | 128 | const getRandomDailyNote = (allDailyNotes: AllDailyNotes) => { 129 | if (!Object.keys(allDailyNotes).length) { 130 | return null; 131 | } 132 | 133 | const dailyNotes = Object.values(allDailyNotes); 134 | const randomIndex = Math.floor(Math.random() * dailyNotes.length); 135 | return dailyNotes[randomIndex]; 136 | }; 137 | 138 | export const reduceTimeSpans = ( 139 | allDailyNotes: AllDailyNotes, 140 | settings: Settings, 141 | startDate: moment.Moment = moment(), 142 | ): RenderedTimeSpan[] => { 143 | const oldestNoteDate = Object.values(allDailyNotes).reduce( 144 | reduceToOldestNote, 145 | startDate, 146 | ); 147 | 148 | const renderedTimeSpans = Object.values( 149 | settings.timeSpans.reduce>( 150 | (acc, { number, unit, recurring }) => { 151 | const mom = moment(startDate); 152 | 153 | // if we have a recurring time span, we want to go back until we reach the oldest note 154 | do { 155 | // go back one unit of the timespan 156 | mom.subtract(number, unit); 157 | 158 | const title = getTitle(settings.useHumanize, mom, startDate, unit); 159 | const notes = getNotesOverMargins( 160 | settings.dayMargin, 161 | mom, 162 | allDailyNotes, 163 | ); 164 | 165 | if (notes.length) { 166 | // used mapped object type to group notes together under same titles, 167 | // even if they come from different time span settings 168 | acc[title] = { 169 | title, 170 | moment: mom.clone(), 171 | notes: (acc[title] 172 | ? acc[title].notes.concat( 173 | notes.filter( 174 | (note) => !isDuplicateNote(note, acc[title].notes), 175 | ), 176 | ) 177 | : notes 178 | ).sort( 179 | settings.sortOrder === "asc" 180 | ? sortTFileByCtimeAsc 181 | : sortTFileByCtimeDesc, 182 | ), 183 | }; 184 | } 185 | } while (mom.isAfter(oldestNoteDate) && recurring); 186 | 187 | return acc; 188 | }, 189 | {}, 190 | ), 191 | ).sort( 192 | settings.sortOrder === "asc" 193 | ? sortRenderedTimeSpanByDateAsc 194 | : sortRenderedTimeSpanByDateDesc, 195 | ); 196 | 197 | // add a random note to the top or bottom of the list 198 | // if the user has set the option 199 | const randomNote = getRandomDailyNote(allDailyNotes); 200 | if (settings.showRandomNote && randomNote) { 201 | const randomNoteTimeSpan: RenderedTimeSpan = { 202 | title: "a random day", 203 | notes: [randomNote], 204 | moment: moment(randomNote.stat.mtime), 205 | }; 206 | 207 | if (settings.randomNotePosition === "top") { 208 | renderedTimeSpans.unshift(randomNoteTimeSpan); 209 | } else { 210 | renderedTimeSpans.push(randomNoteTimeSpan); 211 | } 212 | } 213 | 214 | return renderedTimeSpans; 215 | }; 216 | -------------------------------------------------------------------------------- /src/settingsTab.ts: -------------------------------------------------------------------------------- 1 | import { App, debounce, PluginSettingTab, Setting } from "obsidian"; 2 | import { 3 | DEBOUNCE_DELAY, 4 | defaultTimeSpans, 5 | getTimeSpanTitle, 6 | RandomNotePosition, 7 | SortOrder, 8 | Unit, 9 | } from "./constants"; 10 | import JournalReviewPlugin from "./main"; 11 | 12 | const getMaxTimeSpan = (unit: Unit) => { 13 | switch (unit) { 14 | case Unit.day: 15 | return 31; 16 | case Unit.week: 17 | return 52; 18 | case Unit.month: 19 | return 24; 20 | case Unit.year: 21 | return 100; 22 | } 23 | }; 24 | 25 | export class SettingsTab extends PluginSettingTab { 26 | plugin: JournalReviewPlugin; 27 | 28 | constructor(app: App, plugin: JournalReviewPlugin) { 29 | super(app, plugin); 30 | this.plugin = plugin; 31 | } 32 | 33 | display(): void { 34 | const { containerEl } = this; 35 | 36 | containerEl.empty(); 37 | containerEl.addClass("journal-review-settings"); 38 | 39 | new Setting(containerEl).setName("Time Spans").setHeading(); 40 | 41 | const container = containerEl.createEl("ul"); 42 | container.addClasses(["setting-item", "time-spans-container"]); 43 | 44 | container.createEl("li", { 45 | cls: "setting-item-description", 46 | text: "Define time spans to review, e.g. '1 month' or 'every 6 months'. Overlapping time spans may cause duplicate entries.", 47 | }); 48 | 49 | this.plugin.settings.timeSpans.forEach( 50 | ({ number, unit, recurring }, index) => { 51 | const timeSpanContainer = container.createEl("li"); 52 | 53 | new Setting(timeSpanContainer) 54 | .setName(`Time span #${index + 1}`) 55 | .setDesc(getTimeSpanTitle({ number, unit, recurring })) 56 | .addSlider((slider) => 57 | slider 58 | .setLimits(1, getMaxTimeSpan(unit), 1) 59 | .setDynamicTooltip() 60 | .setValue(number) 61 | .onChange( 62 | debounce( 63 | (value) => { 64 | this.plugin.settings.timeSpans[index].number = value; 65 | void this.plugin.saveSettings(); 66 | this.display(); 67 | }, 68 | DEBOUNCE_DELAY, 69 | true, 70 | ), 71 | ), 72 | ) 73 | .addDropdown((dropdown) => 74 | dropdown 75 | .addOptions(Unit) 76 | .setValue(unit) 77 | .onChange((value) => { 78 | this.plugin.settings.timeSpans[index].unit = value as Unit; 79 | void this.plugin.saveSettings(); 80 | this.display(); 81 | }), 82 | ) 83 | .addToggle((toggle) => 84 | toggle 85 | .setValue(Boolean(recurring)) 86 | .onChange((value) => { 87 | this.plugin.settings.timeSpans[index].recurring = value; 88 | void this.plugin.saveSettings(); 89 | this.display(); 90 | }) 91 | .setTooltip("Recurring?"), 92 | ) 93 | .addButton((button) => 94 | button 95 | .setButtonText("X") 96 | .setIcon("delete") 97 | .setTooltip("Delete") 98 | .onClick(() => { 99 | this.plugin.settings.timeSpans.splice(index, 1); 100 | void this.plugin.saveSettings(); 101 | this.display(); 102 | }), 103 | ); 104 | }, 105 | ); 106 | 107 | new Setting(container.createEl("li")).addButton((button) => 108 | button 109 | .setCta() 110 | .setButtonText("Add Time Span") 111 | .onClick(() => { 112 | this.plugin.settings.timeSpans.push({ 113 | ...defaultTimeSpans[0], 114 | }); 115 | void this.plugin.saveSettings(); 116 | this.display(); 117 | }), 118 | ); 119 | 120 | new Setting(containerEl) 121 | .setName("Lookup Margin") 122 | .setDesc( 123 | "The number of days to include before and after the date being checked", 124 | ) 125 | .addSlider((slider) => 126 | slider 127 | .setDynamicTooltip() 128 | .setValue(this.plugin.settings.dayMargin) 129 | .onChange((value) => { 130 | this.plugin.settings.dayMargin = value; 131 | void this.plugin.saveSettings(); 132 | }), 133 | ); 134 | 135 | new Setting(containerEl) 136 | .setName("Sort Order") 137 | .setDesc("Order time spans and notes either by oldest or newest first.") 138 | .addDropdown((dropdown) => { 139 | dropdown 140 | .addOptions({ asc: "Oldest first", desc: "Newest first" }) 141 | .setValue(this.plugin.settings.sortOrder) 142 | .onChange((value: SortOrder) => { 143 | this.plugin.settings.sortOrder = value; 144 | void this.plugin.saveSettings(); 145 | }); 146 | }); 147 | 148 | new Setting(containerEl) 149 | .setName("Date based on selected note") 150 | .setDesc("Use the date of the currently open daily note.") 151 | .addToggle((toggle) => { 152 | toggle 153 | .setValue(this.plugin.settings.renderOnFileSwitch) 154 | .onChange((value) => { 155 | this.plugin.settings.renderOnFileSwitch = value; 156 | void this.plugin.saveSettings(); 157 | }); 158 | }); 159 | 160 | new Setting(containerEl).setName("Random Daily Note").setHeading(); 161 | 162 | new Setting(containerEl) 163 | .setName("Show Random Daily Note") 164 | .setDesc("Show a random daily note besides the other notes.") 165 | .addToggle((toggle) => 166 | toggle 167 | .setValue(this.plugin.settings.showRandomNote) 168 | .onChange((value) => { 169 | this.plugin.settings.showRandomNote = value; 170 | void this.plugin.saveSettings(); 171 | }), 172 | ); 173 | 174 | new Setting(containerEl) 175 | .setName("Random Note Position") 176 | .setDesc("Whether to show the random daily note on top or bottom.") 177 | .addDropdown((dropdown) => 178 | dropdown 179 | .addOptions({ top: "Top", bottom: "Bottom" }) 180 | .setValue(this.plugin.settings.randomNotePosition) 181 | .onChange((value: RandomNotePosition) => { 182 | this.plugin.settings.randomNotePosition = value; 183 | void this.plugin.saveSettings(); 184 | }), 185 | ); 186 | 187 | new Setting(containerEl).setName("Previews").setHeading(); 188 | 189 | new Setting(containerEl) 190 | .setName("Preview Length") 191 | .setDesc("Length of the preview text to show for each note.") 192 | .addSlider((slider) => 193 | slider 194 | .setLimits(0, 1000, 10) 195 | .setDynamicTooltip() 196 | .setValue(this.plugin.settings.previewLength) 197 | .onChange((value) => { 198 | console.log("preview length", value); 199 | this.plugin.settings.previewLength = value; 200 | void this.plugin.saveSettings(); 201 | }), 202 | ); 203 | 204 | new Setting(containerEl) 205 | .setName("Show Note Title with previews") 206 | .setDesc( 207 | "Render the note title above the preview text, when showing note previews.", 208 | ) 209 | .addToggle((toggle) => 210 | toggle 211 | .setValue(this.plugin.settings.showNoteTitle) 212 | .onChange((value) => { 213 | this.plugin.settings.showNoteTitle = value; 214 | void this.plugin.saveSettings(); 215 | }), 216 | ); 217 | 218 | const humanizeDescription = new DocumentFragment(); 219 | humanizeDescription.textContent = 220 | "Use the 'humanization' feature from moment.js, when rendering the time spans titles. "; 221 | humanizeDescription.createEl("a", { 222 | text: "More info", 223 | attr: { 224 | href: "https://momentjs.com/docs/#/durations/humanize/", 225 | }, 226 | }); 227 | 228 | new Setting(containerEl) 229 | .setName("Humanize Time Spans") 230 | .setDesc(humanizeDescription) 231 | .addToggle((toggle) => 232 | toggle.setValue(this.plugin.settings.useHumanize).onChange((value) => { 233 | this.plugin.settings.useHumanize = value; 234 | void this.plugin.saveSettings(); 235 | }), 236 | ); 237 | 238 | const calloutsDescription = new DocumentFragment(); 239 | calloutsDescription.textContent = 240 | "Use callouts to render note previews, using their styles based on current theme. "; 241 | calloutsDescription.createEl("a", { 242 | text: "More info", 243 | attr: { 244 | href: "https://help.obsidian.md/Editing+and+formatting/Callouts", 245 | }, 246 | }); 247 | 248 | new Setting(containerEl) 249 | .setName("Use Obsidian callouts for note previews") 250 | .setDesc(calloutsDescription) 251 | .addToggle((toggle) => 252 | toggle.setValue(this.plugin.settings.useCallout).onChange((value) => { 253 | this.plugin.settings.useCallout = value; 254 | void this.plugin.saveSettings(); 255 | this.display(); 256 | }), 257 | ); 258 | 259 | if (!this.plugin.settings.useCallout) { 260 | new Setting(containerEl) 261 | .setName("Use quote element for note previews") 262 | .setDesc("Format note previews using the HTML quote element") 263 | .addToggle((toggle) => 264 | toggle.setValue(this.plugin.settings.useQuote).onChange((value) => { 265 | this.plugin.settings.useQuote = value; 266 | void this.plugin.saveSettings(); 267 | }), 268 | ); 269 | } 270 | 271 | new Setting(containerEl) 272 | .setName("Open in new pane") 273 | .setDesc( 274 | "Open the notes in a new pane/tab by default when clicking them.", 275 | ) 276 | .addToggle((toggle) => 277 | toggle 278 | .setValue(this.plugin.settings.openInNewPane) 279 | .onChange((value) => { 280 | this.plugin.settings.openInNewPane = value; 281 | void this.plugin.saveSettings(); 282 | }), 283 | ); 284 | 285 | new Setting(containerEl).setName("Other").setHeading(); 286 | 287 | new Setting(containerEl) 288 | .setName("Use notifications") 289 | .setDesc( 290 | "Use notifications (inside Obsidian) to let you know, when there are new journal entries to review. This will happen when Obsidian is focused and it's a new day.", 291 | ) 292 | .addToggle((toggle) => 293 | toggle 294 | .setValue(this.plugin.settings.useNotifications) 295 | .onChange((value) => { 296 | this.plugin.settings.useNotifications = value; 297 | void this.plugin.saveSettings(); 298 | }), 299 | ); 300 | 301 | new Setting(containerEl) 302 | .setName("Note filtering regex") 303 | .setDesc( 304 | "Use any string or regex (without `/` at beginning and end or flags) to filter the note content before rendering.", 305 | ) 306 | .addText((text) => 307 | text 308 | .setValue(this.plugin.settings.noteMarkdownRegex) 309 | .onChange((value) => { 310 | this.plugin.settings.noteMarkdownRegex = value; 311 | void this.plugin.saveSettings(); 312 | }), 313 | ); 314 | } 315 | } 316 | --------------------------------------------------------------------------------