├── .npmrc ├── .eslintignore ├── images ├── settings-screenshot.png └── query-edit-modal-screenshot.png ├── versions.json ├── src ├── settingStore.ts ├── gui │ ├── modals │ │ ├── EditQueryModal │ │ │ ├── editQueryModal.ts │ │ │ └── EditQueryModal.svelte │ │ └── OpenRandomNoteModal │ │ │ ├── openRandomNoteModal.ts │ │ │ └── OpenRandomNoteModal.svelte │ └── queryItem │ │ ├── QueryList.svelte │ │ ├── QueryItem.svelte │ │ └── QueryView.svelte ├── types.ts ├── settings.ts ├── utilities.ts ├── main.ts └── search.ts ├── .editorconfig ├── styles.css ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── package.json ├── LICENSE ├── esbuild.config.mjs └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /images/settings-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karstenpedersen/obsidian-advanced-random-note/HEAD/images/settings-screenshot.png -------------------------------------------------------------------------------- /images/query-edit-modal-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karstenpedersen/obsidian-advanced-random-note/HEAD/images/query-edit-modal-screenshot.png -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.6": "0.15.0", 3 | "0.0.7": "0.15.0", 4 | "0.0.8": "0.15.0", 5 | "0.0.9": "0.15.0", 6 | "0.0.10": "0.15.0", 7 | "0.0.11": "0.15.0" 8 | } 9 | -------------------------------------------------------------------------------- /src/settingStore.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from "./settings"; 2 | import { writable } from "svelte/store"; 3 | 4 | const settingStore = writable(); 5 | export default { settingStore }; 6 | -------------------------------------------------------------------------------- /.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 = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file will be included with your plugin, and 4 | available in the app when your plugin is enabled. 5 | 6 | If your plugin does not need CSS, delete this file. 7 | 8 | */ 9 | 10 | .arn-input { 11 | width: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "advanced-random-note", 3 | "name": "Advanced Random Note", 4 | "version": "0.0.11", 5 | "minAppVersion": "0.15.0", 6 | "description": "Open random notes with custom queries in languages like Dataview and Regex.", 7 | "author": "Karsten Finderup Pedersen", 8 | "authorUrl": "https://github.com/karstenpedersen", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /.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 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["svelte", "node"], 4 | "strict": true, 5 | "baseUrl": ".", 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "module": "ESNext", 9 | "target": "ES6", 10 | "allowJs": true, 11 | "noImplicitAny": true, 12 | "moduleResolution": "node", 13 | "importHelpers": true, 14 | "isolatedModules": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "lib": ["DOM", "ES5", "ES6", "ES7"] 18 | }, 19 | "include": ["**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /src/gui/modals/EditQueryModal/editQueryModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from "obsidian"; 2 | import Component from "./EditQueryModal.svelte"; 3 | import { Query } from "src/types"; 4 | 5 | export class EditQueryModal extends Modal { 6 | view: Component; 7 | query: Query; 8 | 9 | constructor(app: App, query: Query, handleChange: (query: Query) => void) { 10 | super(app); 11 | this.query = query; 12 | this.view = new Component({ 13 | target: this.contentEl, 14 | props: { 15 | query, 16 | handleChange, 17 | }, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/gui/queryItem/QueryList.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | {#each queries as query} 10 | 18 | {:else} 19 |

Create a query

