diff --git a/src/main/frontend/components/plugins.cljs b/src/main/frontend/components/plugins.cljs index 09b912e15c..d7103426e4 100644 --- a/src/main/frontend/components/plugins.cljs +++ b/src/main/frontend/components/plugins.cljs @@ -26,6 +26,10 @@ (declare open-waiting-updates-modal!) (defonce PER-PAGE-SIZE 15) +(defonce DISABLED-PLUGINS-CLEANUP-THRESHOLD 100) +(defonce DISABLED-PLUGINS-CLEANUP-SNOOZE-MS (* 1000 60 60 24 30)) +(defonce DISABLED-PLUGINS-CLEANUP-NOTIFICATION-ID :lsp-disabled-plugins-cleanup-warning) +(defonce DISABLED-PLUGINS-CLEANUP-SNOOZED-AT-KEY :lsp-disabled-plugins-cleanup-snoozed-at) (def *dirties-toggle-items (atom {})) @@ -254,6 +258,15 @@ (t :plugin/installing)] (t :plugin/install)))]]]) +(defn- set-plugin-disabled! + [id disabled?] + (-> (js-invoke js/LSPluginCore (if disabled? "disable" "enable") id) + (p/then (fn [] + (when-let [^js settings (and disabled? + (some-> (plugin-handler/get-plugin-inst id) (.-settings)))] + (.set settings "disabled-since" (js/Date.now))))) + (p/catch #(js/console.error %)))) + (rum/defc card-ctls-of-installed < rum/static [id name url sponsors unpacked? disabled? installing-or-updating? has-other-pending? @@ -312,7 +325,7 @@ (ui/toggle (not disabled?) (fn [] - (js-invoke js/LSPluginCore (if disabled? "enable" "disable") id) + (set-plugin-disabled! id (not disabled?)) (when (nil? (get @*dirties-toggle-items (keyword id))) (swap! *dirties-toggle-items assoc (keyword id) (not disabled?)))) true)]]) @@ -594,6 +607,168 @@ [:span.pr-3.opacity-80 text] (ui/toggle enabled #() true)])) +(defn- disabled-plugin-sort-key + [{:keys [id name title settings]}] + (let [disabled-since (:disabled-since settings) + plugin-name (or title name id)] + [(if (number? disabled-since) 1 0) + (or disabled-since 0) + (util/safe-lower-case plugin-name) + id])) + +(defn- plugin-in-category? + [category plugin] + (case category + :all true + :plugins (not (:theme plugin)) + :themes (:theme plugin))) + +(defn- get-disabled-plugins-for-removal + [category] + (->> (vals (state/sub [:plugin/installed-plugins])) + (filter #(and (plugin-in-category? category %) + (get-in % [:settings :disabled]))) + (sort-by disabled-plugin-sort-key))) + +(defn- unregister-plugins-sequentially! + [plugin-ids] + (reduce + (fn [chain id] + (p/then chain + (fn [] + (p/let [_ (plugin-common-handler/unregister-plugin id)] + (when (util/electron?) + (plugin-config-handler/remove-plugin id)))))) + (.resolve js/Promise nil) + plugin-ids)) + +(rum/defc bulk-remove-disabled-plugins-container + [category] + (let [plugins (get-disabled-plugins-for-removal category) + plugin-ids (mapv :id plugins) + [selected-ids set-selected-ids!] (rum/use-state (set (take 20 plugin-ids))) + [pending? set-pending!] (rum/use-state false) + selected-plugin-ids (->> plugins + (map :id) + (filter selected-ids) + vec) + all-selected? (and (seq plugin-ids) + (= (count selected-plugin-ids) (count plugin-ids))) + toggle-selected! (fn [id checked?] + (set-selected-ids! + ((if checked? conj disj) selected-ids id))) + remove-selected! (fn [] + (when (and (seq selected-plugin-ids) + (not pending?)) + (-> (shui/dialog-confirm! + [:b (t :plugin/bulk-remove-disabled-delete-alert (count selected-plugin-ids))] + {:cancel-label (t :ui/cancel) + :ok-label (t :ui/delete)}) + (p/then (fn [] + (set-pending! true) + (-> (unregister-plugins-sequentially! selected-plugin-ids) + (p/then (fn [] + (notification/show! + (t :plugin/bulk-remove-disabled-success (count selected-plugin-ids)) + :success) + (shui/dialog-close!))) + (p/catch (fn [e] + (notification/show! (str e) :error))) + (p/finally #(set-pending! false))))))))] + [:div.p-4.flex.flex-col.gap-3 + [:h1.text-xl.font-bold (t :plugin/bulk-remove-disabled-title)] + (if (seq plugins) + [:<> + [:p.opacity-70.text-sm (t :plugin/bulk-remove-disabled-desc)] + [:ul.max-h-96.overflow-y-auto.flex.flex-col.gap-2.ml-0 + (for [{:keys [id name title version icon]} plugins + :let [selected? (contains? selected-ids id)]] + [:li.flex.items-center.gap-3.rounded-md.border.p-2.select-none + {:key id + :class (str "cursor-pointer " + (if selected? + "border-primary bg-base-3" + "border-transparent bg-base-2 hover:bg-base-3")) + :on-click #(when-not pending? + (toggle-selected! id (not selected?)))} + (shui/checkbox + {:checked selected? + :disabled pending? + :on-click #(.stopPropagation %) + :on-checked-change #(toggle-selected! id (true? %))}) + [:span.flex.h-10.w-10.shrink-0.items-center.justify-center.overflow-hidden.rounded.bg-base-3 + (if (and icon (not (string/blank? icon))) + [:img {:src icon + :class "h-full w-full object-contain"}] + [:span.flex.h-6.w-6.items-center.justify-center.overflow-hidden + svg/folder])] + [:div.flex-1.overflow-hidden + [:div.font-medium.truncate (or title name id)] + [:div.text-xs.opacity-60.truncate (str "ID: " id)]] + (when version + [:small.opacity-50.shrink-0 version])])] + [:div.flex.items-center.justify-between.gap-2 + [:div.flex.gap-2 + (shui/button {:variant :ghost + :disabled (or pending? all-selected?) + :on-click #(set-selected-ids! (set plugin-ids))} + (t :plugin/bulk-remove-disabled-select-all)) + (shui/button {:variant :ghost + :disabled (or pending? (empty? selected-plugin-ids)) + :on-click #(set-selected-ids! #{})} + (t :plugin/bulk-remove-disabled-clear-selection))] + [:div.flex.gap-2 + (shui/button {:variant :ghost + :disabled pending? + :on-click #(shui/dialog-close!)} + (t :ui/cancel)) + (shui/button {:disabled (or pending? (empty? selected-plugin-ids)) + :on-click remove-selected!} + (if pending? + (ui/loading (t :plugin/uninstall)) + (t :plugin/bulk-remove-disabled-confirm (count selected-plugin-ids))))]]] + [:div.flex.items-center.justify-center.py-8.opacity-50 + (t :plugin/bulk-remove-disabled-empty)])])) + +(defn- disabled-plugins-cleanup-snoozed? + [] + (let [snoozed-at (storage/get DISABLED-PLUGINS-CLEANUP-SNOOZED-AT-KEY)] + (and (number? snoozed-at) + (< (- (js/Date.now) snoozed-at) DISABLED-PLUGINS-CLEANUP-SNOOZE-MS)))) + +(defn- open-bulk-remove-disabled-plugins-dialog! + [category] + (notification/clear! DISABLED-PLUGINS-CLEANUP-NOTIFICATION-ID) + (shui/dialog-open! + (fn [] + (bulk-remove-disabled-plugins-container category)))) + +(defn- snooze-disabled-plugins-cleanup-warning! + [] + (storage/set DISABLED-PLUGINS-CLEANUP-SNOOZED-AT-KEY (js/Date.now)) + (notification/clear! DISABLED-PLUGINS-CLEANUP-NOTIFICATION-ID)) + +(defn- show-disabled-plugins-cleanup-warning! + [] + (let [disabled-count (count (get-disabled-plugins-for-removal :all))] + (when (and (>= disabled-count DISABLED-PLUGINS-CLEANUP-THRESHOLD) + (not (disabled-plugins-cleanup-snoozed?))) + (notification/show! + [:div.flex.flex-col.gap-2 + [:div (t :plugin/disabled-cleanup-warning-title disabled-count)] + [:div.opacity-70 (t :plugin/disabled-cleanup-warning-desc)] + [:div.flex.gap-2.pt-1 + (ui/button (t :plugin/disabled-cleanup-warning-clean-now) + :small? true + :on-click #(open-bulk-remove-disabled-plugins-dialog! :all)) + (ui/button (t :plugin/disabled-cleanup-warning-later) + :small? true + :variant :ghost + :on-click snooze-disabled-plugins-cleanup-warning!)]] + :warning + false + DISABLED-PLUGINS-CLEANUP-NOTIFICATION-ID)))) + (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static [search-key *search-key category *category sort-by *sort-by filter-by *filter-by total-nums @@ -714,8 +889,12 @@ (let [items (concat (if market? [{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)] :options {:on-click #(reload-market-fn)}}] - [{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)] - :options {:on-click #(plugin-handler/user-check-enabled-for-updates! (not= :plugins category))}}]) + (concat + [{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)] + :options {:on-click #(plugin-handler/user-check-enabled-for-updates! (not= :plugins category))}}] + (when (contains? #{:plugins :themes} category) + [{:title [:span.flex.items-center.gap-1 (ui/icon "trash") (t :plugin/bulk-remove-disabled)] + :options {:on-click #(open-bulk-remove-disabled-plugins-dialog! category)}}]))) (when (util/electron?) [{:title [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings.advanced/network-proxy)] @@ -1438,6 +1617,7 @@ (rum/defc updates-notifications-impl [check-pending? auto-checking? online?] (let [[uid, set-uid] (rum/use-state nil) + [cleanup-warning-pending? set-cleanup-warning-pending?!] (rum/use-state false) [sub-content, _set-sub-content!] (rum-utils/use-atom *updates-sub-content) notify! (fn [content status] (if auto-checking? @@ -1459,22 +1639,38 @@ (when uid (notification/clear! uid)))) [check-pending? sub-content]) + (hooks/use-effect! + (fn [] + (when (and cleanup-warning-pending? + (not auto-checking?)) + (set-cleanup-warning-pending?! false) + (show-disabled-plugins-cleanup-warning!))) + [cleanup-warning-pending? auto-checking?]) + (hooks/use-effect! ;; scheduler for auto updates (fn [] (when online? - (let [last-updates (storage/get :lsp-last-auto-updates)] - (when (and (not (false? last-updates)) - (or (true? last-updates) - (not (number? last-updates)) - ;; interval 12 hours - (> (- (js/Date.now) last-updates) (* 60 60 12 1000)))) - (let [update-timer (js/setTimeout - (fn [] - (plugin-handler/auto-check-enabled-for-updates!) - (storage/set :lsp-last-auto-updates (js/Date.now))) - (if (util/electron?) 3000 (* 60 1000)))] - #(js/clearTimeout update-timer)))))) + (let [auto-update-delay (if (util/electron?) 3000 (* 60 1000)) + last-updates (storage/get :lsp-last-auto-updates) + should-auto-update? (and (not (false? last-updates)) + (or (true? last-updates) + (not (number? last-updates)) + ;; interval 12 hours + (> (- (js/Date.now) last-updates) (* 60 60 12 1000)))) + cleanup-warning-timer (when-not should-auto-update? + (js/setTimeout #(set-cleanup-warning-pending?! true) + (+ auto-update-delay 1000))) + update-timer (when should-auto-update? + (js/setTimeout + (fn [] + (plugin-handler/auto-check-enabled-for-updates!) + (storage/set :lsp-last-auto-updates (js/Date.now)) + (set-cleanup-warning-pending?! true)) + auto-update-delay))] + #(do + (some-> update-timer (js/clearTimeout)) + (some-> cleanup-warning-timer (js/clearTimeout)))))) [online?]) [:<>])) diff --git a/src/main/frontend/handler/plugin.cljs b/src/main/frontend/handler/plugin.cljs index 1f7871fb96..c780a3b193 100644 --- a/src/main/frontend/handler/plugin.cljs +++ b/src/main/frontend/handler/plugin.cljs @@ -1126,6 +1126,17 @@ :remove disj)] (save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))})))) +(defn- remove-pinned-toolbar-items-of-plugin! + [pid] + (let [prefix (str (name pid) ":") + pinned (state/sub [:plugin/preferences :pinnedToolbarItems]) + pinned (if (sequential? pinned) (vec pinned) []) + updated-pinned (->> pinned + (remove #(and (string? %) (string/starts-with? % prefix))) + vec)] + (when (not= pinned updated-pinned) + (save-plugin-preferences! {:pinnedToolbarItems updated-pinned})))) + (defn hook-lifecycle-fn! [type f & args] (when (and type (fn? f)) @@ -1232,6 +1243,7 @@ (let [pid (keyword pid)] ;; effects (unregister-plugin-themes pid) + (remove-pinned-toolbar-items-of-plugin! pid) ;; plugins (swap! state/state medley/dissoc-in [:plugin/installed-plugins pid]) ;; commands diff --git a/src/resources/dicts/en.edn b/src/resources/dicts/en.edn index fc3e19a9b2..f75c2d4c2b 100644 --- a/src/resources/dicts/en.edn +++ b/src/resources/dicts/en.edn @@ -1148,6 +1148,15 @@ :plugin/all "All" :plugin/auto-update-check "Auto check for updates" :plugin/auto-update-check-feedback "Auto check for updates: {1}!" + :plugin/bulk-remove-disabled "Remove disabled plugins and themes" + :plugin/bulk-remove-disabled-clear-selection "Clear selection" + :plugin/bulk-remove-disabled-confirm "Remove selected ({1})" + :plugin/bulk-remove-disabled-delete-alert "Are you sure you want to remove the selected disabled plugins and themes? ({1})" + :plugin/bulk-remove-disabled-desc "Items with an unknown or earlier disabled time are listed first. Select the disabled plugins and themes you want to remove." + :plugin/bulk-remove-disabled-empty "No disabled plugins or themes found." + :plugin/bulk-remove-disabled-select-all "Select all" + :plugin/bulk-remove-disabled-success "Removed disabled plugins and themes: {1}" + :plugin/bulk-remove-disabled-title "Bulk remove disabled plugins and themes" :plugin/check-all-updates "Check all updates" :plugin/check-update "Check update" :plugin/checked "Checked" @@ -1159,6 +1168,10 @@ :plugin/disable-for-performance-feedback "The plugin {1} is disabled." :plugin/disable-now "Disable now" :plugin/disabled "Disabled" + :plugin/disabled-cleanup-warning-clean-now "Clean up now" + :plugin/disabled-cleanup-warning-desc "Too many disabled plugins and themes can make plugin management harder. We recommend removing the ones you no longer need." + :plugin/disabled-cleanup-warning-later "Later" + :plugin/disabled-cleanup-warning-title "You have {1} disabled plugins and themes." :plugin/does-not-support-db "Does not support DB graphs" :plugin/downloads "Downloads" :plugin/empty "Nothing Found." diff --git a/src/resources/dicts/ja.edn b/src/resources/dicts/ja.edn index cd83cbf92b..ce1a705142 100644 --- a/src/resources/dicts/ja.edn +++ b/src/resources/dicts/ja.edn @@ -1136,6 +1136,15 @@ :plugin/all "全て" :plugin/auto-update-check "プラグインの更新を自動チェック" :plugin/auto-update-check-feedback "自動更新チェック: {1}!" + :plugin/bulk-remove-disabled "無効化されたプラグインとテーマを一括削除" + :plugin/bulk-remove-disabled-clear-selection "選択をクリア" + :plugin/bulk-remove-disabled-confirm "選択項目を削除({1})" + :plugin/bulk-remove-disabled-delete-alert "選択した無効化済みプラグインとテーマを削除してもよろしいですか?({1})" + :plugin/bulk-remove-disabled-desc "無効化された日時が不明、または古い項目が先に表示されます。削除する無効化済みプラグインとテーマを選択してください。" + :plugin/bulk-remove-disabled-empty "無効化されたプラグインまたはテーマは見つかりませんでした。" + :plugin/bulk-remove-disabled-select-all "すべて選択" + :plugin/bulk-remove-disabled-success "無効化されたプラグインとテーマを削除しました: {1}" + :plugin/bulk-remove-disabled-title "無効化されたプラグインとテーマを一括削除" :plugin/check-all-updates "全ての更新を確認" :plugin/check-update "更新を確認" :plugin/checked "確認済み" @@ -1147,6 +1156,10 @@ :plugin/disable-for-performance-feedback "プラグイン{1}は無効化されました。" :plugin/disable-now "今すぐ無効化" :plugin/disabled "無効化" + :plugin/disabled-cleanup-warning-clean-now "今すぐ整理" + :plugin/disabled-cleanup-warning-desc "無効化されたプラグインとテーマが多すぎると、プラグイン管理が難しくなります。不要なものを削除することをおすすめします。" + :plugin/disabled-cleanup-warning-later "後で" + :plugin/disabled-cleanup-warning-title "無効化されたプラグインとテーマが {1} 個あります。" :plugin/does-not-support-db "DBグラフはサポートされていません" :plugin/downloads "ダウンロード" :plugin/empty "見つかりませんでした。" diff --git a/src/resources/dicts/zh-cn.edn b/src/resources/dicts/zh-cn.edn index bf06afc0ff..618297bdae 100644 --- a/src/resources/dicts/zh-cn.edn +++ b/src/resources/dicts/zh-cn.edn @@ -1145,6 +1145,15 @@ :plugin/all "全部" :plugin/auto-update-check "是否自动检查更新" :plugin/auto-update-check-feedback "自动检查更新:{1}!" + :plugin/bulk-remove-disabled "批量删除未启用的插件和主题" + :plugin/bulk-remove-disabled-clear-selection "清空选择" + :plugin/bulk-remove-disabled-confirm "删除已选({1})" + :plugin/bulk-remove-disabled-delete-alert "确定要删除选中的插件和主题吗?({1})" + :plugin/bulk-remove-disabled-desc "禁用时间未知或更早的项目会排在前面。请选择要删除的插件和主题。" + :plugin/bulk-remove-disabled-empty "没有找到未启用的插件或主题。" + :plugin/bulk-remove-disabled-select-all "全选" + :plugin/bulk-remove-disabled-success "已删除未启用插件和主题:{1}" + :plugin/bulk-remove-disabled-title "批量删除未启用插件和主题" :plugin/check-all-updates "一键检查更新" :plugin/check-update "检查更新" :plugin/checked "已检查" @@ -1156,6 +1165,10 @@ :plugin/disable-for-performance-feedback "插件 {1} 已被禁用。" :plugin/disable-now "立即禁用" :plugin/disabled "未开启" + :plugin/disabled-cleanup-warning-clean-now "现在就去" + :plugin/disabled-cleanup-warning-desc "过多 disabled 插件和主题会让插件管理变得困难。建议清理不再需要的项目。" + :plugin/disabled-cleanup-warning-later "以后再说" + :plugin/disabled-cleanup-warning-title "你有 {1} 个 disabled 插件和主题。" :plugin/does-not-support-db "不支持 DB 知识库" :plugin/downloads "下载量" :plugin/empty "未找到任何内容。" diff --git a/src/resources/dicts/zh-hant.edn b/src/resources/dicts/zh-hant.edn index d132c42120..7d54a59621 100644 --- a/src/resources/dicts/zh-hant.edn +++ b/src/resources/dicts/zh-hant.edn @@ -1136,6 +1136,15 @@ :plugin/all "所有" :plugin/auto-update-check "是否自動檢查更新" :plugin/auto-update-check-feedback "自動檢查更新: {1}!" + :plugin/bulk-remove-disabled "批次刪除已停用外掛和主題" + :plugin/bulk-remove-disabled-clear-selection "清除選取" + :plugin/bulk-remove-disabled-confirm "刪除已選項目({1})" + :plugin/bulk-remove-disabled-delete-alert "確定要刪除選取的已停用外掛和主題嗎?({1})" + :plugin/bulk-remove-disabled-desc "停用時間未知或較早的項目會排在前面。請選擇要刪除的已停用外掛和主題。" + :plugin/bulk-remove-disabled-empty "找不到已停用的外掛或主題。" + :plugin/bulk-remove-disabled-select-all "全選" + :plugin/bulk-remove-disabled-success "已刪除已停用外掛和主題:{1}" + :plugin/bulk-remove-disabled-title "批次刪除已停用外掛和主題" :plugin/check-all-updates "確認所有更新" :plugin/check-update "確認更新" :plugin/checked "已檢查" @@ -1147,6 +1156,10 @@ :plugin/disable-for-performance-feedback "外掛程式 {1} 已停用。" :plugin/disable-now "立即停用" :plugin/disabled "已關閉" + :plugin/disabled-cleanup-warning-clean-now "現在就去" + :plugin/disabled-cleanup-warning-desc "過多已停用外掛和主題會讓外掛管理變得困難。建議清理不再需要的項目。" + :plugin/disabled-cleanup-warning-later "以後再說" + :plugin/disabled-cleanup-warning-title "你有 {1} 個已停用外掛和主題。" :plugin/does-not-support-db "不支援 DB 圖譜" :plugin/downloads "下載" :plugin/empty "找不到任何項目。"