mirror of https://github.com/logseq/logseq
feat(marketplace): download updates with one click
parent
9a7698c966
commit
de1ea231e3
|
@ -39,7 +39,8 @@
|
|||
zipball
|
||||
(api "zipball"))
|
||||
asset)
|
||||
version])
|
||||
version
|
||||
(:body res)])
|
||||
|
||||
(fn [^js e]
|
||||
(emit :lsp-installed {:status :error :payload e})
|
||||
|
@ -130,7 +131,7 @@
|
|||
(fn [resolve _reject]
|
||||
;;(reset! *installing-or-updating item)
|
||||
;; get releases
|
||||
(-> (p/let [[asset latest-version] (fetch-latest-release-asset item)
|
||||
(-> (p/let [[asset latest-version notes] (fetch-latest-release-asset item)
|
||||
|
||||
_ (debug "[Release Asset] #" latest-version " =>" (:url asset))
|
||||
|
||||
|
@ -160,7 +161,7 @@
|
|||
{:status :completed
|
||||
:only-check only-check
|
||||
:payload (if only-check
|
||||
(assoc item :latest-version latest-version)
|
||||
(assoc item :latest-version latest-version :latest-notes notes)
|
||||
(assoc item :zip dl-url :dst dest))})
|
||||
|
||||
(resolve nil))
|
||||
|
|
|
@ -576,6 +576,69 @@
|
|||
(and updating (= (keyword (:id updating)) pid))
|
||||
true nil (get coming-updates pid))) (:id item)))]])))
|
||||
|
||||
(rum/defcs waiting-coming-updates
|
||||
< rum/reactive
|
||||
{:will-mount (fn [s] (state/reset-unchecked-update) s)}
|
||||
[_s]
|
||||
(let [_ (state/sub :plugin/updates-coming)
|
||||
downloading? (state/sub :plugin/updates-downloading?)
|
||||
unchecked (state/sub :plugin/updates-unchecked)
|
||||
updates (state/all-available-coming-updates)]
|
||||
|
||||
[:div.cp__plugins-waiting-updates
|
||||
[:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (util/safe-parse-int (count updates)))]
|
||||
|
||||
(if (seq updates)
|
||||
;; lists
|
||||
[:ul
|
||||
{:class (when downloading? "downloading")}
|
||||
(for [it updates
|
||||
:let [k (str "lsp-it-" (:id it))
|
||||
c? (not (contains? unchecked (:id it)))
|
||||
notes (util/trim-safe (:latest-notes it))]]
|
||||
[:li.flex.items-center
|
||||
{:key k
|
||||
:class (when c? "checked")}
|
||||
|
||||
[:label.flex-1
|
||||
{:for k}
|
||||
(ui/checkbox {:id k
|
||||
:checked c?
|
||||
:on-change (fn [^js e]
|
||||
(when-not downloading?
|
||||
(state/set-unchecked-update (:id it) (not (util/echecked? e)))))})
|
||||
[:strong.px-3 (:title it)
|
||||
[:sup (str (:version it) " 👉 " (:latest-version it))]]]
|
||||
|
||||
[:div.px-4
|
||||
(when-not (string/blank? notes)
|
||||
(ui/tippy
|
||||
{:html [:p notes]}
|
||||
[:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
|
||||
|
||||
;; all done
|
||||
[:div.py-4 [:strong.text-4xl "\uD83C\uDF89 All updated!"]])
|
||||
|
||||
;; actions
|
||||
(when (seq updates)
|
||||
[:div.pt-5
|
||||
(ui/button
|
||||
(if downloading?
|
||||
[:span (ui/loading " Downloading...")]
|
||||
[:span "Update all of selected"])
|
||||
|
||||
:on-click
|
||||
#(when-not downloading?
|
||||
(state/set-state! :plugin/updates-downloading? true)
|
||||
(plugin-handler/check-or-update-marketplace-plugin
|
||||
(assoc (state/get-next-selected-coming-update) :only-check false)
|
||||
(fn [^js e] (notification/show! e :error))))
|
||||
|
||||
:disabled
|
||||
(or downloading?
|
||||
(and (not (empty? unchecked))
|
||||
(= (count unchecked) (count updates)))))])]))
|
||||
|
||||
(defn open-select-theme!
|
||||
[]
|
||||
(state/set-sub-modal! installed-themes))
|
||||
|
@ -677,5 +740,12 @@
|
|||
(defn open-plugins-modal!
|
||||
[]
|
||||
(state/set-modal!
|
||||
(fn [close!]
|
||||
(plugins-page))))
|
||||
(fn [_close!]
|
||||
(plugins-page))))
|
||||
|
||||
(defn open-waiting-updates-modal!
|
||||
[]
|
||||
(state/set-sub-modal!
|
||||
(fn [_close!]
|
||||
(waiting-coming-updates))
|
||||
{:center? true}))
|
|
@ -425,6 +425,27 @@
|
|||
|
||||
&-details {
|
||||
}
|
||||
|
||||
&-waiting-updates {
|
||||
margin: -15px;
|
||||
|
||||
> ul {
|
||||
li {
|
||||
user-select: none;
|
||||
justify-content: space-between;
|
||||
opacity: .9;
|
||||
|
||||
sup {
|
||||
padding-left: 8px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&:hover, &.checked {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cp__themes {
|
||||
|
|
|
@ -211,6 +211,10 @@
|
|||
(defmethod handle :go/plugins [_]
|
||||
(plugin/open-plugins-modal!))
|
||||
|
||||
(defmethod handle :go/plugins-waiting-lists [_]
|
||||
(plugin/open-waiting-updates-modal!))
|
||||
|
||||
|
||||
(defmethod handle :redirect-to-home [_]
|
||||
(page-handler/create-today-journal!))
|
||||
|
||||
|
@ -241,11 +245,22 @@
|
|||
(str "Checked: " (:title coming))
|
||||
:success))
|
||||
|
||||
;; try to start consume pending item
|
||||
(when-let [n (second (first (:plugin/updates-pending @state/state)))]
|
||||
(plugin-handler/check-or-update-marketplace-plugin
|
||||
(assoc n :only-check true)
|
||||
(fn [^js e] (js/console.error "[Check Err]" n e)))))
|
||||
(if (and updated? (:plugin/updates-downloading? @state/state))
|
||||
;; try to start consume downloading item
|
||||
(if-let [n (state/get-next-selected-coming-update)]
|
||||
(plugin-handler/check-or-update-marketplace-plugin
|
||||
(assoc n :only-check false)
|
||||
(fn [^js e] (js/console.error "[Download Err]" n e)))
|
||||
(state/set-state! :plugin/updates-downloading? false))
|
||||
|
||||
;; try to start consume pending item
|
||||
(if-let [n (second (first (:plugin/updates-pending @state/state)))]
|
||||
(plugin-handler/check-or-update-marketplace-plugin
|
||||
(assoc n :only-check true)
|
||||
(fn [^js e] (js/console.error "[Check Err]" n e)))
|
||||
;; try to open waiting updates list
|
||||
(when (and pending? (seq (state/all-available-coming-updates)))
|
||||
(plugin/open-waiting-updates-modal!)))))
|
||||
|
||||
(defn run!
|
||||
[]
|
||||
|
|
|
@ -159,6 +159,8 @@
|
|||
|
||||
(if (and only-check updates-pending?)
|
||||
(state/consume-updates-coming-plugin payload false)
|
||||
|
||||
;; TODO: consume failed download updates?
|
||||
(notifications/show!
|
||||
(str
|
||||
(if (= :error type) "[Install Error]" "")
|
||||
|
|
|
@ -21,193 +21,195 @@
|
|||
[cljs.cache :as cache]))
|
||||
|
||||
(defonce state
|
||||
(let [document-mode? (or (storage/get :document/mode?) false)
|
||||
current-graph (let [graph (storage/get :git/current-repo)]
|
||||
(when graph (ipc/ipc "setCurrentGraph" graph))
|
||||
graph)]
|
||||
(atom
|
||||
{:route-match nil
|
||||
:today nil
|
||||
:system/events (async/chan 100)
|
||||
:db/batch-txs (async/chan 100)
|
||||
:file/writes (async/chan 100)
|
||||
:notification/show? false
|
||||
:notification/content nil
|
||||
:repo/cloning? false
|
||||
:repo/loading-files? {}
|
||||
:repo/importing-to-db? nil
|
||||
:repo/sync-status {}
|
||||
:repo/changed-files nil
|
||||
:nfs/user-granted? {}
|
||||
:nfs/refreshing? nil
|
||||
:instrument/disabled? (storage/get "instrument-disabled")
|
||||
;; TODO: how to detect the network reliably?
|
||||
:network/online? true
|
||||
:indexeddb/support? true
|
||||
:me nil
|
||||
:git/current-repo current-graph
|
||||
:git/status {}
|
||||
:format/loading {}
|
||||
:draw? false
|
||||
:db/restoring? nil
|
||||
(let [document-mode? (or (storage/get :document/mode?) false)
|
||||
current-graph (let [graph (storage/get :git/current-repo)]
|
||||
(when graph (ipc/ipc "setCurrentGraph" graph))
|
||||
graph)]
|
||||
(atom
|
||||
{:route-match nil
|
||||
:today nil
|
||||
:system/events (async/chan 100)
|
||||
:db/batch-txs (async/chan 100)
|
||||
:file/writes (async/chan 100)
|
||||
:notification/show? false
|
||||
:notification/content nil
|
||||
:repo/cloning? false
|
||||
:repo/loading-files? {}
|
||||
:repo/importing-to-db? nil
|
||||
:repo/sync-status {}
|
||||
:repo/changed-files nil
|
||||
:nfs/user-granted? {}
|
||||
:nfs/refreshing? nil
|
||||
:instrument/disabled? (storage/get "instrument-disabled")
|
||||
;; TODO: how to detect the network reliably?
|
||||
:network/online? true
|
||||
:indexeddb/support? true
|
||||
:me nil
|
||||
:git/current-repo current-graph
|
||||
:git/status {}
|
||||
:format/loading {}
|
||||
:draw? false
|
||||
:db/restoring? nil
|
||||
|
||||
:journals-length 2
|
||||
:journals-length 2
|
||||
|
||||
:search/q ""
|
||||
:search/mode :global
|
||||
:search/result nil
|
||||
:search/graph-filters []
|
||||
:search/q ""
|
||||
:search/mode :global
|
||||
:search/result nil
|
||||
:search/graph-filters []
|
||||
|
||||
;; modals
|
||||
:modal/label ""
|
||||
:modal/show? false
|
||||
:modal/panel-content nil
|
||||
:modal/fullscreen? false
|
||||
:modal/close-btn? nil
|
||||
:modal/subsets []
|
||||
;; modals
|
||||
:modal/label ""
|
||||
:modal/show? false
|
||||
:modal/panel-content nil
|
||||
:modal/fullscreen? false
|
||||
:modal/close-btn? nil
|
||||
:modal/subsets []
|
||||
|
||||
;; right sidebar
|
||||
:ui/fullscreen? false
|
||||
:ui/settings-open? false
|
||||
:ui/sidebar-open? false
|
||||
:ui/left-sidebar-open? (boolean (storage/get "ls-left-sidebar-open?"))
|
||||
:ui/theme (or (storage/get :ui/theme) (if (mobile/is-native-platform?) "light" "dark"))
|
||||
:ui/system-theme? ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
|
||||
:ui/wide-mode? false
|
||||
;; :show-all, :hide-block-body, :hide-block-children
|
||||
:ui/cycle-collapse :show-all
|
||||
;; right sidebar
|
||||
:ui/fullscreen? false
|
||||
:ui/settings-open? false
|
||||
:ui/sidebar-open? false
|
||||
:ui/left-sidebar-open? (boolean (storage/get "ls-left-sidebar-open?"))
|
||||
:ui/theme (or (storage/get :ui/theme) (if (mobile/is-native-platform?) "light" "dark"))
|
||||
:ui/system-theme? ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
|
||||
:ui/wide-mode? false
|
||||
;; :show-all, :hide-block-body, :hide-block-children
|
||||
:ui/cycle-collapse :show-all
|
||||
|
||||
;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
|
||||
;; 1. right sidebar
|
||||
;; 2. zoom-in view
|
||||
;; 3. queries
|
||||
;; 4. references
|
||||
;; graph => {:block-id bool}
|
||||
:ui/collapsed-blocks {}
|
||||
:ui/sidebar-collapsed-blocks {}
|
||||
:ui/root-component nil
|
||||
:ui/file-component nil
|
||||
:ui/custom-query-components {}
|
||||
:ui/show-recent? false
|
||||
:ui/command-palette-open? false
|
||||
:ui/developer-mode? (or (= (storage/get "developer-mode") "true")
|
||||
false)
|
||||
;; remember scroll positions of visited paths
|
||||
:ui/paths-scroll-positions {}
|
||||
:ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
|
||||
false
|
||||
true)
|
||||
:ui/visual-viewport-pending? false
|
||||
:ui/visual-viewport-state nil
|
||||
;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
|
||||
;; 1. right sidebar
|
||||
;; 2. zoom-in view
|
||||
;; 3. queries
|
||||
;; 4. references
|
||||
;; graph => {:block-id bool}
|
||||
:ui/collapsed-blocks {}
|
||||
:ui/sidebar-collapsed-blocks {}
|
||||
:ui/root-component nil
|
||||
:ui/file-component nil
|
||||
:ui/custom-query-components {}
|
||||
:ui/show-recent? false
|
||||
:ui/command-palette-open? false
|
||||
:ui/developer-mode? (or (= (storage/get "developer-mode") "true")
|
||||
false)
|
||||
;; remember scroll positions of visited paths
|
||||
:ui/paths-scroll-positions {}
|
||||
:ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
|
||||
false
|
||||
true)
|
||||
:ui/visual-viewport-pending? false
|
||||
:ui/visual-viewport-state nil
|
||||
|
||||
:document/mode? document-mode?
|
||||
:document/mode? document-mode?
|
||||
|
||||
:github/contents {}
|
||||
:config {}
|
||||
:block/component-editing-mode? false
|
||||
:editor/draw-mode? false
|
||||
:editor/show-page-search? false
|
||||
:editor/show-page-search-hashtag? false
|
||||
:editor/show-date-picker? false
|
||||
;; With label or other data
|
||||
:editor/show-input nil
|
||||
:editor/show-zotero false
|
||||
:editor/last-saved-cursor nil
|
||||
:editor/editing? nil
|
||||
:editor/last-edit-block-input-id nil
|
||||
:editor/last-edit-block-id nil
|
||||
:editor/in-composition? false
|
||||
:editor/content {}
|
||||
:editor/block nil
|
||||
:editor/block-dom-id nil
|
||||
:editor/set-timestamp-block nil
|
||||
:editor/last-input-time nil
|
||||
:editor/pos nil
|
||||
:editor/document-mode? document-mode?
|
||||
:editor/args nil
|
||||
:editor/on-paste? false
|
||||
:editor/last-key-code nil
|
||||
:github/contents {}
|
||||
:config {}
|
||||
:block/component-editing-mode? false
|
||||
:editor/draw-mode? false
|
||||
:editor/show-page-search? false
|
||||
:editor/show-page-search-hashtag? false
|
||||
:editor/show-date-picker? false
|
||||
;; With label or other data
|
||||
:editor/show-input nil
|
||||
:editor/show-zotero false
|
||||
:editor/last-saved-cursor nil
|
||||
:editor/editing? nil
|
||||
:editor/last-edit-block-input-id nil
|
||||
:editor/last-edit-block-id nil
|
||||
:editor/in-composition? false
|
||||
:editor/content {}
|
||||
:editor/block nil
|
||||
:editor/block-dom-id nil
|
||||
:editor/set-timestamp-block nil
|
||||
:editor/last-input-time nil
|
||||
:editor/pos nil
|
||||
:editor/document-mode? document-mode?
|
||||
:editor/args nil
|
||||
:editor/on-paste? false
|
||||
:editor/last-key-code nil
|
||||
|
||||
:db/last-transact-time {}
|
||||
:db/last-persist-transact-ids {}
|
||||
;; whether database is persisted
|
||||
:db/persisted? {}
|
||||
:db/latest-txs (or (storage/get-transit :db/latest-txs) {})
|
||||
:cursor-range nil
|
||||
:db/last-transact-time {}
|
||||
:db/last-persist-transact-ids {}
|
||||
;; whether database is persisted
|
||||
:db/persisted? {}
|
||||
:db/latest-txs (or (storage/get-transit :db/latest-txs) {})
|
||||
:cursor-range nil
|
||||
|
||||
:selection/mode false
|
||||
:selection/blocks []
|
||||
:selection/start-block nil
|
||||
;; either :up or :down, defaults to down
|
||||
;; used to determine selection direction when two or more blocks are selected
|
||||
:selection/direction :down
|
||||
:custom-context-menu/show? false
|
||||
:custom-context-menu/links nil
|
||||
:selection/mode false
|
||||
:selection/blocks []
|
||||
:selection/start-block nil
|
||||
;; either :up or :down, defaults to down
|
||||
;; used to determine selection direction when two or more blocks are selected
|
||||
:selection/direction :down
|
||||
:custom-context-menu/show? false
|
||||
:custom-context-menu/links nil
|
||||
|
||||
;; pages or blocks in the right sidebar
|
||||
;; It is a list of `[repo db-id block-type block-data]` 4-tuple
|
||||
:sidebar/blocks '()
|
||||
;; pages or blocks in the right sidebar
|
||||
;; It is a list of `[repo db-id block-type block-data]` 4-tuple
|
||||
:sidebar/blocks '()
|
||||
|
||||
:preferred-language (storage/get :preferred-language)
|
||||
:preferred-language (storage/get :preferred-language)
|
||||
|
||||
;; electron
|
||||
:electron/auto-updater-downloaded false
|
||||
:electron/updater-pending? false
|
||||
:electron/updater {}
|
||||
:electron/user-cfgs nil
|
||||
;; electron
|
||||
:electron/auto-updater-downloaded false
|
||||
:electron/updater-pending? false
|
||||
:electron/updater {}
|
||||
:electron/user-cfgs nil
|
||||
|
||||
;; plugin
|
||||
:plugin/enabled (and (util/electron?)
|
||||
;; true false :theme-only
|
||||
((fnil identity true) (storage/get :lsp-core-enabled)))
|
||||
:plugin/indicator-text nil
|
||||
:plugin/installed-plugins {}
|
||||
:plugin/installed-themes []
|
||||
:plugin/installed-commands {}
|
||||
:plugin/installed-ui-items {}
|
||||
:plugin/simple-commands {}
|
||||
:plugin/selected-theme nil
|
||||
:plugin/selected-unpacked-pkg nil
|
||||
:plugin/marketplace-pkgs nil
|
||||
:plugin/marketplace-stats nil
|
||||
:plugin/installing nil
|
||||
:plugin/active-readme nil
|
||||
:plugin/updates-pending {}
|
||||
:plugin/updates-coming {}
|
||||
;; plugin
|
||||
:plugin/enabled (and (util/electron?)
|
||||
;; true false :theme-only
|
||||
((fnil identity true) (storage/get :lsp-core-enabled)))
|
||||
:plugin/indicator-text nil
|
||||
:plugin/installed-plugins {}
|
||||
:plugin/installed-themes []
|
||||
:plugin/installed-commands {}
|
||||
:plugin/installed-ui-items {}
|
||||
:plugin/simple-commands {}
|
||||
:plugin/selected-theme nil
|
||||
:plugin/selected-unpacked-pkg nil
|
||||
:plugin/marketplace-pkgs nil
|
||||
:plugin/marketplace-stats nil
|
||||
:plugin/installing nil
|
||||
:plugin/active-readme nil
|
||||
:plugin/updates-pending {}
|
||||
:plugin/updates-coming {}
|
||||
:plugin/updates-downloading? false
|
||||
:plugin/updates-unchecked #{}
|
||||
|
||||
;; pdf
|
||||
:pdf/current nil
|
||||
:pdf/ref-highlight nil
|
||||
;; pdf
|
||||
:pdf/current nil
|
||||
:pdf/ref-highlight nil
|
||||
|
||||
;; all notification contents as k-v pairs
|
||||
:notification/contents {}
|
||||
:graph/syncing? false
|
||||
;; all notification contents as k-v pairs
|
||||
:notification/contents {}
|
||||
:graph/syncing? false
|
||||
|
||||
;; copied blocks
|
||||
:copy/blocks {:copy/content nil :copy/block-tree nil}
|
||||
;; copied blocks
|
||||
:copy/blocks {:copy/content nil :copy/block-tree nil}
|
||||
|
||||
:copy/export-block-text-indent-style (or (storage/get :copy/export-block-text-indent-style)
|
||||
"dashes")
|
||||
:copy/export-block-text-remove-options (or (storage/get :copy/export-block-text-remove-options)
|
||||
#{})
|
||||
:date-picker/date nil
|
||||
:copy/export-block-text-indent-style (or (storage/get :copy/export-block-text-indent-style)
|
||||
"dashes")
|
||||
:copy/export-block-text-remove-options (or (storage/get :copy/export-block-text-remove-options)
|
||||
#{})
|
||||
:date-picker/date nil
|
||||
|
||||
:youtube/players {}
|
||||
:youtube/players {}
|
||||
|
||||
;; command palette
|
||||
:command-palette/commands []
|
||||
;; command palette
|
||||
:command-palette/commands []
|
||||
|
||||
:view/components {}
|
||||
:view/components {}
|
||||
|
||||
:debug/write-acks {}
|
||||
:debug/write-acks {}
|
||||
|
||||
:encryption/graph-parsing? false
|
||||
:encryption/graph-parsing? false
|
||||
|
||||
:favorites/dragging nil
|
||||
:favorites/dragging nil
|
||||
|
||||
:srs/mode? false
|
||||
:srs/mode? false
|
||||
|
||||
:srs/cards-due-count nil})))
|
||||
:srs/cards-due-count nil})))
|
||||
|
||||
;; block uuid -> {content(String) -> ast}
|
||||
(def blocks-ast-cache (atom (cache/lru-cache-factory {} :threshold 5000)))
|
||||
|
@ -1704,7 +1706,7 @@
|
|||
(defn consume-updates-coming-plugin
|
||||
[payload updated?]
|
||||
(when-let [id (keyword (:id payload))]
|
||||
(let [pending? (seq (:plugin/updates-pending @state))]
|
||||
(let [pending? (boolean (seq (:plugin/updates-pending @state)))]
|
||||
(swap! state update :plugin/updates-pending dissoc id)
|
||||
(if updated?
|
||||
(swap! state update :plugin/updates-coming dissoc id)
|
||||
|
@ -1720,6 +1722,28 @@
|
|||
(when-let [pkg (and id (get (:plugin/updates-coming @state) (keyword id)))]
|
||||
(coming-update-new-version? pkg)))
|
||||
|
||||
(defn all-available-coming-updates
|
||||
[]
|
||||
(when-let [updates (vals (:plugin/updates-coming @state))]
|
||||
(filterv #(coming-update-new-version? %) updates)))
|
||||
|
||||
(defn get-next-selected-coming-update
|
||||
[]
|
||||
(when-let [updates (all-available-coming-updates)]
|
||||
(let [unchecked (:plugin/updates-unchecked @state)]
|
||||
(first
|
||||
(if (seq unchecked)
|
||||
(filter #(not (contains? unchecked (:id %))) updates)
|
||||
updates)))))
|
||||
|
||||
(defn set-unchecked-update
|
||||
[id unchecked?]
|
||||
(swap! state update :plugin/updates-unchecked (if unchecked? conj disj) id))
|
||||
|
||||
(defn reset-unchecked-update
|
||||
[]
|
||||
(swap! state assoc :plugin/updates-unchecked #{}))
|
||||
|
||||
(defn sub-right-sidebar-blocks
|
||||
[]
|
||||
(when-let [current-repo (get-current-repo)]
|
||||
|
|
|
@ -181,6 +181,7 @@ html.is-mobile {
|
|||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
Loading…
Reference in New Issue