From 57b81f5d0075a2c8a3b848f184fbe9be1fd47bf1 Mon Sep 17 00:00:00 2001 From: liuxuezhuo Date: Thu, 17 Sep 2026 09:42:30 +0800 Subject: [PATCH 1/2] feat(update): show a badge when a newer release is out Users had to watch the repository to learn about new versions. Check the latest GitHub release once per session and surface a small badge next to the Qoderian mark; clicking it opens the release page. Network failures stay silent so discovery never blocks chat. Co-authored-by: QoderAI --- CHANGELOG.md | 2 + src/features/chat/chat-view.ts | 30 +++++++++++ src/features/update/plugin-update-checker.ts | 54 +++++++++++++++++++ src/i18n/locales/de.json | 4 ++ src/i18n/locales/en.json | 4 ++ src/i18n/locales/es.json | 4 ++ src/i18n/locales/fr.json | 4 ++ src/i18n/locales/ja.json | 4 ++ src/i18n/locales/ko.json | 4 ++ src/i18n/locales/pt.json | 4 ++ src/i18n/locales/ru.json | 4 ++ src/i18n/locales/zh-CN.json | 4 ++ src/i18n/locales/zh-TW.json | 4 ++ src/i18n/types.ts | 6 ++- src/main.ts | 11 ++++ src/style/components/header.css | 25 +++++++++ tests/__mocks__/obsidian.ts | 2 + .../update/plugin-update-checker.test.ts | 45 ++++++++++++++++ 18 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 src/features/update/plugin-update-checker.ts create mode 100644 tests/unit/features/update/plugin-update-checker.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 663a8b5..e41ea5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ version with its date and start a fresh empty `[Unreleased]` above it. - The agent now learns which external context directories are attached: the message you send right after adding or removing one carries the current list, so the agent can use those folders without you referencing every file. +- A non-blocking update badge appears beside the Qoderian mark when GitHub + has a newer stable release; click it to open the release page. ## [1.0.11] - 2026-09-16 diff --git a/src/features/chat/chat-view.ts b/src/features/chat/chat-view.ts index 7807455..4b3d43e 100644 --- a/src/features/chat/chat-view.ts +++ b/src/features/chat/chat-view.ts @@ -6,6 +6,7 @@ import { VIEW_TYPE_QODERIAN } from '../../core/types'; import { t } from '../../i18n/i18n'; import type QoderianPlugin from '../../main'; import { fetchCreditsUsage } from '../../qoder/services/credits-usage'; +import { openExternalBrowserUrl } from '../../qoder/services/qoder-login-service'; import { cancelScheduledAnimationFrame, scheduleAnimationFrame, @@ -46,6 +47,7 @@ export class QoderianView extends ItemView { // DOM Elements private viewContainerEl: HTMLElement | null = null; private logoEl: HTMLElement | null = null; + private updateBadgeEl: HTMLElement | null = null; private newTabButtonEl: HTMLElement | null = null; private newConversationButtonEl: HTMLElement | null = null; private historyButtonEl: HTMLElement | null = null; @@ -226,6 +228,12 @@ export class QoderianView extends ItemView { titleEl.createEl('h4', { text: 'Qoder', cls: 'qoderian-title-text' }); + this.updateBadgeEl = titleEl.createEl('button', { + cls: 'qoderian-update-badge qoderian-hidden', + attr: { type: 'button' }, + }); + void this.refreshUpdateBadge(); + const headerActions = header.createDiv({ cls: 'qoderian-header-actions' }); const feedbackBtn = headerActions.createDiv({ cls: 'qoderian-header-btn' }); setIcon(feedbackBtn, 'message-circle-question'); @@ -317,6 +325,21 @@ export class QoderianView extends ItemView { new QoderianSettingsModal(this.plugin).open(); } + /** Shows the badge once GitHub reports a newer stable release. */ + private async refreshUpdateBadge(): Promise { + const badge = this.updateBadgeEl; + if (!badge) return; + + const update = await this.plugin.getAvailableUpdate(); + if (!update || this.updateBadgeEl !== badge) return; + + badge.dataset.version = update.version; + badge.setText(t('updates.available', { version: update.version })); + badge.setAttribute('aria-label', t('updates.openRelease', { version: update.version })); + badge.removeClass('qoderian-hidden'); + badge.addEventListener('click', () => openExternalBrowserUrl(update.url), { once: true }); + } + private buildInputFooter(): void { if (!this.viewContainerEl) return; @@ -387,6 +410,13 @@ export class QoderianView extends ItemView { setButtonTooltip(this.newConversationButtonEl, t('nav.newConversation')); } if (this.historyButtonEl) setButtonTooltip(this.historyButtonEl, t('nav.chatHistory')); + if (this.updateBadgeEl?.dataset.version) { + this.updateBadgeEl.setText(t('updates.available', { version: this.updateBadgeEl.dataset.version })); + this.updateBadgeEl.setAttribute( + 'aria-label', + t('updates.openRelease', { version: this.updateBadgeEl.dataset.version }), + ); + } this.creditsUsageButton?.refreshLocale(); for (const tab of this.tabManager?.getAllTabs() ?? []) { tab.ui.composerResize?.refreshLocale(); diff --git a/src/features/update/plugin-update-checker.ts b/src/features/update/plugin-update-checker.ts new file mode 100644 index 0000000..9edda68 --- /dev/null +++ b/src/features/update/plugin-update-checker.ts @@ -0,0 +1,54 @@ +import { requestUrl } from 'obsidian'; + +const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/QoderAI/Qoderian/releases/latest'; + +export interface QoderianUpdate { + version: string; + url: string; +} + +interface GitHubReleaseResponse { + html_url?: unknown; + tag_name?: unknown; +} + +function parseVersion(value: string): number[] | null { + const match = value.trim().replace(/^v/i, '').match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/); + if (!match) return null; + return match.slice(1).map(Number); +} + +/** Semver comparison for the three-part release tags used by Qoderian. */ +export function isNewerQoderianVersion(currentVersion: string, latestVersion: string): boolean { + const current = parseVersion(currentVersion); + const latest = parseVersion(latestVersion); + if (!current || !latest) return false; + + for (let index = 0; index < latest.length; index += 1) { + if (latest[index] !== current[index]) return latest[index] > current[index]; + } + return false; +} + +/** + * Checks the latest stable GitHub release. Network and malformed-response + * failures are intentionally silent so update discovery never blocks chat. + */ +export async function fetchAvailableQoderianUpdate( + currentVersion: string, +): Promise { + try { + const response = await requestUrl({ + url: LATEST_RELEASE_API_URL, + headers: { Accept: 'application/vnd.github+json' }, + }); + const release = response.json as GitHubReleaseResponse; + if (typeof release.tag_name !== 'string' || typeof release.html_url !== 'string') return null; + + const version = release.tag_name.replace(/^v/i, ''); + if (!isNewerQoderianVersion(currentVersion, version)) return null; + return { version, url: release.html_url }; + } catch { + return null; + } +} diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 9bd4df8..e0bc4f9 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "Standard", "backToList": "Zurück zur Modellliste" + }, + "updates": { + "available": "Update {version}", + "openRelease": "Qoderian-Version {version} öffnen" } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 7ad8910..fbfab10 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "Default", "backToList": "Back to model list" + }, + "updates": { + "available": "Update {version}", + "openRelease": "Open Qoderian release {version}" } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 3cfbb6b..0d4b5c0 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "Predeterminado", "backToList": "Volver a la lista de modelos" + }, + "updates": { + "available": "Actualizar {version}", + "openRelease": "Abrir la versión {version} de Qoderian" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index db35c7a..dac48cd 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "Par défaut", "backToList": "Retour à la liste des modèles" + }, + "updates": { + "available": "Mise à jour {version}", + "openRelease": "Ouvrir la version {version} de Qoderian" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index f805751..21b624b 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "デフォルト", "backToList": "モデルリストに戻る" + }, + "updates": { + "available": "更新 {version}", + "openRelease": "Qoderian {version} リリースを開く" } } diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index d91904c..aed5c58 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "기본", "backToList": "모델 목록으로 돌아가기" + }, + "updates": { + "available": "업데이트 {version}", + "openRelease": "Qoderian {version} 릴리스 열기" } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 3a1a4cd..fa89db1 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "Padrão", "backToList": "Voltar à lista de modelos" + }, + "updates": { + "available": "Atualizar {version}", + "openRelease": "Abrir a versão {version} do Qoderian" } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 49f95d1..b13d306 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "По умолчанию", "backToList": "Назад к списку моделей" + }, + "updates": { + "available": "Обновить {version}", + "openRelease": "Открыть выпуск Qoderian {version}" } } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index fcad0eb..b4b76e7 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "默认", "backToList": "返回模型列表" + }, + "updates": { + "available": "更新 {version}", + "openRelease": "查看 Qoderian {version} 更新" } } diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index a810c7e..06bcea9 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -400,5 +400,9 @@ "thinkingEffort": "Thinking Effort", "default": "預設", "backToList": "返回模型列表" + }, + "updates": { + "available": "更新 {version}", + "openRelease": "檢視 Qoderian {version} 更新" } } diff --git a/src/i18n/types.ts b/src/i18n/types.ts index be549b1..f2aa0ab 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -344,4 +344,8 @@ export type TranslationKey = // Settings - Language | 'settings.language.name' - | 'settings.language.desc'; + | 'settings.language.desc' + + // Plugin updates + | 'updates.available' + | 'updates.openRelease'; diff --git a/src/main.ts b/src/main.ts index c092353..a2afd17 100644 --- a/src/main.ts +++ b/src/main.ts @@ -24,6 +24,10 @@ import { QoderianView } from './features/chat/chat-view'; import { openFeedbackModal } from './features/feedback/ui/feedback-modal'; import { type InlineEditContext, InlineEditModal } from './features/inline-edit/ui/modal'; import { QoderianSettingTab } from './features/settings/settings-tab'; +import { + fetchAvailableQoderianUpdate, + type QoderianUpdate, +} from './features/update/plugin-update-checker'; import { setLocale, t } from './i18n/i18n'; import type { Locale } from './i18n/types'; import { getActiveQoderCliEdition, setActiveQoderCliEdition } from './qoder/config/cli-edition'; @@ -51,6 +55,13 @@ export default class QoderianPlugin extends Plugin { qoderServices!: QoderServices; private conversations: Conversation[] = []; private lastKnownTabManagerState: AppTabManagerState | null = null; + private updateCheckPromise: Promise | null = null; + + /** Checks once per plugin session so reopening the view does not hit GitHub repeatedly. */ + getAvailableUpdate(): Promise { + this.updateCheckPromise ??= fetchAvailableQoderianUpdate(this.manifest.version); + return this.updateCheckPromise; + } async onload() { await this.loadSettings(); diff --git a/src/style/components/header.css b/src/style/components/header.css index 45523d4..4df04ca 100644 --- a/src/style/components/header.css +++ b/src/style/components/header.css @@ -26,6 +26,31 @@ color: var(--qoderian-brand); } +.qoderian-update-badge { + flex: none; + min-height: 22px; + padding: 2px 7px; + border: 1px solid rgba(var(--qoderian-brand-rgb), 0.32); + border-radius: 999px; + background: rgba(var(--qoderian-brand-rgb), 0.1); + box-shadow: none; + color: var(--qoderian-brand); + font: inherit; + font-size: 10px; + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.qoderian-update-badge:hover, +.qoderian-update-badge:focus-visible { + border-color: rgba(var(--qoderian-brand-rgb), 0.55); + background: rgba(var(--qoderian-brand-rgb), 0.16); + box-shadow: none; + outline: none; +} + .qoderian-header-actions { display: flex; align-items: center; diff --git a/tests/__mocks__/obsidian.ts b/tests/__mocks__/obsidian.ts index e27e9b0..1211d14 100644 --- a/tests/__mocks__/obsidian.ts +++ b/tests/__mocks__/obsidian.ts @@ -465,3 +465,5 @@ export class TFolder { this.name = path.split('/').pop() || ''; } } + +export const requestUrl = jest.fn(); diff --git a/tests/unit/features/update/plugin-update-checker.test.ts b/tests/unit/features/update/plugin-update-checker.test.ts new file mode 100644 index 0000000..d449680 --- /dev/null +++ b/tests/unit/features/update/plugin-update-checker.test.ts @@ -0,0 +1,45 @@ +import { requestUrl } from 'obsidian'; + +import { + fetchAvailableQoderianUpdate, + isNewerQoderianVersion, +} from '@/features/update/plugin-update-checker'; + +const mockRequestUrl = requestUrl as jest.Mock; + +describe('plugin update checker', () => { + beforeEach(() => { + mockRequestUrl.mockReset(); + }); + + it.each([ + ['1.0.7', '1.0.8', true], + ['1.0.7', '1.1.0', true], + ['1.9.9', '2.0.0', true], + ['1.0.7', '1.0.7', false], + ['1.0.8', '1.0.7', false], + ['invalid', '1.0.8', false], + ])('compares %s with %s', (current, latest, expected) => { + expect(isNewerQoderianVersion(current, latest)).toBe(expected); + }); + + it('returns the newer stable GitHub release', async () => { + mockRequestUrl.mockResolvedValue({ + json: { + tag_name: 'v1.1.0', + html_url: 'https://github.com/QoderAI/Qoderian/releases/tag/v1.1.0', + }, + }); + + await expect(fetchAvailableQoderianUpdate('1.0.7')).resolves.toEqual({ + version: '1.1.0', + url: 'https://github.com/QoderAI/Qoderian/releases/tag/v1.1.0', + }); + }); + + it('fails silently when offline', async () => { + mockRequestUrl.mockRejectedValue(new Error('offline')); + + await expect(fetchAvailableQoderianUpdate('1.0.7')).resolves.toBeNull(); + }); +}); From d233154efee13c57f3753b54968972a197d7cb96 Mon Sep 17 00:00:00 2001 From: liuxuezhuo Date: Thu, 17 Sep 2026 14:27:47 +0800 Subject: [PATCH 2/2] fix(update): keep the update entry clickable and put it in the header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The badge was a one-shot button (once: true) sitting in the title row, it wore the theme's default button chrome, and clicking it left Obsidian for the GitHub release page. Move it into the header actions next to the other icons as a brand-colored download button, keep it clickable on every click, and open Qoderian's plugin page instead — Obsidian's own update button lives there, so updating no longer needs a trip to the browser. Co-authored-by: QoderAI (Qwen 3.8 Max) --- CHANGELOG.md | 5 +-- src/features/chat/chat-view.ts | 21 ++++++------- src/features/update/plugin-update-checker.ts | 15 +++++++++ src/i18n/locales/de.json | 3 +- src/i18n/locales/en.json | 3 +- src/i18n/locales/es.json | 3 +- src/i18n/locales/fr.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/ko.json | 3 +- src/i18n/locales/pt.json | 3 +- src/i18n/locales/ru.json | 3 +- src/i18n/locales/zh-CN.json | 3 +- src/i18n/locales/zh-TW.json | 3 +- src/i18n/types.ts | 1 - src/style/components/header.css | 31 +++++++++---------- .../update/plugin-update-checker.test.ts | 6 ++++ 16 files changed, 58 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e41ea5d..f2ec490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,9 @@ version with its date and start a fresh empty `[Unreleased]` above it. - The agent now learns which external context directories are attached: the message you send right after adding or removing one carries the current list, so the agent can use those folders without you referencing every file. -- A non-blocking update badge appears beside the Qoderian mark when GitHub - has a newer stable release; click it to open the release page. +- An update icon appears in the view header when GitHub has a newer stable + release; click it to open Qoderian's plugin page, where Obsidian's update + button lives. ## [1.0.11] - 2026-09-16 diff --git a/src/features/chat/chat-view.ts b/src/features/chat/chat-view.ts index 4b3d43e..7ac8ed6 100644 --- a/src/features/chat/chat-view.ts +++ b/src/features/chat/chat-view.ts @@ -6,7 +6,6 @@ import { VIEW_TYPE_QODERIAN } from '../../core/types'; import { t } from '../../i18n/i18n'; import type QoderianPlugin from '../../main'; import { fetchCreditsUsage } from '../../qoder/services/credits-usage'; -import { openExternalBrowserUrl } from '../../qoder/services/qoder-login-service'; import { cancelScheduledAnimationFrame, scheduleAnimationFrame, @@ -16,6 +15,7 @@ import { setButtonTooltip } from '../../shared/dom/tooltip'; import { createIconSvg, QODER_ICON,QODERIAN_ICON_ID } from '../../shared/icons'; import { openFeedbackModal } from '../feedback/ui/feedback-modal'; import { QoderianSettingsModal } from '../settings/settings-modal'; +import { openQoderianUpdatePage } from '../update/plugin-update-checker'; import type { HistoryConversationStatus } from './controllers/conversation-controller'; import { sendTabInputMessageFromExplicitEnterShortcut, @@ -228,13 +228,14 @@ export class QoderianView extends ItemView { titleEl.createEl('h4', { text: 'Qoder', cls: 'qoderian-title-text' }); - this.updateBadgeEl = titleEl.createEl('button', { - cls: 'qoderian-update-badge qoderian-hidden', + const headerActions = header.createDiv({ cls: 'qoderian-header-actions' }); + this.updateBadgeEl = headerActions.createEl('button', { + cls: 'qoderian-header-btn qoderian-update-badge qoderian-hidden', attr: { type: 'button' }, }); + setIcon(this.updateBadgeEl, 'download'); void this.refreshUpdateBadge(); - const headerActions = header.createDiv({ cls: 'qoderian-header-actions' }); const feedbackBtn = headerActions.createDiv({ cls: 'qoderian-header-btn' }); setIcon(feedbackBtn, 'message-circle-question'); setButtonTooltip(feedbackBtn, t('commands.submitFeedback')); @@ -334,10 +335,9 @@ export class QoderianView extends ItemView { if (!update || this.updateBadgeEl !== badge) return; badge.dataset.version = update.version; - badge.setText(t('updates.available', { version: update.version })); - badge.setAttribute('aria-label', t('updates.openRelease', { version: update.version })); + setButtonTooltip(badge, t('updates.available', { version: update.version })); badge.removeClass('qoderian-hidden'); - badge.addEventListener('click', () => openExternalBrowserUrl(update.url), { once: true }); + badge.addEventListener('click', () => openQoderianUpdatePage(this.plugin.manifest.id)); } private buildInputFooter(): void { @@ -411,10 +411,9 @@ export class QoderianView extends ItemView { } if (this.historyButtonEl) setButtonTooltip(this.historyButtonEl, t('nav.chatHistory')); if (this.updateBadgeEl?.dataset.version) { - this.updateBadgeEl.setText(t('updates.available', { version: this.updateBadgeEl.dataset.version })); - this.updateBadgeEl.setAttribute( - 'aria-label', - t('updates.openRelease', { version: this.updateBadgeEl.dataset.version }), + setButtonTooltip( + this.updateBadgeEl, + t('updates.available', { version: this.updateBadgeEl.dataset.version }), ); } this.creditsUsageButton?.refreshLocale(); diff --git a/src/features/update/plugin-update-checker.ts b/src/features/update/plugin-update-checker.ts index 9edda68..eac0851 100644 --- a/src/features/update/plugin-update-checker.ts +++ b/src/features/update/plugin-update-checker.ts @@ -1,5 +1,7 @@ import { requestUrl } from 'obsidian'; +import { openExternalBrowserUrl } from '../../qoder/services/qoder-login-service'; + const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/QoderAI/Qoderian/releases/latest'; export interface QoderianUpdate { @@ -30,6 +32,19 @@ export function isNewerQoderianVersion(currentVersion: string, latestVersion: st return false; } +/** Obsidian's own URI for a plugin's entry in the community-plugins list. */ +export function qoderianPluginPageUri(pluginId: string): string { + return `obsidian://show-plugin?id=${encodeURIComponent(pluginId)}`; +} + +/** + * Opens Obsidian's plugin page, where the update button lives. Obsidian + * handles its own URI scheme, so this stays inside the app. + */ +export function openQoderianUpdatePage(pluginId: string): void { + openExternalBrowserUrl(qoderianPluginPageUri(pluginId)); +} + /** * Checks the latest stable GitHub release. Network and malformed-response * failures are intentionally silent so update discovery never blocks chat. diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index e0bc4f9..b9ac998 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -402,7 +402,6 @@ "backToList": "Zurück zur Modellliste" }, "updates": { - "available": "Update {version}", - "openRelease": "Qoderian-Version {version} öffnen" + "available": "Update {version}" } } diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index fbfab10..e08640c 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -402,7 +402,6 @@ "backToList": "Back to model list" }, "updates": { - "available": "Update {version}", - "openRelease": "Open Qoderian release {version}" + "available": "Update {version}" } } diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 0d4b5c0..92e82a8 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -402,7 +402,6 @@ "backToList": "Volver a la lista de modelos" }, "updates": { - "available": "Actualizar {version}", - "openRelease": "Abrir la versión {version} de Qoderian" + "available": "Actualizar {version}" } } diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index dac48cd..2d54d69 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -402,7 +402,6 @@ "backToList": "Retour à la liste des modèles" }, "updates": { - "available": "Mise à jour {version}", - "openRelease": "Ouvrir la version {version} de Qoderian" + "available": "Mise à jour {version}" } } diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 21b624b..b13f374 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -402,7 +402,6 @@ "backToList": "モデルリストに戻る" }, "updates": { - "available": "更新 {version}", - "openRelease": "Qoderian {version} リリースを開く" + "available": "更新 {version}" } } diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index aed5c58..c5957b9 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -402,7 +402,6 @@ "backToList": "모델 목록으로 돌아가기" }, "updates": { - "available": "업데이트 {version}", - "openRelease": "Qoderian {version} 릴리스 열기" + "available": "업데이트 {version}" } } diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index fa89db1..9260a2f 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -402,7 +402,6 @@ "backToList": "Voltar à lista de modelos" }, "updates": { - "available": "Atualizar {version}", - "openRelease": "Abrir a versão {version} do Qoderian" + "available": "Atualizar {version}" } } diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index b13d306..77f93f6 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -402,7 +402,6 @@ "backToList": "Назад к списку моделей" }, "updates": { - "available": "Обновить {version}", - "openRelease": "Открыть выпуск Qoderian {version}" + "available": "Обновить {version}" } } diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index b4b76e7..08892fd 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -402,7 +402,6 @@ "backToList": "返回模型列表" }, "updates": { - "available": "更新 {version}", - "openRelease": "查看 Qoderian {version} 更新" + "available": "更新 {version}" } } diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json index 06bcea9..efcbd48 100644 --- a/src/i18n/locales/zh-TW.json +++ b/src/i18n/locales/zh-TW.json @@ -402,7 +402,6 @@ "backToList": "返回模型列表" }, "updates": { - "available": "更新 {version}", - "openRelease": "檢視 Qoderian {version} 更新" + "available": "更新 {version}" } } diff --git a/src/i18n/types.ts b/src/i18n/types.ts index f2aa0ab..2b396e4 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -348,4 +348,3 @@ export type TranslationKey = // Plugin updates | 'updates.available' - | 'updates.openRelease'; diff --git a/src/style/components/header.css b/src/style/components/header.css index 4df04ca..5b65bc0 100644 --- a/src/style/components/header.css +++ b/src/style/components/header.css @@ -26,27 +26,24 @@ color: var(--qoderian-brand); } -.qoderian-update-badge { - flex: none; - min-height: 22px; - padding: 2px 7px; - border: 1px solid rgba(var(--qoderian-brand-rgb), 0.32); - border-radius: 999px; - background: rgba(var(--qoderian-brand-rgb), 0.1); +/* Update badge: an icon button in the header actions, shown only while a newer + release exists. The doubled class keeps it clear of the theme's default + button chrome without !important. */ +.qoderian-header-actions .qoderian-update-badge.qoderian-update-badge { + padding: 0; + height: auto; + min-height: 0; + border: none; + border-radius: 3px; + background: transparent; box-shadow: none; color: var(--qoderian-brand); - font: inherit; - font-size: 10px; - font-weight: 600; - line-height: 1; - white-space: nowrap; - cursor: pointer; } -.qoderian-update-badge:hover, -.qoderian-update-badge:focus-visible { - border-color: rgba(var(--qoderian-brand-rgb), 0.55); - background: rgba(var(--qoderian-brand-rgb), 0.16); +.qoderian-header-actions .qoderian-update-badge.qoderian-update-badge:hover, +.qoderian-header-actions .qoderian-update-badge.qoderian-update-badge:focus-visible { + color: var(--qoderian-brand); + background: rgba(var(--qoderian-brand-rgb), 0.14); box-shadow: none; outline: none; } diff --git a/tests/unit/features/update/plugin-update-checker.test.ts b/tests/unit/features/update/plugin-update-checker.test.ts index d449680..4005b48 100644 --- a/tests/unit/features/update/plugin-update-checker.test.ts +++ b/tests/unit/features/update/plugin-update-checker.test.ts @@ -3,6 +3,7 @@ import { requestUrl } from 'obsidian'; import { fetchAvailableQoderianUpdate, isNewerQoderianVersion, + qoderianPluginPageUri, } from '@/features/update/plugin-update-checker'; const mockRequestUrl = requestUrl as jest.Mock; @@ -37,6 +38,11 @@ describe('plugin update checker', () => { }); }); + it('builds the plugin page URI Obsidian itself handles', () => { + expect(qoderianPluginPageUri('qoderian')).toBe('obsidian://show-plugin?id=qoderian'); + expect(qoderianPluginPageUri('weird id/name')).toBe('obsidian://show-plugin?id=weird%20id%2Fname'); + }); + it('fails silently when offline', async () => { mockRequestUrl.mockRejectedValue(new Error('offline'));