diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab508b9..ba9867b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,6 +42,8 @@ jobs: gh release create "$tag" \ --title="$tag" \ --draft \ + dist/styles.css \ dist/main-debug.js \ dist/main.js \ + dist/manifest.json \ ${{ env.PLUGIN_NAME }}.zip diff --git a/README.md b/README.md index ee7bd79..138da09 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The Amazing Marvin Plugin provides a way to bring your tasks and project structu - **Parent Links**: For easy navigation, notes for subcategories and subprojects include backlinks to their parent category or project. - **Wiki Links**: Sub-Categories and projects Amazing Marvin are added as wiki links. - **Categories and Projects are folder notes**: Categories and projects are created as folder notes, compatible with [Obsidian folder notes](https://github.com/LostPaul/obsidian-folder-notes). +- **Task Creation**: Users can create Amazing Marvin tasks directly within Obsidian, with support for standard Marvin shorthand notations like `+` for dates or `@` for labels. +- **Deep Linking**: Each task and category is equipped with a deep link, providing quick navigation back to Amazing Marvin. ## Usage Instructions @@ -35,6 +37,36 @@ To initiate a sync: 2. Search for and select the command `Sync Amazing Marvin categories and projects`. 3. The plugin will then proceed to update your Obsidian vault with the current structure and content from Amazing Marvin. +Once synced, your Obsidian vault will contain a new `AmazingMarvin` folder. Inside, you'll find the structured notes corresponding to your categories and projects from Amazing Marvin. + +### Creating a Marvin Task + +The task creation dialog is designed to mirror the task input experience in Amazing Marvin closely. It includes the following features: + +- Autocomplete for Categories and Projects using `#` syntax or a search sub-dialog. +- Recognizes shorthand notations for properties like start date (`~`), due date (`@`), and labels (`+`). +- Places a link to the Marvin task as a deep link in Obsidian at the cursor location upon task creation. +- The created Marvin task contains an Advanced URI-friendly link back to the Obsidian note that instigated the task. + +To create a task: + +1. Open Obsidian's Command Palette with `Ctrl/Cmd + P`. +2. Search for and select the command `Create Marvin Task`. +3. Input the task details and select the appropriate category from the dropdown, which shows suggestions as you type. +4. Upon task creation, a markdown checklist item with a link to the Marvin task is inserted at your cursor location in Obsidian. + +### Auto-Mark as Done Feature + +One of the highlights in this version is the ability to auto-mark tasks as done in Amazing Marvin when they are checked off in Obsidian. When this feature is enabled in the plugin settings, checking a task off in your Obsidian note will automatically update the task status in Amazing Marvin. + +Here's how to enable this feature: + +1. Go to `Settings > Obsidian Amazing Marvin Plugin`. +2. Check the option `Attempt to mark tasks as done in Amazing Marvin when checked off in Obsidian`. +3. Save your settings. + +Now, when you check off a task with an Amazing Marvin Link in an Obsidian note, a request will be sent to Amazing Marvin to mark the task as done there as well. + ### Important Considerations - **Data Loss**: Be cautious when editing Amazing Marvin-generated notes in Obsidian, as these changes will be overwritten by the next sync. @@ -42,9 +74,6 @@ To initiate a sync: By following these guidelines, you can ensure your Amazing Marvin data is accurately reflected in Obsidian while being mindful of the plugin's current limitations. -### Viewing Synced Content - -Once synced, your Obsidian vault will contain a new `AmazingMarvin` folder. Inside, you'll find the structured notes corresponding to your categories and projects from Amazing Marvin. ## Installing @@ -53,7 +82,7 @@ Once synced, your Obsidian vault will contain a new `AmazingMarvin` folder. Insi 1. Install the BRAT plugin 1. Open `Settings` -> `Community Plugins` 2. Disable safe mode, if enabled - 3. *Browse*, and search for "BRAT" + 3. *Browse*, and search for "BRAT" 4. Install the latest version of **Obsidian42 - BRAT** 2. Open BRAT settings (`Settings` -> `BRAT`) 1. Scroll to the `Beta Plugin List` section @@ -64,7 +93,7 @@ Once synced, your Obsidian vault will contain a new `AmazingMarvin` folder. Insi ### Manually 1. If you haven't enabled community plugins in Obsidian, follow these [instructions](https://help.obsidian.md/Extending+Obsidian/Community+plugins#Install+a+community+plugin) to do so. -2. Download the latest release from the [releases](https://github.com/cloud-atlas-ai/obsidian-am/releases) page. +2. Download `cloudatlas-obsidian-am.zip` from the [releases](https://github.com/cloud-atlas-ai/obsidian-am/releases). 3. Unzip the release and copy the directory into your vault's plugins folder: `/.obsidian/plugins/cloudatlas-o-am`. 4. Restart Obsidian to recognize the new plugin. 5. In Obsidian's settings under "Community Plugins," find and enable the Obsidian Amazing Marvin Plugin. diff --git a/manifest-beta.json b/manifest-beta.json index 82ac981..5b14bd6 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "cloudatlas-o-am", "name": "Amazing Marvin", - "version": "0.4.2b", + "version": "0.9.0", "minAppVersion": "1.4.16", "description": "Integration with Amazing Marvin", "author": "Cloud Atlas", diff --git a/manifest.json b/manifest.json index 946036c..dc92288 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "cloudatlas-o-am", - "name": "Amazing Marvin", - "version": "0.3.1", + "name": "Amazing Marvin by Cloud Atlas", + "version": "0.9.2", "minAppVersion": "1.4.16", "description": "Integration with Amazing Marvin", "author": "Cloud Atlas", diff --git a/package.json b/package.json index bf0e152..e8b1b7a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloud-atlas", - "version": "0.4.2b", + "version": "0.9.2", "description": "Interoperability between Obsidian and Amazing Marvin", "main": "dist/main.js", "scripts": { diff --git a/src/addTaskModal.ts b/src/addTaskModal.ts new file mode 100644 index 0000000..411e669 --- /dev/null +++ b/src/addTaskModal.ts @@ -0,0 +1,181 @@ +import { App, Modal, Setting, DropdownComponent, TextAreaComponent, FuzzySuggestModal, FuzzyMatch, Notice } from "obsidian"; +import { Category } from "./interfaces"; + +const inboxCategory: Category = { + _id: "__inbox-faux__", // Arbitrary unique ID for the Inbox faux category + title: "Inbox", + type: "faux", + updatedAt: 0, + parentId: "root", + startDate: "", + endDate: "", + note: "", + isRecurring: false, + priority: "", + deepLink: "", + dueDate: "", + done: false, +}; + +function getTitleWithParent(category: Category, categories: Category[]): string { + let parent = category.parentId; + + let parentTitle = []; + while (parent && parent !== "root") { + const parentCategory = categories.find(category => category._id === parent); + if (parentCategory) { + parentTitle.push(parentCategory.title); + parent = parentCategory.parentId; + } else { + break; + } + } + if (parentTitle.length > 0) { + return category.title + ` in ${parentTitle.reverse().join("/")}`; + } + return category.title; +} + +// Suggester Modal Class for Category Selection +class CategorySuggesterModal extends FuzzySuggestModal { + categories: Category[]; + + onChooseItem: (item: Category, _evt: MouseEvent | KeyboardEvent) => void; + getItems(): Category[] { + + // Include the Inbox at the beginning of the categories list + return [inboxCategory, ...this.categories]; + } + getItemText(category: Category): string { + if (category.type === "faux") { + return "Inbox"; + } + return getTitleWithParent(category, this.categories); + } + + constructor(app: App, categories: Category[], onChooseItem: (item: Category, _evt: MouseEvent | KeyboardEvent) => void) { + super(app); + this.categories = categories; + this.onChooseItem = onChooseItem; + } + + onChooseSuggestion(item: FuzzyMatch, _evt: MouseEvent | KeyboardEvent): void { + this.onChooseItem(item.item, _evt); + } +} + +export class AddTaskModal extends Modal { + result: { catId: string, task: string }; + onSubmit: (result: { catId: string, task: string }) => void; + categories: Category[]; + + constructor(app: App, categories: Category[], onSubmit: (result: { catId: string; task: string; }) => void) { + super(app); + this.onSubmit = onSubmit; + this.categories = categories.sort((a, b) => { + return this.getFullPathToCategoryTitle(a, categories).localeCompare(this.getFullPathToCategoryTitle(b, categories)); + }); + this.result = { catId: inboxCategory._id, task: '' }; + } + + onOpen() { + const { contentEl } = this; + let categoryInput: HTMLInputElement; + + contentEl.createEl("h1", { text: "New Amazing Marvin Task" }); + + new Setting(contentEl) + .setName("Category") + .addText(text => { + categoryInput = text.inputEl; + text.setValue(inboxCategory.title); + this.result.catId = inboxCategory._id; + text.onChange(value => { + const suggester = new CategorySuggesterModal(this.app, this.categories, (item: Category) => { + categoryInput.value = item.title; + this.result.catId = item._id; + suggester.close(); + }); + suggester.open(); + }); + }); + + new Setting(contentEl) + .setHeading().setName("Task"); + + new Setting(contentEl) + .addTextArea((textArea: TextAreaComponent) => { + textArea.onChange((value: string) => { + this.result.task = value; + }); + }).settingEl.addClass("am-task-textarea-setting"); + + const shortcutsDesc = document.createDocumentFragment(); + shortcutsDesc.appendText('The Task field accepts labels (@), time estimates (~), and scheduled dates (+). See '); + shortcutsDesc.appendChild(this.getShortcutsLink()); + shortcutsDesc.appendText('.'); + + new Setting(contentEl) + .setDesc(shortcutsDesc); + + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText("Add") + .setCta() + .onClick(() => { + this.addTask(); + })); + + this.modalEl.addEventListener("keydown", (e: KeyboardEvent) => { + if (e.key === "Enter" && e.ctrlKey) { + e.preventDefault(); + this.addTask(); + } + }); + + } + + private addTask() { + if (!this.result.task.trim()) { + new Notice('Please enter a task description.', 4000); + return; + } + this.close(); + if (this.onSubmit && this.result.task) { + this.onSubmit(this.result); + } + } + + + + private getShortcutsLink(): HTMLAnchorElement { + const a = document.createElement('a'); + a.href = 'https://help.amazingmarvin.com/en/articles/1949399-using-shortcuts-while-creating-a-task'; + a.text = 'Using shortcuts while creating a task'; + a.target = '_blank'; + return a; + } + + private getFullPathToCategoryTitle(category: Category, categories: Category[]): string { + let parent = category.parentId; + + let parentTitle = []; + parentTitle.push('/'); + while (parent && parent !== "root") { + const parentCategory = categories.find(c => c._id === parent); + if (parentCategory) { + parentTitle.push(parentCategory.title); + parent = parentCategory.parentId; + } else { + break; + } + } + return `${parentTitle.reverse().join("/")}${category.title}`; + } + + onClose() { + let { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/amTaskWatcher.ts b/src/amTaskWatcher.ts index c8380df..7320ebf 100644 --- a/src/amTaskWatcher.ts +++ b/src/amTaskWatcher.ts @@ -19,14 +19,11 @@ export function amTaskWatcher(_app: App, plugin: AmazingMarvinPlugin) { return; } update.changes.iterChanges((fromA, _toA, _fromB, _toB, change) => { - //only match if the change is a single character and it's an X or x - if (change.length === 1 && (change.sliceString(0, 1) === "X" || change.sliceString(0, 1) === "x")) { - let line = update.state.doc.lineAt(fromA).text; - - const match = line.match(COMPLETED_AM_TASK); - if (match && match[1]) { - plugin.markDone(match[1]); - } + //only match if the change is on an AM task and it's a completed task + let line = update.state.doc.lineAt(fromA).text; + const match = line.match(COMPLETED_AM_TASK); + if (match && match[1]) { + plugin.markDone(match[1]); } }); } diff --git a/src/main.ts b/src/main.ts index 02fcde5..2bce777 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,7 +6,6 @@ import { stringifyYaml, } from "obsidian"; - import { Category, Task @@ -22,8 +21,12 @@ import { getDateFromFile } from "obsidian-daily-notes-interface"; import { amTaskWatcher } from "./amTaskWatcher"; +import { AddTaskModal } from "./addTaskModal"; +import { time } from "console"; -let noticeTimeout: NodeJS.Timeout; +function getAMTimezoneOffset() { + return new Date().getTimezoneOffset() * -1; +} const animateNotice = (notice: Notice) => { let message = notice.noticeEl.innerText; @@ -38,7 +41,7 @@ const animateNotice = (notice: Notice) => { message = message.replace(" ...", " "); } notice.setMessage(message); - noticeTimeout = setTimeout(() => animateNotice(notice), 500); + setTimeout(() => animateNotice(notice), 500); }; const CONSTANTS = { @@ -46,9 +49,11 @@ const CONSTANTS = { categoriesEndpoint: '/api/categories', childrenEndpoint: '/api/children', scheduledOnDayEndpoint: '/api/todayItems', - dueOnDayEndpoint: '/api/dueItems' + dueOnDayEndpoint: '/api/dueItems', + addTaskEndpoint: '/api/addTask', } + export default class AmazingMarvinPlugin extends Plugin { settings: AmazingMarvinPluginSettings; @@ -77,6 +82,46 @@ export default class AmazingMarvinPlugin extends Plugin { this.registerEditorExtension(amTaskWatcher(this.app, this)); } + this.addCommand({ + id: "create-marvin-task", + name: "Create Marvin Task", + editorCallback: async (editor, view) => { + // Fetch categories first and make sure they are loaded + try { + + //if a region of text is selected, at least 3 characters long, use that to add a new task and skip the modal + if (editor.somethingSelected() && editor.getSelection().length > 2) { + this.addMarvinTask('', editor.getSelection(), view.file?.path, this.app.vault.getName()).then(task => { + editor.replaceSelection(`- [${task.done ? 'x' : ' '}] [⚓](${task.deepLink}) ${this.formatTaskDetails(task as Task, '')} ${task.title}`); + }).catch(error => { + new Notice('Could not create Marvin task: ' + error.message); + }); + return; + } + + const categories = await this.fetchTasksAndCategories(CONSTANTS.categoriesEndpoint); + // Ensure categories are fetched before initializing the modal + if (categories.length > 0) { + new AddTaskModal(this.app, categories, async (taskDetails: { catId: string, task: string }) => { + this.addMarvinTask(taskDetails.catId, taskDetails.task, view.file?.path, this.app.vault.getName()) + .then(task => { + editor.replaceRange(`- [${task.done ? 'x' : ' '}] [⚓](${task.deepLink}) ${this.formatTaskDetails(task as Task, '')} ${task.title}`, editor.getCursor()); + }) + .catch(error => { + new Notice('Could not create Marvin task: ' + error.message); + }); + }).open(); + } else { + // Handle the case where categories could not be loaded + new Notice('Failed to load categories from Amazing Marvin.'); + } + } catch (error) { + console.error('Error fetching categories:', error); + new Notice('Failed to load categories from Amazing Marvin.'); + } + } + }); + this.addCommand({ id: 'am-import', name: 'Import Categories and Tasks', @@ -118,6 +163,68 @@ export default class AmazingMarvinPlugin extends Plugin { }); } + async addMarvinTask(catId: string, taskTitle: string, notePath: string = '', vaultName: string = ''): Promise { + const opt = this.settings; + + let requestBody: any = { + title: taskTitle, + timeZoneOffset: getAMTimezoneOffset(), + }; + + if (catId && catId !== '' && catId !== 'root' && catId !== '__inbox-faux__') { + requestBody.parentId = catId; + } + + if (notePath && notePath !== '') { + let link = `obsidian://open?file=${encodeURI(notePath)}${vaultName !== '' ? `&vault=${encodeURI(vaultName)}` : ''}`; + if (this.settings.linkBackToObsidianText !== '') { + requestBody.note = `[${this.settings.linkBackToObsidianText}](${link})`; + } else { + requestBody.note = link; + } + } + + try { + const remoteResponse = await requestUrl({ + url: `https://serv.amazingmarvin.com/api/addTask`, + method: 'POST', + headers: { + 'X-API-Token': opt.apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(requestBody) + }); + + if (remoteResponse.status === 200) { + new Notice("Task added in Amazing Marvin."); + return this.decorateWithDeepLink(remoteResponse.json) as Task; + } else if (remoteResponse.status === 429) { + + const errorNote = document.createDocumentFragment(); + errorNote.appendText('Your request was throttled by Amazing Marvin. Wait a few minutes and try again. Or do it '); + const a = document.createElement('a'); + a.href = 'https://app.amazingmarvin.com/'; + a.text = 'manually'; + a.target = '_blank'; + errorNote.appendChild(a); + errorNote.appendText('.'); + new Notice(errorNote,); + } + } catch (error) { + const errorNote = document.createDocumentFragment(); + errorNote.appendText('Error creating task in Amazing Marvin. You can try again or do it '); + console.error('Error creating task:', error); + const a = document.createElement('a'); + a.href = 'https://app.amazingmarvin.com/'; + a.text = 'manually'; + a.target = '_blank'; + errorNote.appendChild(a); + errorNote.appendText('.'); + + new Notice(errorNote, 0); + } + return Promise.reject(new Error('Error creating task')); + } onunload() { } @@ -136,7 +243,7 @@ export default class AmazingMarvinPlugin extends Plugin { const opt = this.settings; const requestBody = { itemId: taskId, - timeZoneOffset: new Date().getTimezoneOffset() + timeZoneOffset: getAMTimezoneOffset() }; try { @@ -150,13 +257,31 @@ export default class AmazingMarvinPlugin extends Plugin { body: JSON.stringify(requestBody) }); + const note = document.createDocumentFragment(); + const a = document.createElement('a'); + a.href = 'https://app.amazingmarvin.com/#t=' + taskId; + + a.target = '_blank'; + if (remoteResponse.status === 200) { - new Notice("Task marked as done in Amazing Marvin."); + a.text = 'Task'; + note.append(a); + note.appendText(' marked as done in Amazing Marvin.'); + new Notice(note, 5000); return remoteResponse.json; + } else if (remoteResponse.status === 429) { + a.text = 'manually'; + note.appendText('Your request was throttled by Amazing Marvin. Do it manually at '); + console.error('Your request was throttled by Amazing Marvin. Wait a few minutes and try again. Or do it manually.'); + note.appendChild(a); + + new Notice(note, 0); } } catch (error) { const errorNote = document.createDocumentFragment(); errorNote.appendText('Error marking task as done in Amazing Marvin. You should do it '); + console.error('Error marking task as done:', error); + const a = document.createElement('a'); a.href = 'https://app.amazingmarvin.com/#t=' + taskId; a.text = 'manually'; @@ -164,7 +289,6 @@ export default class AmazingMarvinPlugin extends Plugin { errorNote.appendChild(a); new Notice(errorNote, 0); - console.error('Error marking task as done:', error); } } @@ -188,7 +312,7 @@ export default class AmazingMarvinPlugin extends Plugin { errorMessage = `[${response.status}] ${await response.text}`; } catch (err) { errorMessage = err.message; - console.error('Error fetching data from local server:', err); + console.debug('Failed while fetching from local server, will try the public server next:', err); } if (!opt.useLocalServer || errorMessage) { @@ -203,7 +327,11 @@ export default class AmazingMarvinPlugin extends Plugin { errorMessage = `[${response.status}] ${await response.text()}`; } catch (err) { - errorMessage = err.message; + if (response?.status === 429) { + errorMessage = 'Your request was throttled by Amazing Marvin.'; + } else { + errorMessage = err.message; + } } } diff --git a/src/regexp-cursor.ts b/src/regexp-cursor.ts deleted file mode 100644 index c9bec08..0000000 --- a/src/regexp-cursor.ts +++ /dev/null @@ -1,195 +0,0 @@ -// from https://github.com/codemirror/search/blob/main/src/regexp.ts - -import {Text, TextIterator} from "@codemirror/state" - -const empty = {from: -1, to: -1, match: /.*/.exec("")!} - -const baseFlags = "gm" + (/x/.unicode == null ? "" : "u") - -export interface RegExpCursorOptions { - ignoreCase?: boolean - test?: (from: number, to: number, match: RegExpExecArray) => boolean -} - -/// This class is similar to [`SearchCursor`](#search.SearchCursor) -/// but searches for a regular expression pattern instead of a plain -/// string. -export class RegExpCursor implements Iterator<{from: number, to: number, match: RegExpExecArray}> { - private iter!: TextIterator - private re!: RegExp - private test?: (from: number, to: number, match: RegExpExecArray) => boolean - private curLine = "" - private curLineStart!: number - private matchPos!: number - - /// Set to `true` when the cursor has reached the end of the search - /// range. - done = false - - /// Will contain an object with the extent of the match and the - /// match object when [`next`](#search.RegExpCursor.next) - /// sucessfully finds a match. - value = empty - - /// Create a cursor that will search the given range in the given - /// document. `query` should be the raw pattern (as you'd pass it to - /// `new RegExp`). - constructor(private text: Text, query: string, options?: RegExpCursorOptions, - from: number = 0, private to: number = text.length) { - if (/\\[sWDnr]|\n|\r|\[\^/.test(query)) return new MultilineRegExpCursor(text, query, options, from, to) as any - this.re = new RegExp(query, baseFlags + (options?.ignoreCase ? "i" : "")) - this.test = options?.test - this.iter = text.iter() - let startLine = text.lineAt(from) - this.curLineStart = startLine.from - this.matchPos = toCharEnd(text, from) - this.getLine(this.curLineStart) - } - - private getLine(skip: number) { - this.iter.next(skip) - if (this.iter.lineBreak) { - this.curLine = "" - } else { - this.curLine = this.iter.value - if (this.curLineStart + this.curLine.length > this.to) - this.curLine = this.curLine.slice(0, this.to - this.curLineStart) - this.iter.next() - } - } - - private nextLine() { - this.curLineStart = this.curLineStart + this.curLine.length + 1 - if (this.curLineStart > this.to) this.curLine = "" - else this.getLine(0) - } - - /// Move to the next match, if there is one. - next() { - for (let off = this.matchPos - this.curLineStart;;) { - this.re.lastIndex = off - let match = this.matchPos <= this.to && this.re.exec(this.curLine) - if (match) { - let from = this.curLineStart + match.index, to = from + match[0].length - this.matchPos = toCharEnd(this.text, to + (from == to ? 1 : 0)) - if (from == this.curLineStart + this.curLine.length) this.nextLine() - if ((from < to || from > this.value.to) && (!this.test || this.test(from, to, match))) { - this.value = {from, to, match} - return this - } - off = this.matchPos - this.curLineStart - } else if (this.curLineStart + this.curLine.length < this.to) { - this.nextLine() - off = 0 - } else { - this.done = true - return this - } - } - } - - [Symbol.iterator]!: () => Iterator<{from: number, to: number, match: RegExpExecArray}> -} - -const flattened = new WeakMap() - -// Reusable (partially) flattened document strings -class FlattenedDoc { - constructor(readonly from: number, - readonly text: string) {} - get to() { return this.from + this.text.length } - - static get(doc: Text, from: number, to: number) { - let cached = flattened.get(doc) - if (!cached || cached.from >= to || cached.to <= from) { - let flat = new FlattenedDoc(from, doc.sliceString(from, to)) - flattened.set(doc, flat) - return flat - } - if (cached.from == from && cached.to == to) return cached - let {text, from: cachedFrom} = cached - if (cachedFrom > from) { - text = doc.sliceString(from, cachedFrom) + text - cachedFrom = from - } - if (cached.to < to) - text += doc.sliceString(cached.to, to) - flattened.set(doc, new FlattenedDoc(cachedFrom, text)) - return new FlattenedDoc(from, text.slice(from - cachedFrom, to - cachedFrom)) - } -} - -const enum Chunk { Base = 5000 } - -class MultilineRegExpCursor implements Iterator<{from: number, to: number, match: RegExpExecArray}> { - private flat: FlattenedDoc - private matchPos - private re: RegExp - private test?: (from: number, to: number, match: RegExpExecArray) => boolean - - done = false - value = empty - - constructor(private text: Text, query: string, options: RegExpCursorOptions | undefined, from: number, private to: number) { - this.matchPos = toCharEnd(text, from) - this.re = new RegExp(query, baseFlags + (options?.ignoreCase ? "i" : "")) - this.test = options?.test - this.flat = FlattenedDoc.get(text, from, this.chunkEnd(from + Chunk.Base)) - } - - private chunkEnd(pos: number) { - return pos >= this.to ? this.to : this.text.lineAt(pos).to - } - - next() { - for (;;) { - let off = this.re.lastIndex = this.matchPos - this.flat.from - let match = this.re.exec(this.flat.text) - // Skip empty matches directly after the last match - if (match && !match[0] && match.index == off) { - this.re.lastIndex = off + 1 - match = this.re.exec(this.flat.text) - } - if (match) { - let from = this.flat.from + match.index, to = from + match[0].length - // If a match goes almost to the end of a noncomplete chunk, try - // again, since it'll likely be able to match more - if ((this.flat.to >= this.to || match.index + match[0].length <= this.flat.text.length - 10) && - (!this.test || this.test(from, to, match))) { - this.value = {from, to, match} - this.matchPos = toCharEnd(this.text, to + (from == to ? 1 : 0)) - return this - } - } - if (this.flat.to == this.to) { - this.done = true - return this - } - // Grow the flattened doc - this.flat = FlattenedDoc.get(this.text, this.flat.from, this.chunkEnd(this.flat.from + this.flat.text.length * 2)) - } - } - - [Symbol.iterator]!: () => Iterator<{from: number, to: number, match: RegExpExecArray}> -} - -if (typeof Symbol != "undefined") { - RegExpCursor.prototype[Symbol.iterator] = MultilineRegExpCursor.prototype[Symbol.iterator] = - function(this: RegExpCursor) { return this } -} - -export function validRegExp(source: string) { - try { - new RegExp(source, baseFlags) - return true - } catch { - return false - } -} - -function toCharEnd(text: Text, pos: number) { - if (pos >= text.length) return pos - let line = text.lineAt(pos), next - while (pos < line.to && (next = line.text.charCodeAt(pos - line.from)) >= 0xDC00 && next < 0xE000) pos++ - return pos -} diff --git a/src/settings.ts b/src/settings.ts index f0552cb..a52bc39 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,6 +2,7 @@ import { App, Platform, PluginSettingTab, Setting } from "obsidian"; import AmazingMarvinPlugin from "./main"; export interface AmazingMarvinPluginSettings { + linkBackToObsidianText: string; attemptToMarkTasksAsDone: any; useLocalServer: boolean; localServerHost: string; @@ -14,6 +15,7 @@ export interface AmazingMarvinPluginSettings { } export const DEFAULT_SETTINGS: AmazingMarvinPluginSettings = { + linkBackToObsidianText: '', useLocalServer: false, localServerHost: "localhost", localServerPort: 12082, @@ -33,22 +35,16 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { this.plugin = plugin; } - private getAPILink(): HTMLAnchorElement { - const a = document.createElement('a'); - a.href = 'https://app.amazingmarvin.com/pre?api'; - a.text = 'API page'; - a.target = '_blank'; - return a; - } + // refactor a function for link creation that takes the href and text as parameters - private getLocalAPIDocs(): HTMLAnchorElement { - const a = document.createElement('a'); - a.href = 'https://help.amazingmarvin.com/en/articles/5165191-desktop-local-api-server'; - a.text = 'Desktop Local API Server'; - a.target = '_blank'; - return a; - } +private a(href: string, text: string) { + const a = document.createElement('a'); + a.href = href; + a.text = text; + a.target = '_blank'; + return a; +} display(): void { const { containerEl } = this; @@ -56,7 +52,7 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { const TokenDescEl = document.createDocumentFragment(); TokenDescEl.appendText('Get your Token at the '); - TokenDescEl.appendChild(this.getAPILink()); + TokenDescEl.appendChild(this.a('https://app.amazingmarvin.com/pre?api', 'API page')); new Setting(containerEl) .setName("API Token") @@ -73,7 +69,7 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { new Setting(containerEl) .setName("Mark tasks as done") - .setDesc("Attempt to mark tasks as done in Amazing Marvin") + .setDesc("Attempt to mark tasks as done in Amazing Marvin. Note that this only applies to Amazing Marvins tasks imported or created with this plugin.") .addToggle(toggle => toggle .setValue(this.plugin.settings.attemptToMarkTasksAsDone) .onChange(async (value) => { @@ -99,6 +95,29 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setHeading().setName("Task creation"); + + + const noteLink = document.createDocumentFragment(); + // make this text much shorter + noteLink.appendText('Text for note back to Obsidian on tasks created with this plugin. If empty, a link be added.'); + noteLink.append(document.createElement('br')); + + new Setting(containerEl) + .setName("Note link text") + .setDesc(noteLink) + .addText((text) => + text + .setPlaceholder("Note link text") + .setValue(this.plugin.settings.linkBackToObsidianText) + .onChange(async (value) => { + this.plugin.settings.linkBackToObsidianText = value.trim(); + await this.plugin.saveSettings(); + }) + ); + + new Setting(containerEl) .setHeading().setName("Task formatting"); @@ -135,7 +154,7 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { if (Platform.isDesktopApp) { const lsDescEl = document.createDocumentFragment(); lsDescEl.appendText('The local API can speed up the plugin. See the '); - lsDescEl.appendChild(this.getLocalAPIDocs()); + lsDescEl.appendChild(this.a('https://help.amazingmarvin.com/en/articles/5165191-desktop-local-api-server', 'Desktop Local API Server')); lsDescEl.appendText(' for more information.'); let ls = new Setting(containerEl) @@ -153,6 +172,10 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { .setPlaceholder("localhost") .setValue(this.plugin.settings.localServerHost || "localhost") .setDisabled(!this.plugin.settings.useLocalServer) + .onChange(async (value) => { + this.plugin.settings.localServerHost = value; + await this.plugin.saveSettings(); + }) ); // Local Server Port @@ -162,16 +185,19 @@ export class AmazingMarvinSettingsTab extends PluginSettingTab { .setPlaceholder("12082") .setValue(this.plugin.settings.localServerPort?.toString() || "12082") .setDisabled(!this.plugin.settings.useLocalServer) + .onChange(async (value) => { + this.plugin.settings.localServerPort = value; + await this.plugin.saveSettings(); + }) ); // Update the disabled state based on the toggle localServerToggle.addToggle(toggle => toggle.onChange(async (value) => { this.plugin.settings.useLocalServer = value; - await this.plugin.saveSettings(); - localServerHostSetting.setDisabled(!value); localServerPortSetting.setDisabled(!value); - })); + await this.plugin.saveSettings(); + }).setValue(this.plugin.settings.useLocalServer)); } } diff --git a/styles.css b/styles.css index 71cc60f..6640178 100644 --- a/styles.css +++ b/styles.css @@ -6,3 +6,8 @@ available in the app when your plugin is enabled. If your plugin does not need CSS, delete this file. */ + +.am-task-textarea-setting textarea{ + min-height: 5em; + width: 200%; +} diff --git a/versions.json b/versions.json index dbd8c28..febb80c 100644 --- a/versions.json +++ b/versions.json @@ -9,5 +9,18 @@ "0.3.2b2": "1.4.16", "0.4.0b": "1.4.16", "0.4.1b": "1.4.16", - "0.4.2b": "1.4.16" + "0.4.2b": "1.4.16", + "0.5.0b1": "1.4.16", + "0.5.0b2": "1.4.16", + "0.5.0b3": "1.4.16", + "0.5.0b4": "1.4.16", + "0.5.0": "1.4.16", + "0.5.1": "1.4.16", + "0.5.2": "1.4.16", + "0.5.3": "1.4.16", + "0.5.4b": "1.4.16", + "0.5.4": "1.4.16", + "0.6.0": "1.4.16", + "0.9.0": "1.4.16", + "0.9.2": "1.4.16" }