diff --git a/.gitignore b/.gitignore index dac3e746..997ceb7a 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ main.js # It's built from source files in styles/ directory during CI/build process. styles.css +# Don't include the auto-generated release notes import file +src/releaseNotes.ts + # Exclude sourcemaps *.map diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index a495849d..4ea999d9 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -25,6 +25,17 @@ Example: --> +## Added + +- Added release notes viewer + - New view displays release notes in a dedicated tab when plugin updates + - Automatically opens after updating to a new version + - Access anytime via command palette "TaskNotes: View release notes" or Settings > General + - Release notes are rendered using native Obsidian markdown rendering + - Includes link to view all releases on GitHub + - Fully internationalized with i18n support + - Auto-generation system ensures correct version is loaded without manual updates + ## Fixed - (#814) Fixed markdown links in projects field not being recognized on Project notes diff --git a/esbuild.config.mjs b/esbuild.config.mjs index a5de8b8a..efd5f576 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,6 +1,7 @@ import esbuild from "esbuild"; import process from "process"; import builtins from "builtin-modules"; +import { readFileSync } from "fs"; const banner = `/* @@ -11,6 +12,20 @@ if you want to view the source, please visit the github repository of this plugi const prod = (process.argv[2] === "production"); +// Plugin to import markdown files as strings +const markdownPlugin = { + name: 'markdown', + setup(build) { + build.onLoad({ filter: /\.md$/ }, async (args) => { + const text = readFileSync(args.path, 'utf8'); + return { + contents: `export default ${JSON.stringify(text)}`, + loader: 'js', + }; + }); + } +}; + const context = await esbuild.context({ banner: { js: banner, @@ -39,6 +54,7 @@ const context = await esbuild.context({ treeShaking: true, outfile: "main.js", minify: prod, + plugins: [markdownPlugin], }); if (prod) { diff --git a/generate-release-notes-import.mjs b/generate-release-notes-import.mjs new file mode 100644 index 00000000..becf944a --- /dev/null +++ b/generate-release-notes-import.mjs @@ -0,0 +1,20 @@ +import { readFileSync, writeFileSync } from "fs"; + +// Read current version from manifest.json +const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); +const version = manifest.version; + +// Generate the TypeScript file that imports the current version's release notes +const content = `// Auto-generated file - do not edit manually +// This file is regenerated during the build process to import the current version's release notes + +import releaseNotes from "../docs/releases/${version}.md"; + +export const CURRENT_VERSION = "${version}"; +export const CURRENT_RELEASE_NOTES = releaseNotes; +`; + +// Write to src/releaseNotes.ts +writeFileSync("src/releaseNotes.ts", content); + +console.log(`✓ Generated release notes import for version ${version}`); diff --git a/i18n.manifest.json b/i18n.manifest.json index e8dc7059..aeb9f5a7 100644 --- a/i18n.manifest.json +++ b/i18n.manifest.json @@ -127,6 +127,9 @@ "views.stats.sections.projectBreakdown": "c856b0f89a0d1f51f02f82726206cb07cd1ae95e", "views.stats.sections.dateRange": "6bb4b674b3232e32feb264e11f19ee1dbed354a3", "views.stats.filters.minTime": "77f4b7a081b20f78557dd404e14d91b89b52a27c", + "views.releaseNotes.title": "5939771f391859041bc57e5e05b04235b1ef8ee7", + "views.releaseNotes.header": "5939771f391859041bc57e5e05b04235b1ef8ee7", + "views.releaseNotes.viewAllLink": "b37e5d2fb2faa630817557713df6363f4bd0f5cc", "settings.tabs.general": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "settings.tabs.taskProperties": "770bd2ee4e0a455df6d1332def9c6c58f44994bc", "settings.tabs.defaults": "809ab386aa1c870ee286190b1af3eace7f0bfe30", @@ -845,6 +848,7 @@ "commands.pauseResumePomodoro": "1ac6f3fd60f918b25cfb95be70b9d43f3b3281bb", "commands.refreshCache": "0b56ae2b187bdf931d24258272f284d7046ad543", "commands.exportAllTasksIcs": "3f3714587b4802efeb6baf380a59615b8faf0c10", + "commands.viewReleaseNotes": "04c2072af97cf70c22fbf9efa9b62eb2574d23d6", "modals.task.titlePlaceholder": "c8196f1d1039c2fb290add52ee990c571173d6f1", "modals.task.titleLabel": "768e0c1c69573fb588f61f1308a015c11468e05f", "modals.task.titleDetailedPlaceholder": "da3700738242c039de32d3f5e3a0ba10bc28ba0b", diff --git a/i18n.state.json b/i18n.state.json index 5bc73118..38479742 100644 --- a/i18n.state.json +++ b/i18n.state.json @@ -512,6 +512,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "a6bce6cf45ab6987a0491268052e0770559f80a4" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "21a7b4ecae6d99d7c9ae0fe00f0cc0a33f1e2b6f" @@ -3372,6 +3375,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "2b3a3551e913a0618f09a1378c63fa6b832fe6cf" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "03f97eff6f24910100617f3fcbc4665e2e65cd7e" @@ -6042,6 +6046,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "01c1479a894b4e8a3b6e679bd3630c1389c7cd60" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "9239ee2cda84eca4c3440e2a7b50148af67da3d4" @@ -8902,6 +8909,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "8f93640a115b91031d8f53e89543cc0bf04e300b" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "dee0035d1027b937237c8bbdd6e77288df356ff4" @@ -11572,6 +11580,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "b6a500e983b64d611aab3a9d5b2ec65fd4511f10" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "5376df989fac86247d5cd9f983d11ece28e75fcd" @@ -14432,6 +14443,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "e4c5963ce721f4bb182d474d1a9c0c604325c274" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "4ad6efca173a22d4bf622d5729c3ad13612e6902" @@ -17036,6 +17048,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "7deced4ee8b6e75d5dcafff58c7bd89905c98fd6" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "f0aaccbc0dcda37daa045cb4cda85177bedafdb0" @@ -19890,6 +19905,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "4f7b9505464a8b039fee24745afe2a1c951b81d3" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "0e65840f941f2c5e030cf553e4e5a3cc315fe73d" @@ -22419,6 +22435,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "a82231aecd1ee2ec08d32031cf19b6f6a4f2842e" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "fdd17d17b54069e4f68253b0af0b1a50d389b199" @@ -25246,6 +25265,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "1ce699b6bccf33a45ff58320f4dfeb2593c3111e" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "fb99b9f5be8916e4a7b038e3d1d9d9087f8a5653" @@ -27850,6 +27870,9 @@ "source": "77f4b7a081b20f78557dd404e14d91b89b52a27c", "translation": "227f72b88ebec3e37158e855352776b82662a27c" }, + "views.releaseNotes.title": null, + "views.releaseNotes.header": null, + "views.releaseNotes.viewAllLink": null, "settings.tabs.general": { "source": "9239ee2cda84eca4c3440e2a7b50148af67da3d4", "translation": "f1484fa78b69777c083f5b19555ee4d232d70163" @@ -30695,6 +30718,7 @@ "source": "3f3714587b4802efeb6baf380a59615b8faf0c10", "translation": "f4fa2d18fff78b531c07f83bbc7753c26dbc05d9" }, + "commands.viewReleaseNotes": null, "modals.task.titlePlaceholder": { "source": "c8196f1d1039c2fb290add52ee990c571173d6f1", "translation": "37d9b0a0a59872345ce5e206027f8eccf793e6fa" diff --git a/package.json b/package.json index 5c0bbc70..8bdef4dd 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,12 @@ "description": "Note-based task management with calendar, pomodoro and time-tracking integration.", "main": "main.js", "scripts": { - "build": "npm run build-css && tsc -noEmit --skipLibCheck && node esbuild.config.mjs production", + "build": "npm run build-css && node generate-release-notes-import.mjs && tsc -noEmit --skipLibCheck && node esbuild.config.mjs production", "dev": "npm run build && npm run copy-files", - "dev:watch": "npm run build-css && node esbuild.config.mjs", + "dev:watch": "npm run build-css && node generate-release-notes-import.mjs && node esbuild.config.mjs", "copy-files": "node copy-files.mjs", "build-css": "node build-css.mjs", - "version": "node version-bump.mjs && git add manifest.json versions.json", + "version": "node version-bump.mjs && node generate-release-notes-import.mjs && git add manifest.json versions.json src/releaseNotes.ts", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", diff --git a/src/i18n/resources/en.ts b/src/i18n/resources/en.ts index be524d01..d35ee575 100644 --- a/src/i18n/resources/en.ts +++ b/src/i18n/resources/en.ts @@ -204,6 +204,11 @@ export const en: TranslationTree = { minTime: "Min Time (minutes)", }, }, + releaseNotes: { + title: "What's new in TaskNotes {version}", + header: "What's new in TaskNotes {version}", + viewAllLink: "View all release notes on GitHub →", + }, }, settings: { tabs: { @@ -1496,6 +1501,7 @@ export const en: TranslationTree = { pauseResumePomodoro: "Pause/resume pomodoro timer", refreshCache: "Refresh cache", exportAllTasksIcs: "Export all tasks as ICS file", + viewReleaseNotes: "View release notes", }, modals: { task: { diff --git a/src/main.ts b/src/main.ts index 37ee5b0d..0a9cac13 100644 --- a/src/main.ts +++ b/src/main.ts @@ -94,6 +94,8 @@ import { AutoExportService } from "./services/AutoExportService"; // Type-only import for HTTPAPIService (actual import is dynamic on desktop only) import type { HTTPAPIService } from "./services/HTTPAPIService"; import { createI18nService, I18nService, TranslationKey } from "./i18n"; +import { ReleaseNotesView, RELEASE_NOTES_VIEW_TYPE } from "./views/ReleaseNotesView"; +import { CURRENT_RELEASE_NOTES } from "./releaseNotes"; interface TranslatedCommandDefinition { id: string; @@ -461,6 +463,11 @@ export default class TaskNotesPlugin extends Plugin { this.registerView(KANBAN_VIEW_TYPE, (leaf) => new KanbanView(leaf, this)); + this.registerView( + RELEASE_NOTES_VIEW_TYPE, + (leaf) => new ReleaseNotesView(leaf, this, CURRENT_RELEASE_NOTES, this.manifest.version) + ); + // Register essential editor extensions (now safe after layout ready) this.registerEditorExtension(createTaskLinkOverlay(this)); @@ -639,6 +646,9 @@ export default class TaskNotesPlugin extends Plugin { // Migration check was moved to early startup - just show prompts here if needed await this.showMigrationPromptsIfNeeded(); + + // Check for version updates and show release notes if needed + await this.checkForVersionUpdate(); } catch (error) { console.error("Error during lazy service initialization:", error); } @@ -886,6 +896,36 @@ export default class TaskNotesPlugin extends Plugin { } } + /** + * Check for version updates and show release notes if needed + */ + private async checkForVersionUpdate(): Promise { + try { + const currentVersion = this.manifest.version; + const lastSeenVersion = this.settings.lastSeenVersion; + + // If this is a new install or version has changed, show release notes + if (lastSeenVersion && lastSeenVersion !== currentVersion) { + // Show release notes after a delay to ensure UI is ready + setTimeout(async () => { + await this.activateReleaseNotesView(); + // Update lastSeenVersion immediately after showing the release notes + // This ensures they only show once per version + this.settings.lastSeenVersion = currentVersion; + await this.saveSettings(); + }, 1500); // Slightly longer delay than migration to avoid conflicts + } + + // Update lastSeenVersion if it hasn't been set yet (new install) + if (!lastSeenVersion) { + this.settings.lastSeenVersion = currentVersion; + await this.saveSettings(); + } + } catch (error) { + console.error("Error checking for version update:", error); + } + } + /** * Public method for views to wait for migration completion */ @@ -1429,6 +1469,13 @@ export default class TaskNotesPlugin extends Plugin { } }, }, + { + id: "view-release-notes", + nameKey: "commands.viewReleaseNotes", + callback: async () => { + await this.activateReleaseNotesView(); + }, + }, ]; this.registerCommands(); @@ -1560,6 +1607,10 @@ export default class TaskNotesPlugin extends Plugin { return this.activateView(KANBAN_VIEW_TYPE); } + async activateReleaseNotesView() { + return this.activateView(RELEASE_NOTES_VIEW_TYPE); + } + /** * Open and activate the search pane with a tag query * (Renamed from openSearchPaneWithTag for cleaner API) diff --git a/src/markdown.d.ts b/src/markdown.d.ts new file mode 100644 index 00000000..3ec0d845 --- /dev/null +++ b/src/markdown.d.ts @@ -0,0 +1,5 @@ +// Type declarations for importing markdown files as strings +declare module "*.md" { + const content: string; + export default content; +} diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 14c635d2..d869518f 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -238,6 +238,8 @@ export const DEFAULT_SETTINGS: TaskNotesSettings = { customPriorities: DEFAULT_PRIORITIES, // Migration defaults recurrenceMigrated: false, + // Release notes defaults + lastSeenVersion: undefined, // Status bar defaults showTrackedTasksInStatusBar: false, // Time tracking defaults diff --git a/src/settings/tabs/generalTab.ts b/src/settings/tabs/generalTab.ts index 77f13fa8..5b46a6a6 100644 --- a/src/settings/tabs/generalTab.ts +++ b/src/settings/tabs/generalTab.ts @@ -1,4 +1,5 @@ // import { TAbstractFile } from 'obsidian'; +import { Setting } from "obsidian"; import TaskNotesPlugin from "../../main"; import { createSectionHeader, @@ -226,4 +227,20 @@ export function renderGeneralTab( save(); }, }); + + // Release Notes Section + createSectionHeader(container, "Release Notes"); + createHelpText(container, `Current version: ${plugin.manifest.version}`); + + new Setting(container) + .setName("View release notes") + .setDesc("See what's new in the latest version of TaskNotes") + .addButton((button) => + button + .setButtonText("View release notes") + .setCta() + .onClick(async () => { + await plugin.activateReleaseNotesView(); + }) + ); } diff --git a/src/types/settings.ts b/src/types/settings.ts index 213c82c2..bc6a2534 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -85,6 +85,8 @@ export interface TaskNotesSettings { customPriorities: PriorityConfig[]; // Migration tracking recurrenceMigrated?: boolean; + // Release notes tracking + lastSeenVersion?: string; // Status bar settings showTrackedTasksInStatusBar: boolean; // Time tracking settings diff --git a/src/views/ReleaseNotesView.ts b/src/views/ReleaseNotesView.ts new file mode 100644 index 00000000..8761a2ab --- /dev/null +++ b/src/views/ReleaseNotesView.ts @@ -0,0 +1,85 @@ +import { ItemView, WorkspaceLeaf, MarkdownRenderer } from "obsidian"; +import TaskNotesPlugin from "../main"; + +export const RELEASE_NOTES_VIEW_TYPE = "tasknotes-release-notes"; + +const GITHUB_RELEASES_URL = "https://github.com/callumalpass/tasknotes/releases"; + +export class ReleaseNotesView extends ItemView { + plugin: TaskNotesPlugin; + private releaseNotes: string; + private version: string; + + constructor(leaf: WorkspaceLeaf, plugin: TaskNotesPlugin, releaseNotes: string, version: string) { + super(leaf); + this.plugin = plugin; + this.releaseNotes = releaseNotes; + this.version = version; + } + + getViewType(): string { + return RELEASE_NOTES_VIEW_TYPE; + } + + getDisplayText(): string { + return this.plugin.i18n.translate("views.releaseNotes.title", { version: this.version }); + } + + getIcon(): string { + return "book-open"; + } + + async onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass("tasknotes-release-notes-view"); + + // Create a container for the markdown content + const container = contentEl.createDiv({ cls: "tasknotes-release-notes-container" }); + container.style.padding = "20px"; + container.style.maxWidth = "900px"; + container.style.margin = "0 auto"; + + // Header with version + const header = container.createEl("div", { cls: "release-notes-header" }); + header.style.marginBottom = "20px"; + header.createEl("h1", { + text: this.plugin.i18n.translate("views.releaseNotes.header", { version: this.version }) + }); + + // Markdown content + const markdownContainer = container.createEl("div", { cls: "release-notes-content" }); + markdownContainer.style.marginBottom = "30px"; + + // Render the markdown + await MarkdownRenderer.render( + this.plugin.app, + this.releaseNotes, + markdownContainer, + "", + this as any + ); + + // Footer with link to all releases + const footer = container.createEl("div", { cls: "release-notes-footer" }); + footer.style.borderTop = "1px solid var(--background-modifier-border)"; + footer.style.paddingTop = "20px"; + footer.style.textAlign = "center"; + + const link = footer.createEl("a", { + text: this.plugin.i18n.translate("views.releaseNotes.viewAllLink"), + href: GITHUB_RELEASES_URL, + }); + link.style.color = "var(--text-accent)"; + link.style.textDecoration = "none"; + link.addEventListener("click", (e) => { + e.preventDefault(); + window.open(GITHUB_RELEASES_URL, "_blank"); + }); + } + + async onClose() { + const { contentEl } = this; + contentEl.empty(); + } +}