您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
You can set the Storage name to be applied in the userscript menu. If not set or left blank, it will not intervene. Change @match pattern for your enviroment.
// ==UserScript== // @name Proxmox VE 7: Auto-select Backup Storage (panel-scope, configurable) // @namespace hollen9.com // @version 1.0 // @description You can set the Storage name to be applied in the userscript menu. If not set or left blank, it will not intervene. Change @match pattern for your enviroment. // @match https://node1.example.com:8006/* // @match https://node2.example.com:8006/* // @run-at document-idle // @grant unsafeWindow // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; const SCAN_MS = 700; const getExt = () => unsafeWindow?.Ext || window.Ext; // 設定以「主機為範圍」:每個 host 可設定不同預設 Storage const SCOPE_KEY = `pve:autoStorage:host:${location.host}`; // 讀寫目標 Storage 名稱(空字串或未設定 = 不介入) const getTarget = () => (GM_getValue(SCOPE_KEY, '') || '').trim(); const setTarget = (val) => GM_setValue(SCOPE_KEY, (val || '').trim()); // ===== userscript addon 功能表 ===== GM_registerMenuCommand('Set default Backup Storage (this host)', async () => { try { const current = getTarget(); // 嘗試抓取目前畫面上 StorageSelector 的可選項目,方便提示 const suggestions = collectStorageNames(); const hint = suggestions.length ? `\n\nDetected storages:\n- ${suggestions.join('\n- ')}` : ''; const v = prompt( `Enter the Storage name to auto-select on Backup tab for host:\n${location.host}\n` + `(leave blank to disable)\n\nCurrent: ${current || '(disabled)'}${hint}`, current ); if (v === null) return; // cancel setTarget(v); alert(v.trim() ? `Set to "${v.trim()}" for ${location.host}` : `Disabled for ${location.host}`); } catch (e) { alert('Failed to set value: ' + e); } }); GM_registerMenuCommand('Clear default (disable for this host)', () => { setTarget(''); alert(`Disabled for ${location.host}`); }); GM_registerMenuCommand('Show current setting (this host)', () => { const cur = getTarget(); alert(`Host: ${location.host}\nCurrent default storage: ${cur || '(disabled)'}`); }); // 嘗試收集目前頁面上能看到的 Storage 名稱(僅用於提示) function collectStorageNames() { const Ext = getExt(); if (!Ext?.ComponentQuery) return []; const list = [] .concat(Ext.ComponentQuery.query('pveStorageSelector')) .concat(Ext.ComponentQuery.query('PVE\\\.form\\\.StorageSelector')) .concat(Ext.ComponentQuery.query('combo[name=storage]')); const names = new Set(); list.forEach(c => { try { const store = c.getStore?.() || c.store; const valueField = c.valueField || 'storage'; store?.each?.(rec => { const v = rec.get?.(valueField); if (typeof v === 'string' && v) names.add(v); }); } catch (_) {} }); return [...names].sort(); } // ===== Backup 分頁偵測 & 套用 ===== function findBackupPanelOf(cmp) { if (!cmp?.up) return null; try { let p = cmp.up('panel[itemId=backup]'); if (p) return p; p = cmp.up('panel[title=Backup]'); if (p?.isVisible?.()) return p; } catch (_) {} return null; } function findVisibleStorageSelectors() { const Ext = getExt(); if (!Ext?.ComponentQuery) return []; let list = [] .concat(Ext.ComponentQuery.query('pveStorageSelector')) .concat(Ext.ComponentQuery.query('PVE\\\.form\\\.StorageSelector')) .concat(Ext.ComponentQuery.query('combo[name=storage]')); const seen = new Set(); return list.filter(c => { if (!c) return false; const id = c.getId?.() || c.$className + Math.random(); if (seen.has(id)) return false; seen.add(id); if (!c.isVisible?.() || c.disabled || c.readOnly) return false; if (c.$className === 'PVE.form.StorageSelector') return true; if (typeof c.fieldLabel === 'string' && c.fieldLabel.trim() === 'Storage') return true; return false; }); } function applyOnceInBackupPanel() { const TARGET_STORAGE = getTarget(); // 每次 loop 都讀,允許隨時修改設定 if (!TARGET_STORAGE) return; // 未設定:不介入 const selectors = findVisibleStorageSelectors(); if (!selectors.length) return; selectors.forEach(combo => { const panel = findBackupPanelOf(combo); if (!panel) return; // 只在 Backup 分頁內動作 if (panel.__pve_applied_once) return; if (panel.__pve_user_modified) return; const store = combo.getStore?.() || combo.store; if (!store) return; const doSet = () => { const valueField = combo.valueField || 'storage'; const rec = store.findRecord ? store.findRecord(valueField, TARGET_STORAGE, 0, false, true, true) : null; if (!rec) return; // 清單裡沒有該名稱 -> 不介入 const cur = combo.getValue?.(); if (cur !== TARGET_STORAGE) { combo.setValue(TARGET_STORAGE); } panel.__pve_applied_once = true; if (!combo.__bind_change) { combo.__bind_change = true; combo.on?.('change', () => { panel.__pve_user_modified = true; }, { single: false }); } }; if (typeof store.isLoaded === 'function' && !store.isLoaded()) { store.on?.('load', () => setTimeout(doSet, 0), { single: true }); } else { doSet(); } }); } function loop() { const Ext = getExt(); if (!Ext) return; applyOnceInBackupPanel(); } setInterval(loop, SCAN_MS); document.addEventListener('visibilitychange', loop); window.addEventListener('focus', loop); loop(); })();