diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 64243a82f5..4ae6ff0bd8 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -3669,11 +3669,6 @@ class ChatProviderTemplate(TypedDict): "type": "string", "hint": "如果唤醒前缀为 /, 额外聊天唤醒前缀为 chat,则需要 /chat 才会触发 LLM 请求", }, - "provider_settings.prompt_prefix": { - "description": "用户提示词", - "type": "string", - "hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。", - }, "provider_settings.image_compress_enabled": { "description": "启用图片压缩", "type": "bool", @@ -3697,6 +3692,12 @@ class ChatProviderTemplate(TypedDict): }, "slider": {"min": 1, "max": 100, "step": 1}, }, + "provider_settings.prompt_prefix": { + "description": "用户提示词", + "type": "string", + "hint": "可使用 {{prompt}} 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。", + "collapsed": True, + }, "provider_tts_settings.dual_output": { "description": "开启 TTS 时同时输出语音和文字内容", "type": "bool", diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css index 3e1ffbc326..1871b5b648 100644 --- a/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css +++ b/dashboard/src/assets/mdi-subset/materialdesignicons-subset.css @@ -1,4 +1,4 @@ -/* Auto-generated MDI subset – 261 icons */ +/* Auto-generated MDI subset – 256 icons */ /* Do not edit manually. Run: pnpm run subset-icons */ @font-face { @@ -112,6 +112,10 @@ content: "\F09D1"; } +.mdi-broadcast::before { + content: "\F1720"; +} + .mdi-broom::before { content: "\F00E2"; } @@ -464,10 +468,6 @@ content: "\F0234"; } -.mdi-filter-variant::before { - content: "\F0236"; -} - .mdi-folder::before { content: "\F024B"; } @@ -552,6 +552,10 @@ content: "\F02DC"; } +.mdi-hook::before { + content: "\F06E2"; +} + .mdi-identifier::before { content: "\F0EFE"; } @@ -760,10 +764,6 @@ content: "\F03E4"; } -.mdi-pause-circle-outline::before { - content: "\F03E6"; -} - .mdi-pencil::before { content: "\F03EB"; } @@ -784,22 +784,10 @@ content: "\F03F6"; } -.mdi-pin::before { - content: "\F0403"; -} - -.mdi-pin-outline::before { - content: "\F0931"; -} - .mdi-play::before { content: "\F040A"; } -.mdi-play-circle-outline::before { - content: "\F040D"; -} - .mdi-plus::before { content: "\F0415"; } @@ -1024,14 +1012,6 @@ content: "\F056E"; } -.mdi-view-grid::before { - content: "\F0570"; -} - -.mdi-view-list::before { - content: "\F0572"; -} - .mdi-volume-high::before { content: "\F057E"; } diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff index 02f37fd732..0739236499 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff differ diff --git a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 index a26ce1678c..1582327459 100644 Binary files a/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 and b/dashboard/src/assets/mdi-subset/materialdesignicons-webfont-subset.woff2 differ diff --git a/dashboard/src/components/extension/PinnedPluginItem.vue b/dashboard/src/components/extension/PinnedPluginItem.vue deleted file mode 100644 index 5104d3f69b..0000000000 --- a/dashboard/src/components/extension/PinnedPluginItem.vue +++ /dev/null @@ -1,216 +0,0 @@ - - - - - - - - - - - - {{ plugin.display_name || plugin.name }} - - - - - - - - - - - - {{ plugin.display_name || plugin.name }} - {{ authorDisplay || (plugin.author || '') }} - - - - - - - - - mdi-book-open-page-variant - - - - - - - - mdi-cog - - - - - - - - mdi-refresh - - - - - - - - mdi-update - - - - - - - - mdi-information - - - - - - - - mdi-delete - - - - - - - - - {{ isPinned ? 'mdi-pin' : 'mdi-pin-outline' }} - - - - - diff --git a/dashboard/src/components/shared/ExtensionCard.vue b/dashboard/src/components/shared/ExtensionCard.vue index f7c05c5c59..9953422a6c 100644 --- a/dashboard/src/components/shared/ExtensionCard.vue +++ b/dashboard/src/components/shared/ExtensionCard.vue @@ -1,8 +1,7 @@ - + + + + + + + + + + + {{ tm("titles.installedAstrBotPlugins") }} + + {{ selectedPluginId }} + + + + {{ tm("detail.notFound") }} + + + + diff --git a/dashboard/src/views/extension/InstalledPluginsTab.vue b/dashboard/src/views/extension/InstalledPluginsTab.vue index 97b3a56a92..41fe79382e 100644 --- a/dashboard/src/views/extension/InstalledPluginsTab.vue +++ b/dashboard/src/views/extension/InstalledPluginsTab.vue @@ -1,11 +1,6 @@ @@ -284,134 +172,10 @@ const pinnedPlugins = computed(() => { > - - - - - - - - - - {{ - showReserved ? "mdi-eye-off" : "mdi-eye" - }} - {{ - showReserved - ? tm("buttons.hideSystemPlugins") - : tm("buttons.showSystemPlugins") - }} - - - - mdi-update - {{ tm("buttons.updateAll") }} - - - - - - - {{ tm("filters.all") }} - - - {{ tm("status.enabled") }} - - - {{ tm("status.disabled") }} - - - - - - - - - - - - - - - - - {{ tm('titles.pinnedPlugins') }} - - - - - - - - - - - - - - - { - - - - - - - - - - - - - {{ item.display_name && item.display_name.length ? item.display_name : item.name }} - - - - {{ item.name }} - - - - {{ tm("status.system") }} - - - - - - - - - {{ item.desc }} - - - - {{ tm("card.status.supportPlatform") }}: - - - {{ platformId }} - - - - - {{ tm("card.status.astrbotVersion") }}: - - - {{ item.astrbot_version }} - - - - - - - - {{ item.version }} - - - mdi-alert - - {{ tm("messages.hasUpdate") }} - {{ item.online_version }} - - - - - {{ item.online_version }} - - - {{ tm("buttons.update") }} - - - - - - {{ item.author }} - - - - - - {{ isPinned(item.name) ? 'mdi-pin' : 'mdi-pin-outline' }} - - - - {{ tm("buttons.enable") }} - - - {{ tm("buttons.disable") }} - - - - {{ tm("buttons.reload") }} - - - - {{ tm("buttons.configure") }} - - - - {{ tm("buttons.viewDocs") }} - - - - - - - - - {{ tm("buttons.viewInfo") }} - - - - {{ tm("buttons.update") }} - - - - {{ tm("buttons.uninstall") }} - - - - - - - - mdi-puzzle-outline - - {{ tm("empty.noPlugins") }} - - - {{ tm("empty.noPluginsDesc") }} - - - - - - - - - + { togglePin(extension)" class="rounded-lg" style="background-color: rgb(var(--v-theme-mcpCardBg))" + @click="openPluginDetail(extension)" @configure="openExtensionConfig(extension.name)" @uninstall=" (ext, options) => uninstallExtension(ext.name, options) @@ -821,74 +324,25 @@ const pinnedPlugins = computed(() => { + + + + + + diff --git a/dashboard/src/views/extension/PluginDetailPage.vue b/dashboard/src/views/extension/PluginDetailPage.vue new file mode 100644 index 0000000000..d6104015e0 --- /dev/null +++ b/dashboard/src/views/extension/PluginDetailPage.vue @@ -0,0 +1,954 @@ + + + + + + + + {{ tm("titles.installedAstrBotPlugins") }} + + + {{ displayName }} + + + + + + + {{ displayName }} + + {{ pluginDesc }} + + + + + + {{ tm("detail.contents") }} + + + + + {{ group.title }} + {{ group.handlers.length }} + + + + + + + + + + {{ + isCommandGroupExpanded(item.key) + ? "mdi-chevron-down" + : "mdi-chevron-right" + }} + + + + + {{ item.displayCommand }} + + + + + + + {{ tm("detail.subCommandsCount", { count: item.children.length }) }} + + + {{ item.handler.desc || tm("status.unknown") }} + + + + + + + + + + + + + {{ getHandlerDisplayName(handler, group.key) }} + + + + + + {{ getHandlerTiming(handler) }} + + {{ handler.desc || tm("status.unknown") }} + + + + + + + + + + + {{ tm("detail.noContents") }} + + + + + + {{ tm("detail.info.title") }} + + + + + {{ row.label }} + + + {{ row.actionText }} + + + {{ row.value }} + + + {{ row.value || tm("status.unknown") }} + + + + + + + + + {{ tm("detail.docsTitle") }} + + + + + + + {{ readmeError }} + + + {{ tm("detail.docsEmpty") }} + + + + + + + + + diff --git a/dashboard/src/views/extension/extensionPreferenceStorage.mjs b/dashboard/src/views/extension/extensionPreferenceStorage.mjs deleted file mode 100644 index 31cd90bbef..0000000000 --- a/dashboard/src/views/extension/extensionPreferenceStorage.mjs +++ /dev/null @@ -1,84 +0,0 @@ -export const SHOW_RESERVED_PLUGINS_STORAGE_KEY = "showReservedPlugins"; -export const PLUGIN_LIST_VIEW_MODE_STORAGE_KEY = "pluginListViewMode"; -export const PIN_UPDATES_ON_TOP_STORAGE_KEY = "pinUpdatesOnTop"; - -/** - * Resolve the storage backend for reading preferences. - * Pass `null` to explicitly disable storage access in callers/tests. - */ -const getStorageForRead = (storageOverride) => { - if (storageOverride === null) { - return null; - } - if (storageOverride !== undefined) { - return typeof storageOverride?.getItem === "function" - ? storageOverride - : null; - } - if (typeof window === "undefined") { - return null; - } - try { - const localStorage = window.localStorage ?? null; - return typeof localStorage?.getItem === "function" ? localStorage : null; - } catch { - return null; - } -}; - -/** - * Resolve the storage backend for writing preferences. - * Pass `null` to explicitly disable storage access in callers/tests. - */ -const getStorageForWrite = (storageOverride) => { - if (storageOverride === null) { - return null; - } - if (storageOverride !== undefined) { - return typeof storageOverride?.setItem === "function" - ? storageOverride - : null; - } - if (typeof window === "undefined") { - return null; - } - try { - const localStorage = window.localStorage ?? null; - return typeof localStorage?.setItem === "function" ? localStorage : null; - } catch { - return null; - } -}; - -export const readBooleanPreference = (key, fallback, storage) => { - const targetStorage = getStorageForRead(storage); - if (!targetStorage) { - return fallback; - } - - try { - const saved = targetStorage.getItem(key); - if (saved === "true") { - return true; - } - if (saved === "false") { - return false; - } - return fallback; - } catch { - return fallback; - } -}; - -export const writeBooleanPreference = (key, value, storage) => { - const targetStorage = getStorageForWrite(storage); - if (!targetStorage) { - return; - } - - try { - targetStorage.setItem(key, String(value)); - } catch { - // Ignore restricted storage environments. - } -}; diff --git a/dashboard/src/views/extension/useExtensionPage.js b/dashboard/src/views/extension/useExtensionPage.js index 3ceecc85bc..f7bcc026e8 100644 --- a/dashboard/src/views/extension/useExtensionPage.js +++ b/dashboard/src/views/extension/useExtensionPage.js @@ -14,16 +14,8 @@ import { getValidHashTab, replaceTabRoute, } from "@/utils/hashRouteTabs.mjs"; -import { - PIN_UPDATES_ON_TOP_STORAGE_KEY, - PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, - SHOW_RESERVED_PLUGINS_STORAGE_KEY, - readBooleanPreference, - writeBooleanPreference, -} from "./extensionPreferenceStorage.mjs"; import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; -import { useDisplay } from "vuetify"; const useRandomPluginsDisplay = ({ activeTab, marketSearch, currentPage }) => { const showRandomPlugins = ref(true); @@ -78,7 +70,6 @@ export const useExtensionPage = () => { const { tm } = useModuleI18n("features/extension"); const router = useRouter(); const route = useRoute(); - const { width } = useDisplay(); const getSelectedGitHubProxy = () => { if (typeof window === "undefined" || !window.localStorage) return ""; @@ -129,11 +120,6 @@ export const useExtensionPage = () => { message: "", }); - // 从 localStorage 恢复显示系统插件的状态,默认为 false(隐藏) - const getInitialShowReserved = () => { - return readBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, false); - }; - const showReserved = ref(getInitialShowReserved()); const snack_message = ref(""); const snack_show = ref(false); const snack_success = ref("success"); @@ -178,23 +164,7 @@ export const useExtensionPage = () => { repoUrl: null, }); - // 新增变量支持列表视图 - // 从 localStorage 恢复显示模式,默认为 false(卡片视图) - const getInitialListViewMode = () => { - return readBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, false); - }; - const isListView = ref(getInitialListViewMode()); const pluginSearch = ref(""); - const installedStatusFilter = ref("all"); - const installedSortBy = ref("default"); - const installedSortOrder = ref("desc"); - const getInitialPinUpdatesOnTop = () => { - return readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true); - }; - const pinUpdatesOnTop = ref(getInitialPinUpdatesOnTop()); - watch(pinUpdatesOnTop, (val) => { - writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, val); - }); const loading_ = ref(false); // 分页相关 @@ -348,67 +318,9 @@ export const useExtensionPage = () => { return items; }); - const installedSortItems = computed(() => [ - { title: tm("sort.default"), value: "default" }, - { title: tm("sort.installTime"), value: "install_time" }, - { title: tm("sort.name"), value: "name" }, - { title: tm("sort.author"), value: "author" }, - { title: tm("sort.updateStatus"), value: "update_status" }, - ]); - - const installedSortUsesOrder = computed( - () => installedSortBy.value !== "default", - ); - - // 插件表格的表头定义 - const showAuthorColumn = computed(() => width.value >= 1280); - const pluginHeaders = computed(() => { - const headers = [ - { - title: tm("table.headers.name"), - key: "name", - sortable: false, - width: showAuthorColumn.value ? "24%" : "26%", - }, - { - title: tm("table.headers.description"), - key: "desc", - sortable: false, - width: showAuthorColumn.value ? "32%" : "36%", - }, - { - title: tm("table.headers.version"), - key: "version", - sortable: false, - width: showAuthorColumn.value ? "12%" : "14%", - }, - ]; - - if (showAuthorColumn.value) { - headers.push({ - title: tm("table.headers.author"), - key: "author", - sortable: false, - width: "10%", - }); - } - - headers.push({ - title: tm("table.headers.actions"), - key: "actions", - sortable: false, - width: showAuthorColumn.value ? "22%" : "24%", - }); - - return headers; - }); - // 过滤要显示的插件 const filteredExtensions = computed(() => { const data = Array.isArray(extension_data?.data) ? extension_data.data : []; - if (!showReserved.value) { - return data.filter((ext) => !ext.reserved); - } return data; }); @@ -421,126 +333,35 @@ export const useExtensionPage = () => { }, ); - const compareInstalledPluginAuthors = (left, right) => - normalizeStr(left?.author ?? "").localeCompare( - normalizeStr(right?.author ?? ""), - undefined, - { sensitivity: "base" }, - ); - - const getInstalledAtTimestamp = (plugin) => { - const parsed = Date.parse(plugin?.installed_at ?? ""); - return Number.isFinite(parsed) ? parsed : null; - }; - const compareInstalledFallback = (left, right) => { + const reservedDiff = + Number(!!left.plugin?.reserved) - Number(!!right.plugin?.reserved); + if (reservedDiff !== 0) { + return reservedDiff; + } + const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin); return nameCompare !== 0 ? nameCompare : left.index - right.index; }; - const compareInstalledUpdatePinning = (left, right) => { - const leftHasUpdate = left.plugin?.has_update ? 1 : 0; - const rightHasUpdate = right.plugin?.has_update ? 1 : 0; - return rightHasUpdate - leftHasUpdate; - }; - const sortInstalledPlugins = (plugins) => { return plugins .map((plugin, index) => ({ plugin, index, - installedAtTimestamp: getInstalledAtTimestamp(plugin), })) - .sort((left, right) => { - if ( - pinUpdatesOnTop.value && - installedSortBy.value !== "update_status" - ) { - // Pinning updates is a primary grouping; the selected sort order still - // applies within the "has update" and "no update" groups below. - const pinCompare = compareInstalledUpdatePinning(left, right); - if (pinCompare !== 0) { - return pinCompare; - } - } - - if (installedSortBy.value === "install_time") { - const leftTimestamp = left.installedAtTimestamp; - const rightTimestamp = right.installedAtTimestamp; - - if (leftTimestamp == null && rightTimestamp == null) { - return compareInstalledFallback(left, right); - } - if (leftTimestamp == null) { - return 1; - } - if (rightTimestamp == null) { - return -1; - } - - const timeDiff = - installedSortOrder.value === "desc" - ? rightTimestamp - leftTimestamp - : leftTimestamp - rightTimestamp; - return timeDiff !== 0 - ? timeDiff - : compareInstalledFallback(left, right); - } - - if (installedSortBy.value === "name") { - const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin); - if (nameCompare !== 0) { - return installedSortOrder.value === "desc" - ? -nameCompare - : nameCompare; - } - return compareInstalledFallback(left, right); - } - - if (installedSortBy.value === "author") { - const authorCompare = compareInstalledPluginAuthors( - left.plugin, - right.plugin, - ); - if (authorCompare !== 0) { - return installedSortOrder.value === "desc" - ? -authorCompare - : authorCompare; - } - return compareInstalledFallback(left, right); - } - - if (installedSortBy.value === "update_status") { - const updateDiff = - installedSortOrder.value === "desc" - ? compareInstalledUpdatePinning(left, right) - : compareInstalledUpdatePinning(right, left); - return updateDiff !== 0 - ? updateDiff - : compareInstalledFallback(left, right); - } - - return compareInstalledFallback(left, right); - }) + .sort(compareInstalledFallback) .map((item) => item.plugin); }; // 通过搜索过滤插件 const filteredPlugins = computed(() => { - const plugins = filteredExtensions.value.filter((plugin) => { - if (installedStatusFilter.value === "enabled") { - return !!plugin.activated; - } - if (installedStatusFilter.value === "disabled") { - return !plugin.activated; - } - return true; - }); - const query = buildSearchQuery(pluginSearch.value); const filtered = query - ? plugins.filter((plugin) => matchesPluginSearch(plugin, query)) - : plugins; + ? filteredExtensions.value.filter((plugin) => + matchesPluginSearch(plugin, query), + ) + : filteredExtensions.value; return sortInstalledPlugins(filtered); }); @@ -658,12 +479,6 @@ export const useExtensionPage = () => { }); // 方法 - const toggleShowReserved = () => { - showReserved.value = !showReserved.value; - // 保存到 localStorage - writeBooleanPreference(SHOW_RESERVED_PLUGINS_STORAGE_KEY, showReserved.value); - }; - const toast = (message, success) => { snack_message.value = message; snack_show.value = true; @@ -922,7 +737,10 @@ export const useExtensionPage = () => { // 确认强制更新 // 显示更新全部插件确认对话框 const showUpdateAllConfirm = () => { - if (updatableExtensions.value.length === 0) return; + if (updatableExtensions.value.length === 0) { + toast(tm("messages.noUpdatesAvailable"), "info"); + return; + } updateAllConfirmDialog.show = true; }; @@ -945,7 +763,11 @@ export const useExtensionPage = () => { }; const updateAllExtensions = async () => { - if (updatingAll.value || updatableExtensions.value.length === 0) return; + if (updatingAll.value) return; + if (updatableExtensions.value.length === 0) { + toast(tm("messages.noUpdatesAvailable"), "info"); + return; + } updatingAll.value = true; loadingDialog.title = tm("status.loading"); loadingDialog.statusCode = 0; @@ -1624,11 +1446,6 @@ export const useExtensionPage = () => { }, 300); // 300ms 防抖延迟 }); - // 监听显示模式变化并保存到 localStorage - watch(isListView, (newVal) => { - writeBooleanPreference(PLUGIN_LIST_VIEW_MODE_STORAGE_KEY, newVal); - }); - watch( [() => dialog.value, () => extension_url.value, () => uploadTab.value], async ([dialogOpen, _, currentUploadTab]) => { @@ -1693,8 +1510,6 @@ export const useExtensionPage = () => { extractTabFromHash, syncTabFromHash, extension_data, - getInitialShowReserved, - showReserved, snack_message, snack_show, snack_success, @@ -1710,13 +1525,7 @@ export const useExtensionPage = () => { forceUpdateDialog, updateAllConfirmDialog, changelogDialog, - getInitialListViewMode, - isListView, pluginSearch, - installedStatusFilter, - installedSortBy, - installedSortOrder, - pinUpdatesOnTop, loading_, currentPage, marketCategoryFilter, @@ -1755,9 +1564,6 @@ export const useExtensionPage = () => { toPinyinText, toInitials, plugin_handler_info_headers, - installedSortItems, - installedSortUsesOrder, - pluginHeaders, filteredExtensions, filteredPlugins, filteredMarketPlugins, @@ -1772,7 +1578,6 @@ export const useExtensionPage = () => { totalPages, paginatedPlugins, updatableExtensions, - toggleShowReserved, toast, resetLoadingDialog, onLoadingDialogResult, diff --git a/dashboard/tests/extensionPreferenceStorage.test.mjs b/dashboard/tests/extensionPreferenceStorage.test.mjs deleted file mode 100644 index e864f5fae5..0000000000 --- a/dashboard/tests/extensionPreferenceStorage.test.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import test from 'node:test'; -import assert from 'node:assert/strict'; - -import { - PIN_UPDATES_ON_TOP_STORAGE_KEY, - readBooleanPreference, - writeBooleanPreference, -} from '../src/views/extension/extensionPreferenceStorage.mjs'; - -test("readBooleanPreference returns fallback when storage access throws", () => { - const storage = { - getItem() { - throw new Error("SecurityError"); - }, - }; - - assert.equal( - readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage), - true, - ); -}); - -test("readBooleanPreference parses stored boolean strings", () => { - const storage = { - getItem(key) { - return key === PIN_UPDATES_ON_TOP_STORAGE_KEY ? "false" : null; - }, - }; - - assert.equal( - readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage), - false, - ); -}); - -test("readBooleanPreference treats explicit null storage as unavailable", () => { - assert.equal( - readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, null), - true, - ); -}); - -test("readBooleanPreference treats invalid storage overrides as unavailable", () => { - assert.equal( - readBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, {}), - true, - ); -}); - -test("writeBooleanPreference stores boolean strings and swallows storage errors", () => { - const writes = []; - const storage = { - setItem(key, value) { - writes.push([key, value]); - throw new Error("QuotaExceededError"); - }, - }; - - assert.doesNotThrow(() => - writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, storage), - ); - assert.deepEqual(writes, [[PIN_UPDATES_ON_TOP_STORAGE_KEY, "true"]]); -}); - -test("writeBooleanPreference ignores explicit null storage", () => { - assert.doesNotThrow(() => - writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, null), - ); -}); - -test("writeBooleanPreference ignores invalid storage overrides", () => { - assert.doesNotThrow(() => - writeBooleanPreference(PIN_UPDATES_ON_TOP_STORAGE_KEY, true, {}), - ); -});
+ {{ pluginDesc }} +
+ {{ item.displayCommand }} +