20 | {/each} 21 |
22 | 23 | 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /src/gui/modals/OpenRandomNoteModal/openRandomNoteModal.ts: -------------------------------------------------------------------------------- 1 | import { App, Modal } from "obsidian"; 2 | import Component from "./OpenRandomNoteModal.svelte"; 3 | import { Query } from "src/types"; 4 | 5 | export class RandomNoteModal extends Modal { 6 | view: Component; 7 | queries: Query[]; 8 | submitCallback: ((query: Query) => Promise) | undefined = undefined; 9 | 10 | constructor( 11 | app: App, 12 | queries: Query[], 13 | submitCallback: (query: Query) => Promise 14 | ) { 15 | super(app); 16 | this.queries = queries; 17 | this.view = new Component({ 18 | target: this.contentEl, 19 | props: { queries, handleSubmit: this.handleSubmit }, 20 | }); 21 | this.submitCallback = submitCallback; 22 | } 23 | 24 | handleSubmit = (query: Query): void => { 25 | if (this.submitCallback) { 26 | this.submitCallback(query); 27 | } 28 | this.close(); 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { TFile } from "obsidian"; 2 | 3 | export const QUERY_TYPES = ["Default", "Dataview", "Regex"] as const; 4 | export type QueryType = (typeof QUERY_TYPES)[number]; 5 | 6 | export const OPEN_TYPES = ["Active Leaf", "New Leaf", "New Window"] as const; 7 | export type OpenType = (typeof OPEN_TYPES)[number]; 8 | 9 | export const QUERY_OPEN_TYPES = ["Default", "Active Leaf", "New Leaf", "New Window"] as const; 10 | export type QueryOpenType = (typeof QUERY_OPEN_TYPES)[number]; 11 | 12 | export interface Query { 13 | id: string; 14 | name: string; 15 | query: string; 16 | type: QueryType; 17 | createCommand: boolean; 18 | useDisabledFolders: boolean; 19 | openType: QueryOpenType; 20 | } 21 | 22 | export interface ProcessedDefaultQuery { 23 | path: string; 24 | file: string; 25 | tags: SearchTag[]; 26 | } 27 | 28 | export interface SearchTag { 29 | included: boolean; 30 | tag: string; 31 | } 32 | 33 | export type RandomNoteResult = TFile[]; 34 | -------------------------------------------------------------------------------- /src/gui/modals/OpenRandomNoteModal/OpenRandomNoteModal.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |

Select Random Note Query

14 | 15 | {#if queries.length > 0} 16 |
17 | 22 | 23 |
24 | {:else} 25 |

26 | Go to settings tab to make search queries. 27 |

28 | {/if} 29 | 30 | 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advanced-random-note", 3 | "version": "0.0.11", 4 | "description": "Create commands from custom queries to open random notes", 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 && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "Karsten F. Pedersen", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@fortawesome/free-solid-svg-icons": "^6.4.2", 16 | "@tsconfig/svelte": "^5.0.0", 17 | "@types/node": "^16.11.6", 18 | "@typescript-eslint/eslint-plugin": "5.29.0", 19 | "@typescript-eslint/parser": "5.29.0", 20 | "builtin-modules": "3.3.0", 21 | "esbuild": "0.17.3", 22 | "esbuild-svelte": "^0.7.4", 23 | "obsidian": "latest", 24 | "obsidian-dataview": "^0.5.56", 25 | "svelte": "^4.0.5", 26 | "svelte-awesome": "^3.2.1", 27 | "svelte-preprocess": "^5.0.4", 28 | "tslib": "2.4.0", 29 | "typescript": "4.7.4" 30 | }, 31 | "dependencies": { 32 | "uuid": "^9.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Karsten Finderup Pedersen 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 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import builtins from "builtin-modules"; 2 | import esbuild from "esbuild"; 3 | import esbuildSvelte from "esbuild-svelte"; 4 | import process from "process"; 5 | import sveltePreprocess from "svelte-preprocess"; 6 | 7 | const banner = `/* 8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 9 | if you want to view the source, please visit the github repository of this plugin 10 | */ 11 | `; 12 | 13 | const prod = process.argv[2] === "production"; 14 | 15 | const context = await esbuild.context({ 16 | banner: { 17 | js: banner, 18 | }, 19 | entryPoints: ["src/main.ts"], 20 | bundle: 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 | plugins: [ 38 | esbuildSvelte({ 39 | compilerOptions: { css: true }, 40 | preprocess: sveltePreprocess(), 41 | }), 42 | ], 43 | format: "cjs", 44 | target: "es2018", 45 | logLevel: "info", 46 | sourcemap: prod ? false : "inline", 47 | treeShaking: true, 48 | outfile: "main.js", 49 | }); 50 | 51 | if (prod) { 52 | await context.rebuild(); 53 | process.exit(0); 54 | } else { 55 | await context.watch(); 56 | } 57 | -------------------------------------------------------------------------------- /src/gui/queryItem/QueryItem.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | 34 | 37 | 38 | 39 |
40 | 47 | 48 | 51 | 52 | 55 | 56 | 59 |
60 |
61 | 62 | 103 | -------------------------------------------------------------------------------- /src/gui/queryItem/QueryView.svelte: -------------------------------------------------------------------------------- 1 | 71 | 72 |
73 | 74 | 82 | 83 | 84 |
85 | 86 | 87 |
88 |
89 | 90 | 103 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { App, PluginSettingTab, Setting } from "obsidian"; 2 | import QueryView from "src/gui/queryItem/QueryView.svelte"; 3 | import AdvancedRandomNote from "./main"; 4 | import { type OpenType, type Query } from "./types"; 5 | import { getOpenTypeLabels, toRecord } from "./utilities"; 6 | 7 | export interface Settings { 8 | queries: Array; 9 | disabledFolders: string; 10 | debug: boolean; 11 | openType: OpenType; 12 | setActive: boolean; 13 | } 14 | 15 | export const DEFAULT_SETTINGS: Settings = { 16 | queries: [], 17 | disabledFolders: "", 18 | debug: false, 19 | openType: "Active Leaf", 20 | setActive: true, 21 | }; 22 | 23 | export class SettingTab extends PluginSettingTab { 24 | plugin: AdvancedRandomNote; 25 | 26 | constructor(app: App, plugin: AdvancedRandomNote) { 27 | super(app, plugin); 28 | this.plugin = plugin; 29 | } 30 | 31 | display(): void { 32 | this.containerEl.empty(); 33 | this.addSetting(); 34 | this.addQueriesSetting(); 35 | } 36 | 37 | addSetting() { 38 | // Make files active 39 | new Setting(this.containerEl) 40 | .setName("Open files as active") 41 | .setDesc("Make files active when they are opened.") 42 | .addToggle((toggle) => { 43 | toggle 44 | .setValue(this.plugin.settings.setActive) 45 | .onChange(async (value) => { 46 | this.plugin.settings.setActive = value; 47 | await this.plugin.saveSettings(); 48 | }); 49 | }); 50 | 51 | // Open type 52 | new Setting(this.containerEl) 53 | .setName("Open in") 54 | .setDesc("Where to open files.") 55 | .addDropdown((dropdown) => 56 | dropdown 57 | .addOptions(toRecord(getOpenTypeLabels())) 58 | .setValue(this.plugin.settings.openType) 59 | .onChange(async (value) => { 60 | this.plugin.settings.openType = value as OpenType; 61 | await this.plugin.saveSettings(); 62 | }) 63 | ); 64 | 65 | // Disabled folders setting 66 | new Setting(this.containerEl) 67 | .setName("Disabled folders") 68 | .setDesc("Skips these folders when searching for files.") 69 | .addTextArea((text) => { 70 | text.setPlaceholder("templates/") 71 | .setValue(this.plugin.settings.disabledFolders) 72 | .onChange(async (value) => { 73 | this.plugin.settings.disabledFolders = value.trim(); 74 | await this.plugin.saveSettings(); 75 | }); 76 | }); 77 | } 78 | 79 | addQueriesSetting() { 80 | // Title 81 | this.containerEl.createEl("div", { 82 | text: "Queries", 83 | cls: "setting-item setting-item-heading", 84 | }); 85 | 86 | // Add query list 87 | const setting = new Setting(this.containerEl); 88 | setting.infoEl.remove(); 89 | setting.settingEl.style.display = "block"; 90 | new QueryView({ 91 | target: setting.settingEl, 92 | props: { 93 | plugin: this.plugin, 94 | queries: this.plugin.settings.queries, 95 | saveQueries: async (queries: Query[]) => { 96 | this.plugin.settings.queries = queries; 97 | await this.plugin.saveSettings(); 98 | }, 99 | }, 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | TAbstractFile, 4 | TFile, 5 | TFolder, 6 | type PluginManifest, 7 | } from "obsidian"; 8 | import { v4 as uuidv4 } from "uuid"; 9 | import AdvancedRandomNote from "./main"; 10 | import { OPEN_TYPES, type Query } from "./types"; 11 | 12 | export function moveElementInArray( 13 | arr: T[], 14 | fromIndex: number, 15 | toIndex: number 16 | ): void { 17 | if (toIndex < 0 || toIndex === arr.length) { 18 | return; 19 | } 20 | const element = arr[fromIndex]; 21 | arr[fromIndex] = arr[toIndex]; 22 | arr[toIndex] = element; 23 | } 24 | 25 | export function getRandomElement(arr: T[]): T | null { 26 | if (arr.length === 0) return null; 27 | 28 | const index = Math.floor(Math.random() * arr.length); 29 | return arr[index]; 30 | } 31 | 32 | export function addOrRemoveQueryCommand( 33 | plugin: AdvancedRandomNote, 34 | query: Query 35 | ) { 36 | if (query.createCommand) { 37 | plugin.addQueryCommand(query); 38 | } else { 39 | plugin.removeQueryCommand(query); 40 | } 41 | } 42 | 43 | export function findObsidianCommand(app: App, commandId: string) { 44 | // @ts-ignore 45 | return app.commands.findCommand(commandId); 46 | } 47 | 48 | export function deleteObsidianCommand(app: App, commandId: string) { 49 | if (findObsidianCommand(app, commandId)) { 50 | // @ts-ignore 51 | delete app.commands.commands[commandId]; 52 | // @ts-ignore 53 | delete app.commands.editorCommands[commandId]; 54 | } 55 | } 56 | 57 | export function getPluginCommandId( 58 | commandId: string, 59 | manifest: PluginManifest 60 | ): string { 61 | return manifest.id + ":" + commandId; 62 | } 63 | 64 | export function getTagString(tag: string): string { 65 | if (tag.startsWith("!#")) { 66 | return tag.replace("!", ""); 67 | } else if (tag.startsWith("!")) { 68 | return tag.replace("!", "#"); 69 | } else if (tag.startsWith("#")) { 70 | return tag; 71 | } else { 72 | return "#" + tag; 73 | } 74 | } 75 | 76 | export function getTagStrings(tags: string[]): string[] { 77 | return tags.map((tag) => getTagString(tag)); 78 | } 79 | 80 | export function getQueryCommandId() { 81 | return "query:" + uuidv4(); 82 | } 83 | 84 | export function createQuery(name: string, query: string): Query { 85 | return { 86 | id: getQueryCommandId(), 87 | name, 88 | query, 89 | type: "Default", 90 | createCommand: false, 91 | useDisabledFolders: true, 92 | openType: "Default" 93 | }; 94 | } 95 | 96 | export function getFullPath(file: TFile): string { 97 | return file.path + "/" + file.name + "." + file.path; 98 | } 99 | 100 | export function flattenFiles(abstractFiles: TAbstractFile[]): TFile[] { 101 | return abstractFiles.map(abstractFile => flattenFile(abstractFile)).flat(); 102 | } 103 | 104 | export function flattenFile(abstractFile: TAbstractFile): TFile[] { 105 | if (abstractFile instanceof TFolder) { 106 | return flattenFiles(abstractFile.children); 107 | } 108 | 109 | return [abstractFile] as TFile[]; 110 | } 111 | 112 | export function toRecord(arr: string[]): Record { 113 | const recordObject: Record = {}; 114 | arr.forEach(item => recordObject[item] = item) 115 | return recordObject; 116 | } 117 | 118 | export function getOpenTypeLabels(): string[] { 119 | return OPEN_TYPES.map((item) => item); 120 | } -------------------------------------------------------------------------------- /src/gui/modals/EditQueryModal/EditQueryModal.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 |

41 | 48 |

49 | 50 |
51 | 52 |
53 |
54 |
Create command
55 |
56 | Make the query into an executable command. 57 |
58 |
59 |
60 |
{ 64 | query.createCommand = !query.createCommand; 65 | emitHandleChange(); 66 | }} 67 | > 68 | 73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 |
Use disabled folders
81 |
82 | Use disabled folders from settings. 83 |
84 |
85 |
86 |
{ 90 | query.useDisabledFolders = !query.useDisabledFolders; 91 | emitHandleChange(); 92 | }} 93 | > 94 | 99 |
100 |
101 |
102 | 103 | 104 |
105 |
106 |
Open in
107 |
Where to open the file.
108 |
109 |
110 | 119 |
120 |
121 | 122 | 123 |
124 |
125 |
Query type
126 |
127 | Use Regex, Dataview, or the default. 128 |
129 |
130 |
131 | 140 |
141 |
142 | 143 |
144 |
145 |
Query
146 |
147 | {queryDescription} 148 |
149 |
150 |