您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Quick management of owned HumbleBundle stuff
当前为
// ==UserScript== // @name Humble librarian // @namespace http://tampermonkey.net/ // @version 0.3.9 // @description Quick management of owned HumbleBundle stuff // @author LeXofLeviafan // @icon https://humblebundle-a.akamaihd.net/static/hashed/47e474eed38083df699b7dfd8d29d575e3398f1e.ico // @match *://www.humblebundle.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/ramda.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/lib/coffeescript-browser-compiler-legacy/coffeescript.js // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // ==/UserScript== var inline_src = String.raw` MSEC = 1 SECOND = 1000*MSEC MINUTE = 60*SECOND compact = R.filter R.identity each = R.flip R.forEach mapcat = R.flip R.chain prefixOf = R.flip R.startsWith sort = R.sortBy R.identity descending = R.descend R.identity $merge = Object.assign merge = R.mergeAll fromPairs = R.compose R.fromPairs, compact notEquals = R.compose R.complement, R.equals notEmpty = (x) -> not R.isEmpty (x or []) nilEmpty = (x) -> if notEmpty x then x ojuxt = R.curry (o, x) -> R.mapObjIndexed ((f) -> f x), o qstr = (s) -> if not s.includes('?') then "" else s[1 + s.indexOf '?'..] query = (s) -> fromPairs (l[1..] for l in qstr(s).split('&').map(R.match /([^=]+)=(.*)/) when l[0]) $e = (tag, options...) -> $merge document.createElement(tag), options... $get = (xpath, e=document) -> document.evaluate(xpath, e, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue $find = (selector, e=document) -> e.querySelector selector $find_ = (selector, e=document) -> Array.from e.querySelectorAll selector $text = (e) -> e and (e.innerText or e.data or "").trim() or "" $content = (e) -> e and e.innerHTML or "" $timer = (time, action, args...) -> setTimeout (-> action args...), time $inst = -> "#{new Date} #{Math.random()}".replace /0\./, '#' $forever = (f) -> setInterval f, SECOND//2 $watcher = (f) -> new MutationObserver (xs) -> each xs, (x) -> each x.addedNodes, f $notNils = R.pickBy R.complement R.isNil auxclick = (f) -> {onauxclick: f, oncontextmenu: -> no} $redraw = -> $timer SECOND//16, m.redraw throttle = (delay, action, args...) -> do (last = 0) -> -> now = +new Date if now > last+delay last = now action args... debounce = (delay, action, args...) -> do (last = null) -> -> last = cur = $inst() $timer delay, -> last is cur and action last, args... cache = (expire, f) -> do (value = null) -> calc = throttle expire, -> value = f() -> calc(); value $storageCache = (key, default_) -> cache SECOND//4, -> GM_getValue key, default_ $update = (key, data) -> GM_setValue key, merge [(GM_getValue key), data] $storageUpdater = (key) -> do (changes = {}) -> push = debounce SECOND//4, -> console.warn {changes}; $update(key, changes); changes = {}; $redraw() (k, v) -> changes[k] = $notNils merge [changes[k], v]; push() $updateData = (get, $update, key, value) -> data = get()[key] or {} R.whereEq($notNils(value), data) or $update key, merge [data, value] $storage = (key, default_) -> [$storageCache(key, default_), $storageUpdater(key)] [$libraryData, $libraryPush] = $storage 'library', {} [$purchasesData, $purchasesPush] = $storage 'purchases', {} $addGroup = (s) -> GM_setValue 'groups', sort R.union GM_getValue('groups', []), [s] $delGroup = (s) -> GM_setValue 'groups', R.without [s], GM_getValue('groups', []) groupExists = (s) -> s in GM_getValue 'groups', [] EXECUTABLE = "Android Windows Linux Mac".split ' ' TYPE = "game software resource music tabletop puzzlebook fiction nonfiction tutorial other".split ' ' EXEC_TYPES = ['game', 'software', undefined, null] # type defaults to 'game' URL = location.href HOST = location.host PAGE = location.pathname PARAMS = query location.search title_ = (x) -> x.title_ or x.title steamSearchUrl = (term) -> "https://store.steampowered.com/search/?term=#{encodeURIComponent term}" $steamLookup = (e, header) -> e and do (url = steamSearchUrl if R.is String, header then header else header.innerHTML.trim()) -> # innerText may be capsed $merge e, title: "Look up on Steam", style: "cursor: pointer; pointer-events: all", onclick: (e) -> open url, '_blank'; e.stopPropagation(); no groupList = (groups = GM_getValue 'groups', []) -> sort R.uniq [groups..., compact(R.map R.prop('group'), R.values $libraryData())...] grouping = (groups, library) -> R.groupBy ((x) -> x.group or groups.find(prefixOf title_ x) or ""), R.sortBy R.compose(R.toLower, title_), R.values library matcher = (subs) -> do (z = R.toLower "#{subs}") -> (s) -> R.includes z, R.toLower "#{s}" parseDate = (s) -> new Date("#{s} UTC").toJSON().replace(/T.*/, '') htmlDecode = (s) -> s and new DOMParser().parseFromString(s, "text/html").documentElement.textContent if HOST is "www.humblebundle.com" purchases = fromPairs ([x.title.replace(/^Humble [^ ]+ Bundle: /, '').replace(/^Humble (.+) Bundle$/, '$1'), x.date] for _, x of $purchasesData()) GM_addStyle ".hb-lib-owned {color: limegreen !important}" ["a.bundle"###, "a.monthly"###].forEach (s) -> $find_(s).forEach (e) -> do (name = $find(".name", e)) -> if htmlDecode(name?.innerHTML) of purchases name.classList.add 'hb-lib-owned' e.title = "Owned since #{purchases[name.innerHTML]}" if HOST is "www.humblebundle.com" then switch # checking in case user wants to have overlay on other sites when PAGE.match "^/home/(library|purchases|keys|coupons)$" _iconUrl = (s) -> s and s.match(/^url\("(.*)"\)$/)[1] _keyExpired = (e) -> R.includes "expired", $text $find(".keyfield.redeemed .keyfield-value", e) _downloadVisible = (e) -> e.parentNode.style.display isnt 'none' _download = (e) -> [caption, meta] = e.children [title, link] = R.map $text, caption.childNodes [info] = meta.childNodes {title, link, info: $text(info).replace(/ \|$/, "")} _downloads = (platform, downloads, audioDownloads) -> disabled = platform?.classList.contains 'disabled' category = platform and $text $find(".selected", platform) others = unless platform then [] else $find_(".choice", platform).map $text noExec = R.none ((s) -> s in EXECUTABLE), [category, others...] _files = if category and (disabled or noExec) R.mapObjIndexed R.map(_download), R.groupBy ((e) -> if _downloadVisible(e) then category else others[0]), downloads files = _files and if noExec and others.length is 1 then _files else R.pick [category], _files audio = notEmpty(audioDownloads) and compact audioDownloads.map _download (audio or category) and {categories: compact([audio and 'Audio', category, others...]).sort().join(' '), \ files: merge [audio and {Audio: audio}, files]} _keys = (platformSections) -> mapcat platformSections, (section) -> type = R.find(notEquals('platform'), section.classList) platform = unless type is 'generic' then $text $find("h3", section) $find_(".key-redeemer", section).map (e) -> $notNils platform: platform title: $text $find(".heading-text h4", e) instructions: $content( $find(".custom-instruction", e) ) or null expiration: $content( $find(".expiration-messaging", e) ) or null status: switch when _keyExpired e then 'expired' when $find(".keyfield.redeemed", e) then 'redeemed' when $find(".keyfield.redeemed-gift", e) then 'gifted' when $find(".keyfield.enabled", e) then 'unused' else "???" # this will appear if I missed something _links = (customLinks) -> customLinks.map (e) -> $text $find(".js-raw-html", e) _selector = ojuxt icon: (e) -> _iconUrl($find(".icon", e).style.backgroundImage) or null title: (e) -> $text $find(".text-holder h2", e) publisher: (e) -> $text( $find(".text-holder p", e) ) or null _details = ojuxt title: (e) -> $text $find(".details-heading .text-holder h2", e) url: (e) -> $find(".details-heading a", e)?.href downloads: (e) -> _downloads $find(".select-holder .custom-select", e), ($find_(".#{s}-section .download-button", e) for s in ['download', 'audio'])... keys: (e) -> nilEmpty _keys $find_(".key-redeemers .platform", e) links: (e) -> nilEmpty _links $find_(".custom-html .show-whitebox", e) _purchase = ojuxt id: (e) -> e.getAttribute 'data-hb-gamekey' title: (e) -> $text $find(".product-name", e) date: (e) -> parseDate $text $find(".order-placed", e) total: (e) -> do (s = $text $find(".total", e)) -> if s isnt "--" then s GM_addStyle ".hb-lib-downloads-steamicon {padding-right: 5px}" _steamLookups = debounce SECOND//4, -> each $find_(".platform.steam .key-redeemer .heading-text h4"), (e) -> do (icon = $e 'i', className: "hb hb-steam hb-lib-downloads-steamicon") -> $steamLookup icon, e e.prepend icon _watcher = (getData) -> $watcher (e) -> _steamLookups() data = getData e $updateData($libraryData, $libraryPush, data.title, data) _initLib = R.once -> setTimeout -> _watcher(_selector).observe $find(".subproducts-holder"), childList: yes _watcher(_details).observe $find(".details-holder"), childList: yes _initKeys = R.once -> do (steamLookups = (e) -> do (steam = $find(".platform .hb-steam", e)) -> steam and $steamLookup steam, $find(".game-name h4")) -> R.map steamLookups, $find_ ".unredeemed-keys-table tr" $watcher(steamLookups).observe $find(".unredeemed-keys-table"), childList: yes, subtree: yes _regPurchase = (e) -> e.nodeName is 'DIV' and do (data = _purchase e) -> $updateData($purchasesData, $purchasesPush, data.id, data) _initPurchases = R.once -> R.map _regPurchase, $find_ ".js-purchase-holder .results .body .row" $watcher(_regPurchase).observe $find(".js-purchase-holder .results .body"), childList: yes do (isLibrary = -> location.pathname is "/home/library") -> if isLibrary() then _initLib() else $forever -> isLibrary() and _initLib() do (isKeys = -> location.pathname is "/home/keys") -> if isKeys() then _initKeys() else $forever -> isKeys() and _initKeys() do (isPurchases = -> location.pathname is "/home/purchases") -> if isPurchases() then _initPurchases() else $forever -> isPurchases() and _initPurchases() when PAGE.match "^/(software|games|books)/" GM_addStyle ".hb-lib-owned .item-title {color: limegreen !important} .hb-lib-owned.item-details:hover .img-container:after {box-shadow: inset 0 0 0 2px limegreen}" each $find_(".tier-item-details-view"), (e) -> $steamLookup $find(".hb-steam", e), $find(".header-area .heading-medium", e) _convert = R.replace /\s+/g, " " keys = new Set mapcat(R.values( $libraryData() ), (x) -> (x.aliases or []).concat (x.keys or []).map R.prop 'title').map _convert each $find_(".item-details"), (e) -> do (title = $text($find ".item-title", e), sub = $find(".item-flavor-text", e)) -> subtitle = $text((sub?.children.length is 0) and sub) if [title, "#{title} (#{subtitle})"].map(_convert).some (s) -> s of $libraryData() or keys.has s e.classList.add 'hb-lib-owned' e.title = "Already in collection" when PAGE.match "^/membership/" each $find_(".content-choice"), (e) -> $steamLookup $find(".hb-steam", e), $find(".content-choice-title", e) each $find_(".js-choice-details"), (e) -> $steamLookup $find(".hb-steam", e), $find(".title span", e) _watcher = $watcher (e) -> $steamLookup $find(".hb-steam", e), $find(".title span", e) _watcher.observe $find("#site-modal"), childList: yes, subtree: yes when PAGE.match "^/store" each $find_(".entity"), (e) -> $steamLookup $find(".hb-steam", e), do (x = $find(".entity-title", e)) -> x?.title or x _watcher = $watcher (e) -> e.classList and each $find_(".entity, .entity-block-container", e), (x) -> $steamLookup $find(".hb-steam", x), $find(".entity-title", x) each [$find_('.entity-lists')..., $find('.search-results-holder')], (e) -> e and _watcher.observe e, childList: yes, subtree: yes _productWatcher = $watcher (e) -> if e.className is "product-details-page" each ($find(".#{s} .hb-steam", e) for s in ['platform-delivery', 'availability-section', 'user-rating-view']), (x) -> x and $steamLookup x, $find(".js-human-name .human_name-view") _watcher.observe $find(".recommendations-row", e), childList: yes, subtree: yes _productWatcher.observe $find(".js-page-content"), childList: yes when PAGE is "/downloads" GM_addStyle ".hb-lib-downloads-steamicon {padding-right: 5px}" [_keys, _downloads, _data] = [{}, {}, -> $purchasesData()[PARAMS.key] or {id: PARAMS.key, title: $find('#hibtext').childNodes[2].data?.trim()}] _keysWatcher = $watcher (section) -> do (data = _data(), keys = $find_(".key-redeemer h4", section), title = $text $find('h2', section)) -> $merge _keys, fromPairs keys.map (e) -> [$text(e), if title is "Key" then "" else title] $updateData $purchasesData, $purchasesPush, data.id, merge [data, keys: merge [data.keys, _keys]] if title is "Steam" then each keys, (e) -> do (icon = $e 'i', className: "hb hb-steam hb-lib-downloads-steamicon") -> $steamLookup icon, e; e.prepend icon _downloadsWatcher = $watcher (root) -> each $find_(".whitebox-redux", root), (section) -> do (data = _data(), platforms = $find_(".js-platform-button", section).map $text) -> if platforms.length is 1 $merge _downloads, fromPairs $find_(".download-rows .row .title", section).map (e) -> [$text(e), platforms[0]] $updateData $purchasesData, $purchasesPush, data.id, merge [data, downloads: merge [data.downloads, _downloads]] _couponsWatcher = $watcher (e) -> do (name = $find(".coupon-name", e)?.innerText, icon = $find(".platforms .hb-steam", e)) -> name and icon and $steamLookup icon, name.replace(/^ *[0-9]+% off +/, "") do (e = $find ".key-container") -> e and _keysWatcher.observe e, childList: yes do (e = $find ".js-all-downloads-holder") -> e and _downloadsWatcher.observe e, childList: yes do (e = $find ".js-coupon-whitebox-holder") -> e and _couponsWatcher.observe e, childList: yes GM_addStyle ".hb-lib-overlay {position: fixed; top: 0; right: 0; z-index: 1000; background: rgba(0, 0, 0, 0.8); color: grey; font-size: medium; max-height: 80%; max-width: 80%; display: flex; flex-direction: column; margin-left: 75px} .hb-lib-overlay-toggle {display: inline-block; padding: 1ex; font-family: monospace; cursor: pointer} .hb-lib-overlay-header {margin: 0 1ex} .hb-lib-overlay-body {margin: 1ex; overflow: hidden; display: flex; flex-direction: column} .hb-lib-grow {flex-grow: 1} .hb-lib-overlay-header {display: flex; font-size: large; font-weight: bold} .hb-lib-scrollbox {overflow: auto; margin-top: 1ex} .hb-lib-group li {padding-right: 1em} .hb-lib-icon {padding-right: 1ex} .hb-lib-selectable {cursor: pointer} .hb-lib-selectable:hover {opacity: .5} .hb-lib-purchase {padding-left: 1ex} .hb-lib-preview {height: 0; position: relative; left: -10ex; top: 1ex} .hb-lib-custom-title, .hb-lib-custom-group, .hb-lib-row {display: flex} .hb-lib-publisher {font-size: x-small; cursor: pointer} .hb-lib-select-types, .hb-lib-custom-type select, .hb-lib-custom-type option {text-transform: capitalize} .hb-lib-select-types label {padding: 1ex; cursor: pointer} .hb-lib-category {padding-top: 1em} .hb-lib-category ul {margin-top: 0}" overlay = $e 'div', className: 'hb-lib-overlay' document.body.appendChild overlay defState = -> open: no, filter: "", view: null, group: null, publisher: null, purchase: null, expand: {}, unused: no, types: fromPairs ([s, yes] for s in TYPE) state = defState() $setState = (x) -> $merge state, x; $redraw() document.addEventListener 'keydown', (e) -> if e.key is 'Escape' then $setState defState() $editItem = (changes, item=state.view.item) -> $merge item, changes; $libraryPush item.title, item; setTimeout $redraw, SECOND//2 $addAlias = (item=state.view.item) -> $editItem aliases: [(item.aliases or [])..., ""] $delAlias = (i, item=state.view.item) -> $editItem aliases: nilEmpty R.remove i, 1, item.aliases $setAlias = (i, s, item=state.view.item) -> $editItem aliases: R.update i, s, item.aliases $setInState = (value, path...) -> $setState R.assocPath path, value, state $setTypes = (items, value) -> each items, (x) -> $libraryPush x.title, merge [x, type: value] $mkGroup = -> $addGroup state.filter; $setState filter: "" $renameGroup = (group, items) -> $delGroup state.group; $addGroup group; $setState {group} each items, ((x) -> x.group and $libraryPush x.title, merge [x, {group}]); $itemView = (item, extras...) -> do (groups = groupList()) -> $setState merge [extras..., view: {item, groups, group: item.group or R.findLast(((s) -> item.title.startsWith s), groups) or ""}] titleFilter = -> matcher state.filter or "" typeFilter = (item) -> state.types[item.type or 'game'] and (not state.unused or (item.keys or []).some R.propEq 'status', 'unused') itemFilter = (p) -> (item) -> p(title_ item) and typeFilter item purchaseTitle = (item) -> item.title + (if item.date then " (#{item.date})" else "") purchaseFilter = R.curry ({keys={}, downloads={}}, item) -> (item.keys or []).some((x) -> x.title of keys) or R.unnest(R.values item.downloads?.files or {}).some((x) -> x.title of downloads) ViewItem = view: ({attrs: {item, group, groups}}) -> [ m '.hb-lib-overlay-header', item.icon and m('img.hb-lib-icon', src: item.icon) m '.hb-lib-entry-title.hb-lib-grow', m('a', {href: item.url, target: '_blank'}, item.title), m('.hb-lib-publisher', {onclick: -> $setState publisher: item.publisher}, item.publisher) m('.hb-lib-overlay-toggle', {onclick: -> $setState view: null}, '⇚') m '.hb-lib-scrollbox', m '.hb-lib-custom-title', title: "Custom title", m 'label', m 'input', type: 'checkbox', checked: not R.isNil(item.title_), onchange: -> $editItem title_: if @checked then "" else null m 'input.hb-lib-grow', disabled: R.isNil(item.title_), value: title_(item), oninput: -> $editItem title_: @value m '.hb-lib-custom-group', title: "Custom group", m 'label', m 'input', type: 'checkbox', checked: not R.isNil(item.group), onchange: -> $editItem group: if @checked then group else null m 'input.hb-lib-grow', {disabled: R.isNil(item.group), placeholder: "Custom group…", onfocus: (=> @groups = yes),\ onblur: (=> $timer SECOND//16, => @groups = no), value: item.group or group, oninput: -> $editItem group: @value} @groups and groups.filter(matcher item.group).map (s) -> m '.hb-lib-selectable', {key: s, onclick: -> $editItem group: s}, s m '.hb-lib-custom-type', title: "Type", m 'label', "Type: ", m 'select', {onchange: -> $editItem type: @value or null}, TYPE.map (s) -> m 'option', {selected: item.type is s, value: if s is 'game' then "" else s}, s m '.hb-lib-aliases', title: "Aliases can be used for additional key matching (in case of typos)", m '.hb-lib-row', m('.hb-lib-grow', "Aliases"), m('button', {onclick: -> $addAlias()}, "+") (item.aliases or []).map (s, i) -> m '.hb-lib-row', m('input.hb-lib-grow', {title: s, value: s, oninput: -> $setAlias i, @value}), m('button', {onclick: -> $delAlias i}, '-') R.sortBy(R.prop('date'), R.values $purchasesData()).filter((x) -> purchaseFilter(x, item)).map (x) -> m '.hb-lib-row', "Found in:", m '.hb-lib-purchase.hb-lib-selectable', {onclick: -> $setState purchase: x.id}, purchaseTitle x item.downloads and do (categories = item.downloads.categories.split ' ') -> m '.hb-lib-downloads', m '.hb-lib-category', "Downloads: ", categories.join ", " compact categories.map (s) -> do (files = (item.type not in EXEC_TYPES or s not in EXECUTABLE) and item.downloads.files[s]) -> if files m '.hb-lib-category', {key: s}, "#{s} downloads:", m 'ul', files.map (x) -> m 'li', "#{x.title} (#{x.link})", m('br'), "#{x.info}" item.keys and do (platforms = R.groupBy R.propOr("Generic", 'platform'), item.keys) => R.keys(platforms).sort().map (s) => m '.hb-lib-category', {key: s}, "#{s} keys:", m 'ul', platforms[s].map (x) => m 'li', {onmouseenter: => @hover = x.title}, "#{x.title} [#{x.status}]", if @hover is x.title then [m.trust(x.instructions), m.trust(x.expiration)] else (x.instructions or x.expiration) and " (…)" item.links and m '.hb-lib-category', "Additional content:", m 'ul', item.links.map (s, i) => m 'li', {onmouseenter: => @hover = "link##{i}"}, unless @hover is "link##{i}" then "(…)" else m.trust s ] EditGroup = view: ({attrs: {title, items}}) -> do (prefixGroup = groupExists title) => [ m '.hb-lib-overlay-header', title: "Title", m('input.hb-lib-grow', value: title, onchange: -> $renameGroup @value, items), m('.hb-lib-overlay-toggle', {onclick: -> $setState group: null}, '⇚') groupExists(title) and m 'button', {onclick: -> $setState group: null; $delGroup title}, "Remove prefix group" m '.hb-lib-custom-type', title: "Type", m 'label', "Set type for all: ", m 'select', {onchange: -> $setTypes items, @value or null}, m 'option', selected: yes, disabled: yes TYPE.map (s) -> m 'option', {value: if s is 'game' then "" else s}, s m 'ul.hb-lib-scrollbox.hb-lib-group', items.map (x) -> m 'li', {key: x.title, title: x.title}, title_(x), x.type and " [#{x.type}]" ] ViewGroup = oninit: -> Object.defineProperty @, 'open', get: (=> state.expand[@title]), set: (x) => $setInState x, 'expand', @title view: ({attrs: {@title="", items, filter}}) -> do (filtering = not filter @title) => m '.hb-lib-group', m '.hb-lib-overlay-header.hb-lib-selectable', not filtering and {onclick: => @open = not @open}, m '.hb-lib-grow', @title and {title: "Right/middle click to edit", auxclick(=> $setState group: @title)...}, @title or "—", " [#{items.length}]" not filtering and m('.hb-lib-overlay-toggle', if @open then '-' else '+') (@open or filtering) and m 'ul', items.map (x) => m.fragment {key: x.title}, [ m 'li.hb-lib-selectable', {title: x.title, onmouseenter: (=> @preview = x.title), onmouseleave: (=> @preview = null),\ onclick: (=> $setState view: {item: x, group: @title, groups: groupList()})}, title_(x), x.type and " [#{x.type}]" @preview is x.title and x.icon and m '.hb-lib-preview', m 'a', m 'img', src: x.icon ] BrowsePurchase = view: ({attrs: {id, items}}) -> do (purchase = $purchasesData()[id]) -> [ m '.hb-lib-overlay-header.hb-lib-selectable', m '.hb-lib-grow', "Purchase: #{purchaseTitle purchase} [#{items.length}]" m '.hb-lib-overlay-toggle', {onclick: -> $setState purchase: null}, '⇚' m 'ul.hb-lib-group.hb-lib-scrollbox', style: "padding-bottom: 20ex", R.sortBy(title_, items).map (x) => m.fragment {key: x.title}, [ m 'li.hb-lib-selectable', {title: x.title, onmouseenter: (=> @preview = x.title), onmouseleave: (=> @preview = null), onclick: => $itemView x, purchase: null}, title_(x), x.type and " [#{x.type}]" @preview is x.title and x.icon and m '.hb-lib-preview', m 'a', m 'img', src: x.icon ] ] BrowsePublisher = view: ({attrs: {name, items}}) -> [ m '.hb-lib-overlay-header.hb-lib-selectable', m '.hb-lib-grow', "Publisher: #{name} [#{items.length}]" m '.hb-lib-overlay-toggle', {onclick: -> $setState publisher: null}, '⇚' m 'ul.hb-lib-group.hb-lib-scrollbox', style: "padding-bottom: 20ex", R.sortBy(title_, items).map (x) => m.fragment {key: x.title}, [ m 'li.hb-lib-selectable', {title: x.title, onmouseenter: (=> @preview = x.title), onmouseleave: (=> @preview = null), onclick: => $itemView x, publisher: null}, title_(x), x.type and " [#{x.type}]" @preview is x.title and x.icon and m '.hb-lib-preview', m 'a', m 'img', src: x.icon ] ] Browse = oninit: -> Object.defineProperty @, 'filter', get: titleFilter filtered: (items, title="", p=@filter) -> items.filter if p title then typeFilter else itemFilter p view: ({attrs: {grouped}}) -> do (groups=R.omit([""], grouped), noGroup=grouped[""], p=@filter) => [ m '.hb-lib-row', m 'input.hb-lib-grow', {title: "Filter", placeholder: "Search…", value: state.filter, oninput: -> $setState filter: @value} m 'button', {onclick: $mkGroup, disabled: not state.filter}, "Add prefix group" m '.hb-lib-select-types', title: "Types", TYPE.map (s) -> m 'label', {key: s}, m('input', type: 'checkbox', checked: state.types[s], onchange: -> $setInState @checked, 'types', s), s m 'button', {title: "Show unused keys", onclick: -> $setState unused: not state.unused}, "#{if state.unused then '☑' else '☐'} Unused" m '.hb-lib-scrollbox', style: "padding-bottom: 15ex", compact R.toPairs(groups).sort().map ([title, xs]) => do (items = @filtered xs, title) => notEmpty(items) and m ViewGroup, {title, items, key: title, @filter} noGroup and m ViewGroup, {@filter, items: @filtered noGroup} ] m.mount overlay, view: -> do (library = $libraryData(), groups = R.sort descending, GM_getValue 'groups', []) -> notEmpty(library) and [ m 'h4.hb-lib-overlay-header', state.open and m('span.hb-lib-grow', "Humble library") m('span.hb-lib-overlay-toggle', {onclick: -> $setState open: not state.open}, if state.open then '×' else '+') state.open and m '.hb-lib-overlay-body', switch when state.purchase then m BrowsePurchase, id: state.purchase, items: R.values(library).filter purchaseFilter $purchasesData()[state.purchase] when state.publisher then m BrowsePublisher, name: state.publisher, items: R.values(library).filter (x) -> x.publisher is state.publisher when state.view then m ViewItem, state.view when state.group then m EditGroup, title: state.group, items: grouping(groups, library)[state.group] or [] else m Browse, {grouped: grouping(groups, library)} ] setInterval $redraw, MINUTE//4 `; eval( CoffeeScript.compile(inline_src) );