Advertisement
Guest User

Backloggery data

a guest
May 8th, 2021
49
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JSON 42.42 KB | None | 0 0
  1. // ==UserScript==
  2. // @name         Backloggery interop
  3. // @namespace    http://tampermonkey.net/
  4. // @version      0.8.10
  5. // @description  Backloggery integration with game library websites
  6. // @author       LeXofLeviafan
  7. // @include      *://www.backloggery.com/games.php?*
  8. // @include      *://www.backloggery.com/update.php?*
  9. // @include      *://www.backloggery.com/newgame.php?*
  10. // @include      *://steamcommunity.com/id/star_fang/games/*
  11. // @exclude      *://steamcommunity.com/id/<username>/games/*
  12. // @include      *://steamcommunity.com/id/star_fang/stats/*
  13. // @exclude      *://steamcommunity.com/id/<username>/stats/*
  14. // @include      *://steamcommunity.com/stats/star_fang/achievements
  15. // @include      *://steamcommunity.com/stats/*/achievements/*
  16. // @include      *://store.steampowered.com/app/*
  17. // @include      *://steamdb.info/app/*
  18. // @include      *://astats.astats.nl/astats/User_Games.php?*
  19. // @include      *://www.gog.com/account
  20. // @include      *://www.humblebundle.com/home/*
  21. // @include      *://www.humblebundle.com/subscription/trove
  22. // @include      *://*.gamersgate.com/account/*
  23. // @include      *://psnprofiles.com/<username>
  24. // @include      *://psnprofiles.com/*?*
  25. // @include      *://psnprofiles.com/trophies/*
  26. // @exclude      *://psnprofiles.com/<username>
  27. // @require      https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.min.js
  28. // @require      https://cdn.jsdelivr.net/npm/coffeescript@2.4.1/lib/coffeescript-browser-compiler-legacy/coffeescript.js
  29. // @grant        GM_info
  30. // @grant        GM_getValue
  31. // @grant        GM_setValue
  32. // @grant        GM_addStyle
  33. // ==/UserScript==
  34.  
  35. var inline_src = String.raw`
  36.  
  37.   ROMANS = {: 'I',: 'II',: 'III',: 'IV',: 'V',: 'VI',: 'VII',: 'VIII',: 'IX',: 'X',: 'XI',: 'XII',: 'L',: 'C',: 'D',: 'M'}
  38.   roman = RegExp("[#{Object.keys(ROMANS).join('')}]", 'g')
  39.  
  40.   identity = (x) -> x
  41.   merge = (os...) -> Object.assign {}, os...
  42.   fromPairs = (pairs) -> merge ([k]: v for [k, v] in pairs.filter identity)...
  43.   keymap = (ks, f) -> fromPairs ([k, f k] for k in ks)
  44.   objmap = (o, f) -> fromPairs ([k, f(v, k, o)] for k, v of o)
  45.   objfilter = (o, f) -> fromPairs ([k, v] for k, v of o when f(v, k, o))
  46.   pick = (o, keys...) -> fromPairs ([k, o[k]] for k in keys when k of o)
  47.   method = (o, k, def=->) -> o?[k]?.bind?(o) or def
  48.   setFn = (xs) -> method (new Set xs), 'has'
  49.   last = (l) -> l[l.length - 1]
  50.   when_ = (x, f) -> x and f(x)
  51.   replace = (s, re, pattern) -> s.match(re) and s.replace(re, pattern)
  52.   qstr = (s) -> if not s.includes('?') then "" else s[1 + s.indexOf '?'..]
  53.   query = (s) -> fromPairs (l[1..] for l in qstr(s).split('&').map((s) -> s.match /([^=]+)=(.*)/) when l)
  54.   slugify = (s) -> s.replace(roman, (c) -> ROMANS[c]).toLowerCase().replace(/[.]/g, '').replace(/[^a-z0-9+]+/g, '-').replace(/(^-*|-*$)/g, '')
  55.   capitalize = (s) -> do (z = "#{s}") -> z[...1].toUpperCase() + z[1..]
  56.   statStr = (o, ks...) -> ("#{capitalize k}: #{o[k]}" for k in ks when k of o).join '\n'
  57.   forever = (f) -> setInterval f, 100
  58.  
  59.   PAGE = location.href
  60.   PARAMS = query location.search
  61.   RE =
  62.     backloggeryUpdate:  "backloggery\\.com/update\\.php"
  63.     backloggeryCreate:  "backloggery\\.com/newgame\\.php"
  64.     backloggeryLibrary: "backloggery\\.com/games\\.php"
  65.     steamLibrary:       "steamcommunity\\.com/id/[^/]+/games/\\?tab=all"
  66.     steamRecent:        "steamcommunity\\.com/id/[^/]+/games($|/$|/\\?tab=recent)"
  67.     steamAchievements:  "steamcommunity\\.com/id/[^/]+/stats/[^/]+"
  68.     steamAchievements2: "steamcommunity\\.com/stats/[^/]+/achievements"
  69.     steamDetails:       "store\\.steampowered\\.com/app/([^/]+)"
  70.     steamDbDetails:     "steamdb\\.info/app/[^/]+"
  71.     steamStats:         "astats\\.astats\\.nl/astats/User_Games\\.php"
  72.     gogLibrary:         "gog\\.com/account"
  73.     humbleLibrary:      "humblebundle\\.com/home/(library|purchases|keys|coupons)"
  74.     humbleTrove:        "humblebundle\\.com/subscription/trove"
  75.     ggateLibrary:       "gamersgate\\.com/account/(games|wishlist|achievements)"  # they share a page and can switch without reload
  76.     psnLibrary:         "psnprofiles\\.com/([^/?]+)/?($|\\?)"
  77.     psnDetails:         "psnprofiles\\.com/trophies/([^/?]+)/([^/?]+)$"
  78.   PSN_ID = (GM_info.script.options.override.use_includes or []).reduce ((x, s) -> x or s.match(RE.psnLibrary)?[1]), null
  79.  
  80.   _PSN_HW = {PS3: '3', PS4: '4', VITA: 'V'}
  81.   _psnData = (images) -> (id) -> objmap(objfilter(GM_getValue('psn', {}), (x) -> _PSN_HW[id] in x.platforms),
  82.                                         (x, k) -> merge x, image: images[k], url: "https://psnprofiles.com/trophies/#{k}/#{PSN_ID}")
  83.   DATA = do (TROVE = objmap(GM_getValue('humble-trove', {}), (o) -> merge o, url: "https://www.humblebundle.com/monthly/trove")
  84.              STATS = GM_getValue('steam-stats', {}),  PLATFORMS = GM_getValue('steam-platforms', {}),
  85.              psnData = _psnData(GM_getValue 'psn-img', {})) ->
  86.     steam:  objmap GM_getValue('steam', {}), (x, k) -> merge(x, url: x.link, achievements: STATS[k] or '?', worksOn: PLATFORMS[k] or 's')
  87.     gog:    objmap GM_getValue('gog',   {}), (x) -> merge(x, url: "https://gog.com#{x.url}", completed: if x.completed then 'yes' else 'no')
  88.     humble: objmap merge(TROVE, GM_getValue 'humble'), (x, id) -> merge(TROVE[id], x)
  89.     ggate:  objmap GM_getValue('ggate', {}), (x, id) -> merge(x, url: "https://gamersgate.com/#{id}")
  90.     ps3:    psnData('PS3'),   ps4: psnData('PS4'),  psvita: psnData('VITA')
  91.   OS = w: ["Windows", 'fa-windows'], l: ["Linux", 'fa-linux'], m: ["MacOS", 'fa-apple'], a: ["Android", 'fa-android'], s: ["Steam", 'fa-steam']
  92.   CUSTOM_ICONS = {steam: "fab fa-steam", windows: "fab fa-windows", linux: "fab fa-linux", mac: "fab fa-apple", android: "fab fa-android", \
  93.                   console: "fas fa-gamepad", xbox: "fab fa-xbox", playstation: "fab fa-playstation", web: "fab fa-html5", \
  94.                   nodejs: "fab fa-node-js", flash: "fab fa-adobe", dice: "fas fa-dice", d20: "fas fa-dice-d20", trophy: "fas fa-trophy"}
  95.   slugs = (o) -> fromPairs ([slugify(v.name), k] for k, v of o)
  96.  
  97.   $clear  = (e) -> e.removeChild e.firstChild while e.firstChild;  e
  98.   $append = (parent, children...) -> parent.appendChild e for e in children;  parent
  99.   $before = (neighbour, children...) -> neighbour.parentElement.insertBefore(e, neighbour) for e in children;  neighbour
  100.   $after  = (neighbour, children...) -> $before(neighbour.nextSibling, children...);  neighbour
  101.   $e      = (tag, options, children...) -> $append Object.assign(document.createElement(tag), options), children...
  102.   $get   = (xpath, e=document) -> document.evaluate(xpath, e, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
  103.   $find  = (selector, e=document) -> e.querySelector selector
  104.   $find_ = (selector, e=document) -> Array.from e.querySelectorAll selector
  105.   $hasClass = (e, clss) -> do (l = e.classList) -> l and clss.split(' ').every((s) -> not s or l.contains s)
  106.   $visibility = (e, x) -> e.style.visibility = if x then 'visible' else 'hidden'
  107.   $markUpdate = (k) -> GM_setValue 'updated', merge(GM_getValue('updated'), [k]: +new Date)
  108.   $assertEq = (a, b, err) -> a is b or alert(if typeof err isnt 'function' then err else err a, b)
  109.   $stop = (f=->) -> (e) -> f e;  e.stopPropagation();  no
  110.   $keepScroll = (e, f) -> do (x = e.scrollTop) -> f();  m.redraw();  e.scrollTop = x
  111.   $query = (url) -> new Promise (resolve, reject) -> do (xhr = new XMLHttpRequest) ->
  112.     xhr.open 'GET', url
  113.     [xhr.onerror, xhr.onload] = [reject, -> resolve JSON.parse xhr.response]
  114.     xhr.send()
  115.   words = (s) -> slugify(s).split('-').sort().reverse()
  116.   matching = (ss, zs) -> do (res = 0, i = 0, j = 0) ->
  117.     while i < ss.length and j < zs.length
  118.       [s, z] = [ss[i], zs[j]]
  119.       if s is z
  120.         i++;  j++;  res += 2
  121.       else if z.startsWith s
  122.         i++;  j++;  res += 1
  123.       else
  124.         if s < z then j++ else i++
  125.     res
  126.   order = (sets, exclude, text, k) -> do (d = DATA[k],  l = words(text),  f = (s) -> not exclude["#{k}##{s}"]) ->
  127.     o = objmap(sets[k] or {}, (ss) -> matching l, ss)
  128.     Object.keys(sets[k] or {}).sort (a, b) -> f(b)-f(a) or o[b]-o[a] or d[a].name.localeCompare d[b].name
  129.   $addChanges = (newChanges) -> do (changes = GM_getValue 'changes', []) ->
  130.     oldChanges = new Set changes
  131.     GM_setValue 'changes', [changes..., (id for id in newChanges when not oldChanges.has id)...]
  132.   WATCH_FIELDS = "name worksOn completed achievements platforms status trophies".split ' '
  133.   WATCH_META = {'steam-stats': 'steam', 'steam-platforms': 'steam'}
  134.   WATCH_LIBRARY = {'humble-trove': 'humble'}
  135.   $update = (library, games1) -> do (library_ = WATCH_LIBRARY[library] or library,  games0 = GM_getValue library, {}) ->
  136.     [ids1, ids0] = [games1, games0].map Object.keys
  137.     removed = (id for id in ids0 when id not of games1)
  138.     added   = (id for id in ids1 when id not of games0)
  139.     updated = (id for id in ids0 when id of games1 and WATCH_FIELDS.some (k) -> games0[id][k] isnt games1[id][k])
  140.     $markUpdate library
  141.     $addChanges ("#{library_}##{id}" for id in [removed..., updated...])
  142.     GM_setValue library, games1
  143.     setTimeout -> alert "Backloggery interop: added #{added.length} games, removed #{removed.length} games"
  144.   $mergeData = (k, o) -> do (library = WATCH_META[k],  old = GM_getValue k, {}) ->
  145.     library and $addChanges ("#{library}##{id}" for id in Object.keys o when id of old and old[id] isnt o[id])
  146.     GM_setValue k, merge(old, o)
  147.   $logo = (k, id) -> do (o = if k is 'custom' then id else DATA[k][id]) -> switch k
  148.     when 'steam'  then [o.logo, "https://steamcdn-a.akamaihd.net/steam/apps/#{id}/header.jpg"]
  149.     when 'gog'    then [196, 392].map((x) -> "https:#{o.image}_#{x}.jpg")
  150.     else [o.icon or o.image, o.image or o.icon]
  151.   $append document.head, $e('link', rel: 'stylesheet', href: "https://use.fontawesome.com/releases/v5.7.0/css/all.css")
  152.   GM_addStyle "#loader {position: fixed;  top: 50%;  left: 50%;  z-index: 10000;  transform: translate(-50%, -50%);
  153.                        font-size: 300px;  text-shadow: -1px 0 grey, 0 1px grey, 1px 0 grey, 0 -1px grey}"
  154.   GM_addStyle "@-webkit-keyframes rotation {from {-webkit-transform:rotate(0deg)}
  155.                                            to {-webkit-transform:rotate(360deg)}}
  156.               @keyframes rotation {from {transform:rotate(0deg) translate(-50%, -50%);  -webkit-transform:rotate(0deg)}
  157.                                    to {transform:rotate(360deg) translate(-50%, -50%);  -webkit-transform:rotate(360deg)}}"
  158.   GM_addStyle ".rotating {animation: rotation 2s linear infinite}"
  159.   LOGO = ".logo {height: 0;  width: 0;  display: flex;  flex-direction: row-reverse}
  160.          .logo img {border: 1px solid darkorchid;  background: #1b222f}"
  161.  
  162.  
  163.   if PAGE.match RE.backloggeryUpdate
  164.  
  165.     SETS = objmap DATA, (o) -> objmap(o, (x) -> words x.name)
  166.     legend         = $get '//*[@id="content-wide"]/section/form/fieldset[1]/legend'
  167.     systemDropdown = $get '//*[@id="content-wide"]/section/form/fieldset[1]/div[2]'
  168.     delBtns        = $get '//*[@id="content-wide"]/section/form/div[2]/div'
  169.     status         = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]'
  170.     _system        = when_ systemDropdown, (e) -> $find('select', e)
  171.     swap = -> do ([a, b] = [_system, $find '#detail2 select']) -> [a.value, b.value] = [b.value, a.value]
  172.     do (_bl = GM_getValue('backlog', {})[PARAMS.gameid]) ->
  173.       k = Object.keys(DATA).find (k) -> _bl?[k+'Id']
  174.       changeId = k and "#{k}##{_bl[k+'Id']}"
  175.       GM_setValue 'changes', (id for id in GM_getValue('changes', []) when id isnt changeId)
  176.  
  177.     unless legend and systemDropdown and delBtns # deleted/doesn't exist
  178.      backlog = GM_getValue('backlog', {})
  179.      if PARAMS.gameid in backlog
  180.        delete backlog[PARAMS.gameid]
  181.        GM_setValue('backlog', backlog)
  182.    else
  183.      for es in $find("[name=#{s}console]").children for s in ['', 'orig_']
  184.        e.innerText = "Windows" for e in es when e.value is "PC"
  185.      $append systemDropdown, $e('tt', innerText: "⇄", onclick: swap, style: "cursor: pointer;  padding-left: 8px;  font-size: large")
  186.      $before delBtns, $e('input', type: 'submit', name: 'submit2', className: 'greengray', value: "Stealth Save ⇄", onclick: swap)
  187.      $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex"))
  188.      $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex"))
  189.      GM_addStyle ".overlay {position: relative;  max-height: 0;  top: -40px;  z-index: 2;  margin-right: 10px;  display: flex;  flex-direction: row-reverse}
  190.                   .overlay input {height: 20px;  background: #4b4b4b;  color: white;  width: 500px;  border: 1px solid black;  padding-left: 1ex;  margin-bottom: 0}
  191.                   .overlay .options {display: flex;  flex-direction: column;  max-height: 500px;  overflow-y: auto;  background: grey}
  192.                   .overlay button {height: 28px;  background: #4b4b4b;  color: white;  border-radius: 10px 8px 8px 10px;  padding: 5px}
  193.                   .overlay .option {white-space: nowrap;  display: flex;  margin: .5px}   .overlay .trash {cursor: pointer}
  194.                   .overlay * {flex-shrink: 0}  .overlay button b {flex: 1;  padding-left: 1ex;  text-align: left;  overflow: hidden;  text-overflow: ellipsis}
  195.                   .oslist {display: flex;  position: absolute;  width: 505px;  padding-top: 7.5px;  pointer-events: none}   .oslist.shift {padding-right: 20px}
  196.                   .os {padding-left: .75ex;  font-size: 20px;  color: white}   .oslist .action {padding-left: 1ex;  pointer-events: all}
  197.                   .action {color: white;  cursor: pointer}   .anchor .action {position: absolute;  top: 10px;  right: 7.5px}
  198.                   .iconlist {display: flex;  position: absolute;  width: 505px;  padding-top: 7.5px;  pointer-events: none;  color: white !important}
  199.                   fieldset, .anchor {position: relative}   .tooltip {background: rgba(0, 0, 0, 0.8)}
  200.                   .icons {padding: 1ex;  padding-bottom: unset}   .icons .btn {margin: .25em}  .btn.selected {border-color: white}
  201.                   .btn {background: #4b4b4b;  color: white;  border: 1px solid black;  cursor: pointer;  font-size: 20px;  padding: 2px;  border-radius: 5px}
  202.                   .btn.fa-eye {margin-left: 1.25px}   .done, .preview {display: block;  margin: 1ex auto}   .done {width: 90%;  cursor: pointer}
  203.                   #{LOGO} .logo img {height: 100px}   .preview {border: 1px solid darkorchid;  max-width: calc(100% - 2ex)}"
  204.      gameName = $find '[name=name]'
  205.      _bl = GM_getValue('backlog')?[PARAMS.gameid] or {}
  206.      excluded = GM_getValue('exclude', {})
  207.      data = (k=state.list) -> DATA[k]
  208.      id = (s=state.list) -> "#{s}Id"
  209.      id$ = (s=state.list) -> _bl[ id(s) ]
  210.      eId$ = (k=id$(s), s=state.list) -> "#{s}##{k}"
  211.      data$ = (s=state.list) -> data(s)?[ id$(s) ]
  212.      title = (k=state.list) -> data$(k)?.name or gameName.value
  213.      _icons = -> (_bl.custom?.icons or "").split ' '
  214.      _order = (s=state.title, k=state.list) -> order(SETS, excluded, s, k)
  215.      state = do (list = (_system.value or '').toLowerCase()) ->
  216.        list:   list
  217.        title:  title list
  218.        active: no
  219.        order:  _order(title(list), list)
  220.      when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "", statStr(o, 'completed') or o.status or '']
  221.      _setBl = (o) -> $mergeData 'backlog', [PARAMS.gameid]: Object.assign(_bl, o)
  222.      _delBl = (...ks) -> delete _bl[k] for k in ks;  _setBl()
  223.      _setExcl = (k, x) ->
  224.        excluded[ eId$(k) ] = x
  225.        $mergeData('exclude', [eId$(k)]: x)
  226.        state.order = _order()
  227.      if id$() and not data$() then _delBl id(), 'ignore'
  228.      section2 = $find_('fieldset')[1]
  229.      $append section2, $e('div', id: 'ignore', style: "position: absolute;  top: -25px;  left: 110px")
  230.      m.mount ignore, view: -> id$() and [
  231.        do (x = !_bl.ignore) -> m('i.btn', class: "far fa-eye#{if x then '' else '-slash'}", title: (if x then "Watch" else "Ignore"), onclick: -> _setBl(ignore: x))
  232.      ]
  233.      section1 = $find_('fieldset')[0]
  234.      $before section1, $e('div', id: 'logo', className: 'logo')
  235.      m.mount logo, view: -> switch
  236.        when _bl.custom then m('a', {target: '_blank', href: _bl.custom.url}, m('img', src: $logo('custom', _bl.custom)[1]))
  237.        when id$()      then m('a', {target: '_blank', href: data$().url}, m('img', src: $logo(state.list, id$())[1]))
  238.      $append section1, $e('div', id: 'custom', style: "position: absolute;  top: -25px;  left: 170px")
  239.      toggleCustom = (x) -> ->
  240.        state.active = no
  241.        if _bl.custom then _delBl('custom') else _delBl(id(), 'ignore');  _setBl custom: {}
  242.      m.mount custom, view: -> do (x = _bl.custom) -> m('i.btn', class: "fa fa-#{if x then 'edit' else 'list'}", title: (if x then "Custom" else "Listed"), onclick: toggleCustom x)
  243.      preview = null
  244.      document.addEventListener 'keydown', (e) -> if e.key is 'Escape'
  245.        _delBl id(), 'ignore'
  246.        Object.assign(state, active: no, title: title(), order: _order title())
  247.        preview = achievements.innerText = completed.innerText = ""
  248.        m.redraw()
  249.      $reset = -> do (list = (_system.value or '').toLowerCase()) -> if list isnt state.list
  250.        Object.assign(state, {list}, active: no, title: title(list), order: _order(title(list), list))
  251.        m.redraw()
  252.      $$ = (k) -> ->
  253.        Object.assign(state, active: no, title: data()[k].name)
  254.        state.order = _order()
  255.        _setBl([id()]: k)
  256.        when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "",  statStr(o, 'completed') or o.status or '']
  257.        no
  258.      $custom = -> _setBl custom: Object.assign _bl.custom, updated: +new Date
  259.      toggleIcon = (s) -> do (icons = _icons()) ->
  260.        _bl.custom.icons = (if s not in icons then [icons..., s] else icons.filter (x) -> x isnt s).join(' ').trim()
  261.        $custom()
  262.      gameName.onchange = _system.onchange = $reset
  263.      overlay = $e('div', style: "display: flex;  flex-direction: column;  width: calc(505px + 1ex);  position: relative")
  264.      $after(legend, $e('div', {className: 'overlay'}, overlay))
  265.      worksOn = (o) -> (do ([s, cls] = OS[c]) -> m("i.fab.#{cls}.os", title: s)) for c in (o.worksOn or '').split ''
  266.      m.mount overlay, view: -> do (x = _bl.custom,  o = data()) -> switch
  267.        when x then [
  268.          m('input', type: 'url', value: x.url or "", title: "URL", onclick: (-> state.active = yes), oninput: (-> x.url = @value), onchange: $custom)
  269.           m '.oslist', {class: x.url and 'shift'}, m('div', style: "flex: 1"),
  270.             _icons().map((s) -> m "i.os", class: CUSTOM_ICONS[s], title: s)
  271.             x.url and m('a.action', title: "Test", target: '_blank', href: x.url, m 'i.fas.fa-external-link-alt')
  272.          state.active and m '.tooltip',
  273.            m '.anchor', m('input', type: 'url', value: x.icon or "", title: "Icon URL", oninput: (-> x.icon = @value), onchange: $custom),
  274.              x.icon  and m 'i.action.fas.fa-eye', title: "Preview", onmouseenter: (-> preview = x.icon),  onmouseleave: (-> preview = null)
  275.            m '.anchor', m('input', type: 'url', value: x.image or "", title: "Poster URL", oninput: (-> x.image = @value), onchange: $custom),
  276.              x.image and m 'i.action.fas.fa-eye', title: "Preview", onmouseenter: (-> preview = x.image), onmouseleave: (-> preview = null)
  277.            m '.anchor.icons', Object.keys(CUSTOM_ICONS).map (s) ->
  278.              m 'i.btn', class: CUSTOM_ICONS[s] + (if s in _icons() then " selected" else ""), title: s, onclick: (-> toggleIcon s)
  279.            m '.anchor', m 'button.done', onclick: (-> [preview, state.active] = [null, no];  $custom();  no), "Done"
  280.            preview and m '.anchor', m 'img.preview', src: preview
  281.        ]
  282.        when o then [
  283.          m('input', value: state.title, title: id$() or "", onclick: (-> state.active = yes), \
  284.                     oninput: -> _delBl id(), 'ignore';  state.title = @value;  state.order = _order())
  285.          id$() and m('.oslist', m('div', style: "flex: 1"), worksOn o[ id$() ])
  286.          state.active and m('.options', state.order.map (k) -> do (x = not excluded[ eId$(k) ]) -> [
  287.            m('button.option', {key: k, disabled: not x, onclick: $$(k), title: o[k].name}
  288.              m('i.trash', class: "fas fa-trash#{if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \
  289.                               onclick: $stop(-> _setExcl k, x))
  290.              m('b', o[k].name), worksOn o[k])
  291.          ])
  292.        ]
  293.  
  294.  else if PAGE.match RE.backloggeryCreate
  295.  
  296.    BACKLOG = GM_getValue 'backlog', {}
  297.    MATCHED = objmap DATA, (_, s) -> setFn (x["#{s}Id"] for k, x of BACKLOG when x["#{s}Id"])
  298.    UNMATCHED = objmap DATA, (o, s) -> pick(o, (k for k of o when not MATCHED[s](k))...)
  299.    SETS = objmap UNMATCHED, (o) -> objmap(o, (x) -> words x.name)
  300.    excluded = GM_getValue 'exclude', {}
  301.    GM_addStyle ".os {padding-left: 1ex;  line-height: 0;  font-size: 16px}
  302.                 #names {position: absolute;  max-height: 500px;  width: 730px;  top: 50px;  left: 9px;  z-index: 2;
  303.                         display: flex;  flex-direction: column;  overflow-y: auto;  background: grey}
  304.                 #names > button {flex-shrink: 0;  height: 24px;  border-radius: 10px;  display: flex;  flex-direction: row;
  305.                                  margin-top: 1px;  text-align: left;  padding-left: 1ex;}
  306.                 #names > button > .name {flex-grow: 1;  white-space: nowrap;  overflow: hidden;  text-overflow: ellipsis}
  307.                 #names > button > i {padding-right: .5em;  color: black;  cursor: pointer}
  308.                 #names > button > * {line-height: 1.9}
  309.                 #{LOGO} .logo img {height: 100px}"
  310.    for es in $find("[name=#{s}console]").children for s in ['', 'orig_']
  311.      e.innerText = "Windows" for e in es when e.value is "PC"
  312.    status         = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]'
  313.     [name, system] = ['name', 'console'].map (s) -> $find "[name=#{s}]"
  314.     name.autocomplete = 'off'
  315.     eId = (k, s=system.value) -> "#{s.toLowerCase()}##{k}"
  316.     for e in system.children
  317.       when_(UNMATCHED[ e.value.toLowerCase() ], (o) -> e.innerText += " (+#{(k for k of o when not excluded[ eId(k, e.value) ]).length})")
  318.     $after($find('.info.help', detail2), $e('span', id: 'oslist', style: "padding-left: 1ex"))
  319.     $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex"))
  320.     $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex"))
  321.     $find_('fieldset')[0].style.position = 'relative'
  322.     $after($find('[name=name]'), $e('div', id: 'names'))
  323.     data = (k=system.value) -> UNMATCHED[ k.toLowerCase() ] or {};
  324.     _order = (text=name.value, k=system.value) -> order(SETS, excluded, text, k.toLowerCase())
  325.     state = id: null,  active: no,  order: _order()
  326.     _setExcl = (k, x) ->
  327.       excluded[ eId(k) ] = x
  328.       $mergeData('exclude', [eId(k)]: x)
  329.       state.order = _order()
  330.     _redraw = (id=state.id, o=data()[id]) ->
  331.       $clear oslist
  332.       o and $append oslist, ((do ([s, cls] = OS[c]) -> $e('i', className: "fab #{cls} os", title: s)) for c in (o.worksOn or '').split(''))...
  333.       [achievements.innerText, completed.innerText] = [o?.achievements or "", statStr(o or {}, 'completed') or o?.status or '']
  334.     $before($find_('fieldset')[0], $e('div', id: 'logo', className: 'logo'))
  335.     m.mount logo, view: -> do (k = system.value.toLowerCase(),  o = data()[state.id]) ->
  336.       o and m('a', {target: '_blank', href: o.url}, m('img', src: $logo(k, state.id)[1]))
  337.     $upd = (id) ->
  338.       _redraw id
  339.       when_ data()[id], (o) -> name.value = o.name
  340.       Object.assign state, {id}, active: not id, order: _order()
  341.       no
  342.     name.oninput = name.onclick = -> $upd();  m.redraw()
  343.     system.onchange = ->
  344.       Object.assign state, id: null, order: _order()
  345.       _redraw();  m.redraw()
  346.     document.addEventListener 'keydown', (e) -> if e.key is 'Escape'
  347.       Object.assign state, id: null, active: no
  348.       _redraw();  m.redraw()
  349.     m.mount names, view: -> state.active and
  350.       state.order.map (k) -> do (x = not excluded[eId(k)]) ->
  351.         m 'button', {key: k, disabled: not x, onclick: -> $upd(k)}, m('span.name', {title: data()[k].name}, data()[k].name),
  352.           m 'i.fas', class: "fa-trash#{if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \
  353.                      onclick: $stop(-> $keepScroll names, -> _setExcl(k, x))
  354.  
  355.   else if PAGE.match RE.backloggeryLibrary
  356.  
  357.     INCOMPLETE = ["(-)", "(u)", "(U)"]
  358.     SLUGS = objmap DATA, slugs
  359.     $assertEq(Object.keys(DATA.steam).length, Object.keys(SLUGS.steam).length, (n, m) -> "Steam names have #{n-m} collisions!")
  360.     $assertEq(Object.keys(DATA.gog).length,   Object.keys(SLUGS.gog).length,   (n, m) -> "GOG names have #{n-m} collisions!")
  361.     UPD = GM_getValue 'updated', {}
  362.     LIBRARIES = Object.keys DATA
  363.     CHANGES = do (backlog = GM_getValue('backlog', {}),  changes = new Set GM_getValue('changes', [])) ->
  364.       objfilter backlog, (x, k) -> LIBRARIES.some (s) -> changes.has "#{s}##{x[s+'Id']}"
  365.     CHANGED = Object.keys(CHANGES).sort (a, b) -> CHANGES[a].name.localeCompare CHANGES[b].name
  366.     $s = (e) -> e.innerText.trim()
  367.     info = (e) -> $find '.gamerow', e
  368.     name = (e) -> $find 'b', e
  369.     id = (e) -> query($find('a', e).href).gameid
  370.     $achievements = (e) -> $find '.info span', info e
  371.     $completion = (e) -> $find 'img', $find_('h2 a', e)[1]
  372.     $type = (s, e) -> $s(info e).match RegExp("\\b#{s}\\b", 'i')
  373.     $slug = (k, e) -> SLUGS[k][ slugify($s name e) ]
  374.     overlay = $e('div', style: "z-index:2; pointer-events:none; position:fixed; top:0; left:0; width:100%; height:100%; display:flex")
  375.     $append document.body, overlay
  376.     GM_addStyle "#{LOGO}  .logo img {max-height: 62px}  .logo.steam img {max-height: 67px}  .logo.gog img {max-height: 64px}
  377.                 .os {font-weight: 100;  padding-left: .75ex;  line-height: 0 !important;  font-size: 20px;  position: relative;  top: 2.5px}
  378.                 section.gamebox.processed .logo img {max-height: 64px}
  379.                 .tooltip {margin: auto;  align-items: center;  display: flex;  flex-direction: column;
  380.                           background: rgba(0, 0, 0, 0.8);  padding: 2em;  transform: translateZ(0) translateX(-99px)}
  381.                 .changelist {position: absolute;  top: 0;  right: 0;  pointer-events: all;  background: rgba(0, 0, 0, 0.8);
  382.                              max-width: 33%;  max-height: 50%;  display: flex;  flex-direction: column}
  383.                 .changelist.collapsed {opacity: .5}   .changelist:hover {opacity: 1}
  384.                 .changelist .items {overflow-y: auto}   .changelist .items > .item {margin: 1em}
  385.                 .changelist > h1 {cursor: pointer;  position: relative;  padding: 1em;  padding-right: 3em}
  386.                 .changelist > h1 > .right {position: absolute;  right: 0;  margin-right: 1em}"
  387.     changeListCollapsed = no
  388.     overlayData = null
  389.     $$ = (x) -> -> overlayData = x;  m.redraw()
  390.     m.mount overlay, view: -> switch
  391.       when overlayData        then m '.tooltip',
  392.         m('img', src: overlayData.image, style: "max-width: 548px")
  393.         m('pre', {style: "padding-top:1em; font-weight:bold"}, overlayData.stats)
  394.       when CHANGED.length > 0 then m '.changelist', {class: if changeListCollapsed then 'collapsed' else ""},
  395.         m 'h1', {onclick: -> changeListCollapsed = not changeListCollapsed}, "Unseen changes (#{CHANGED.length}) ",
  396.           m 'span.right', "[#{if changeListCollapsed then '+' else '–'}]"
  397.         unless changeListCollapsed then m '.items',
  398.           CHANGED.map (k) -> m '.item', m 'a', {href: "https://www.backloggery.com/update.php?user=#{PARAMS.user}&gameid=#{k}"},
  399.                                           CHANGES[k].name, (" [#{s}]" for s in LIBRARIES when CHANGES[k]["#{s}Id"])
  400.     $tweak = (e, [k, k_=k], id, [ignore, markParam, markCond], [append, appendFmt=identity], [stats, statsUpdated]) ->
  401.       [[icon, image], x] = [$logo(k, id), if k is 'custom' then id else DATA[k][id]]
  402.       _data = {image,  stats: (stats and "#{stats}\nUpdated: #{new Date(statsUpdated or UPD[k_])}")}
  403.       e.style.background = if ignore then 'darkgrey' else unless markCond(markParam e) then '' else 'lightcoral'
  404.       name(e).title = "#{x.name}\nUpdated: #{new Date if k is 'custom' then x.updated else UPD[k_]}"
  405.       name(e).innerHTML += unless append then '' else " [#{appendFmt append}]"
  406.       (do ([s, cls] = OS[c]) -> $append name(e), $e('i', className: "fab #{cls} os", title: s)) for c in (x.worksOn||'').split('')
  407.       $append name(e), $e('i', className: "#{CUSTOM_ICONS[s]} os", title: s) for s in (x.icons or "").split(' ') when s if k is 'custom'
  408.       $before e, $e('div', {className: "logo #{k}", onmouseleave: $$(), onmouseenter: $$ _data},
  409.                     $e('a', merge(target: '_blank', x.url and href: x.url), $e('img', src: icon)))
  410.     _renameWindows = (s) -> replace(s, /^PC( \(.*\))?$/, "Windows$1") or replace(s, /^(.*)\(PC\)$/, "$1(Windows)") or s
  411.     e.innerText = _renameWindows e.innerText for e in $find_ "aside .sysbox"
  412.     content.addEventListener 'DOMNodeInserted', ({target}) -> if $hasClass(target, "system title")
  413.       target.innerText = _renameWindows target.innerText
  414.     content.addEventListener 'DOMNodeInserted', ({target}) -> if $hasClass(target, 'gamebox') and info target
  415.       backlog = GM_getValue 'backlog', {}
  416.       _id = id target
  417.       _bl = backlog[_id] = Object.assign(backlog[_id] or {}, name: $s name target)
  418.       $syncId = (k) -> _bl["#{k}Id"] = _bl["#{k}Id"] or $slug(k, target);  DATA[k][ _bl["#{k}Id"] ]
  419.       _type = $find 'b', info(target)
  420.       _type.innerText = _renameWindows _type.innerText
  421.       _psn = ['ps3', 'ps4', 'psvita'].find((s) -> $type s, target)
  422.       if _bl.custom
  423.         $tweak target, ['custom'], merge(_bl.custom, name: ""), [yes, (->''), (->'')], ["custom"], ["Custom", _bl.custom.updated]
  424.       else if $type 'steam', target
  425.         data = $syncId 'steam'
  426.         stats = data?.achievements
  427.         _markCond = (e) -> stats is '?' or (not e and stats isnt "0 / 0") or (e and not e.innerText.startsWith "Achievements: #{stats} (")
  428.         data and $tweak target, ['steam'], _bl.steamId, [_bl.ignore, $achievements, _markCond],
  429.                         [data.hours_forever, (s) -> "#{s}h"], [statStr(data, 'achievements'), UPD['steam-stats']]
  430.       else if $type 'gog', target
  431.         data = $syncId 'gog'
  432.         completed = data?.completed is 'yes'
  433.         data and $tweak target, ['gog'], _bl.gogId, [_bl.ignore, $completion, (e) -> completed is INCOMPLETE.includes e.alt],
  434.                         [data.rating, (n) -> "#{n/10}/5"], [statStr(data, 'completed', 'category')]
  435.       else if $type 'humble', target
  436.         data = $syncId 'humble'
  437.         data and $tweak target, ['humble'], _bl.humbleId, [_bl.ignore, (->''), (->'')],
  438.                         [not data.icon and "Humble Trove"], [statStr(data, 'developer', 'publisher')]
  439.       else if $type 'ggate', target
  440.         data = $syncId 'ggate'
  441.         data and $tweak target, ['ggate'], _bl.ggateId, [_bl.ignore, (->''), (->'')], [], [statStr(data, 'developer', 'publisher')]
  442.       else if _psn
  443.         data = $syncId _psn
  444.         stats = data?.achievements
  445.         _markCond = (e) -> (not e and stats isnt "0 / 0") or (e and not e.innerText.startsWith "Achievements: #{stats} (")
  446.         data and $tweak target, [_psn, 'psn'], _bl["#{_psn}Id"], [_bl.ignore, $achievements, _markCond],
  447.                         [data.rank, (s) -> "#{s} rank"], [statStr(data, 'achievements', 'status', 'trophies', 'progress')]
  448.       GM_setValue 'backlog', backlog
  449.  
  450.   else if PAGE.match RE.steamLibrary
  451.  
  452.     $update 'steam', fromPairs ([o.appid, pick(o, 'link', 'logo', 'name', 'hours_forever')] for o in rgGames)
  453.  
  454.   else if PAGE.match RE.steamRecent
  455.  
  456.     stats = ([x.appid, "#{x.ach_completed} / #{x.ach_total}"] for x in rgGames when x.ach_completion)
  457.     $markUpdate 'steam-stats'
  458.     $mergeData 'steam-stats', fromPairs stats
  459.     alert "Game library interop: updated #{stats.length} games"
  460.  
  461.   else if PAGE.match RE.steamAchievements  # personal
  462.  
  463.     when_ $find('#topSummaryAchievements'), (e) -> do (id = $find('.gameLogo a').href.match(/\d+$/)[0]) ->
  464.       $mergeData('steam-stats', [id]: e.innerText.match(/(\d+) of (\d+)/)[1..].join(" / "))
  465.  
  466.   else if PAGE.match RE.steamAchievements2 # global
  467.  
  468.     when_ $find('#compareAvatar') and $find('#headerContentLeft'), (e) -> do (id = $find('.gameLogo a').href.match(/\d+$/)[0]) ->
  469.       $mergeData('steam-stats', [id]: e.innerText.match(/\d+ \/ \d+/)[0])
  470.  
  471.   else if PAGE.match RE.steamDetails
  472.  
  473.     ID = PAGE.match(RE.steamDetails)[1]
  474.     if $find '.game_area_already_owned'
  475.       platforms = $find_('.platform_img', $find '.game_area_purchase_game')
  476.       worksOn = (s[0] for s in ['win', 'linux', 'mac'] when platforms.some (e) -> $hasClass(e, s)).join('')
  477.       worksOn && $mergeData('steam-platforms', [ID]: worksOn)
  478.  
  479.   else if PAGE.match RE.steamDbDetails
  480.  
  481.     if $find '#js-app-install.btn-primary'  # it's green when the game is owned
  482.      info = $find('.span8')
  483.      id = $find_('td', info)[1].innerText
  484.      worksOn = (s[0] for s in ['windows', 'linux', 'macos'] when $find(".octicon-#{s}", info)).join('')
  485.      worksOn and $mergeData('steam-platforms', [id]: worksOn)
  486.  
  487.  else if PAGE.match RE.steamStats
  488.  
  489.    _achievements = (ss) -> (s.match(/\d+/)?[0] or s for s in ss).join(" / ")
  490.    stats = if PARAMS.DisplayType isnt '2' then do (_table = $find '.tablesorter') ->  # list
  491.                _header = $find_ 'th', $find('thead tr', _table)
  492.                _body = ($find_('td', e) for e in $find_ 'tr', $find('tbody', _table))
  493.                [_name$, _total$, _my$] = (_header.findIndex((e) -> e.innerText is s) for s in ["Name", "Total\nAch.", "Gained\nAch."])
  494.                _body.map (l) -> [query($find("a[href^='Steam_Game_Info.php']", l[_name$]).href).AppID,
  495.                                  _achievements(e.innerText for e in [l[_my$], l[_total$]])]
  496.              else do (_body = $get '/html/body/center/center/center/center') ->       # table
  497.                _table = Array.from(_body.children).find((x) -> x.tagName is 'TABLE' and not x.classList.contains 'Pager')
  498.                _ids = (query(e.href).AppID for e in $find_('a', _table))
  499.                [_ids[i], _achievements( last($find_ 'p', e).innerText.match(/Achievements: (.*) of (.*)/)[1..] )] for e, i in $find_('table', _table)
  500.     throw "Invalid update" if stats.length > 0 and not stats[0][0]? # ensuring that next layout change won't break updater
  501.     $markUpdate 'steam-stats'
  502.     $mergeData 'steam-stats', fromPairs stats
  503.     alert "Game library interop: updated #{stats.length} games"
  504.  
  505.   else if PAGE.match RE.gogLibrary
  506.  
  507.     queryPage = (page=0) -> $query "/account/getFilteredProducts?mediaType=1&page=#{page+1}"
  508.     worksOn = (o) -> o and worksOn: (k[0].toLowerCase() for k, v of o when v).join('')
  509.     scrape = -> queryPage().then (o) -> do (completed = o.tags.find((x) -> x.name.toLowerCase() is 'completed').id) ->
  510.       Promise.all([Promise.resolve(o), [1...o.totalPages].map(queryPage)...]).then (data) ->
  511.         games = [].concat(data.map((x) => x.products)...)
  512.                   .map (o) => [o.id, merge(pick(o, 'image', 'rating', 'url'), worksOn(o.worksOn),
  513.                                            name: o.title, category: o.category or undefined, completed: o.tags.includes completed)]
  514.         $update 'gog', fromPairs games
  515.     $append $find('.collection-header'),
  516.             $e('i', className: "fas fa-sync-alt _clickable account__filters-option", title: "Sync Backloggery", onclick: scrape)
  517.  
  518.   else if PAGE.match RE.humbleLibrary
  519.  
  520.     PLATFORMS = windows: 'w',  linux: 'l',  osx: 'm',  android: 'a'
  521.     url = -> ($find('.details-heading a') or {}).href
  522.     platformSelector = -> $find '.js-platform-select-holder'
  523.     worksOn = -> do (e = platformSelector()) ->
  524.       (PLATFORMS[k] for k of PLATFORMS when e.querySelector '.hb-'+k).join('')
  525.  
  526.     scrape = -> for e in $find_ '.subproduct-selector'
  527.       e.click()
  528.       name:      $find('h2', e).innerText
  529.       publisher: $find('p',  e).innerText
  530.       icon:      $find('.icon', e).style.backgroundImage.match(/^url\("(.*)"\)$/)?[1]
  531.       url:       url()
  532.       worksOn:   worksOn()
  533.     GM_addStyle "#syncBackloggery {position: absolute;  top: 28px;  left: 400px;  cursor: pointer}"
  534.     $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
  535.     $visibility loader, off
  536.     main = $find '.base-main-wrapper'
  537.     main.style.position = 'relative'
  538.     $append main, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: ->
  539.       $visibility loader, on
  540.       setTimeout ->
  541.         $update 'humble', fromPairs scrape().map (x) -> x.worksOn and [slugify(x.name), x]
  542.         $visibility loader, off)
  543.     $visibility syncBackloggery, off
  544.     forever -> when_ $find('#switch-platform'), (e) ->
  545.       $visibility(syncBackloggery, (e.value is 'all') and not search.value and location.pathname is "/home/library")
  546.  
  547.   else if PAGE.match RE.humbleTrove
  548.  
  549.     name = -> $find('.product-human-name').innerText
  550.     credits = (t) -> $find(".#{t}")?.innerText.trim()  # t in {'dev', 'pub'}
  551.     worksOn = -> (e.getAttribute('data-platform')[0] for e in $find('.platforms').children).join('')
  552.     scrape = -> $find_('#trove-main .trove-grid-item').reduce ((p, e) -> p.then (l) ->
  553.       e.click()
  554.       l.push(name: name(), developer: credits('dev'), publisher: credits('pub'), image: $find('img', e).src, worksOn: worksOn())
  555.       $find('.dismiss-action').click()
  556.       return new Promise (resolve) -> setTimeout (-> resolve l), 200
  557.     ), Promise.resolve []
  558.     $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating", style: "color: white"))
  559.     $visibility loader, off
  560.     setTimeout (-> $before $find('.trove-sorter').firstElementChild,
  561.                            $e('i', className: "fas fa-sync-alt", style: "cursor: pointer", title: "Sync Backloggery", onclick: ->
  562.                               $visibility loader, on
  563.                               scrape().then (xs) ->
  564.                                 $update 'humble-trove', fromPairs([slugify(x.name), x] for x in xs)
  565.                                 $visibility loader, off)),
  566.                1000
  567.  
  568.   else if PAGE.match RE.ggateLibrary
  569.  
  570.     PLATFORMS = pc: 'w',  linux: 'l',  mac: 'm',  android: 'a'
  571.     worksOn = (e) -> x.src.match(/inline_(pc|linux|mac|android)\.png$/)?[1] for x in $find_ 'img', e
  572.     loadImage = (id) -> new Promise (resolve) ->
  573.       wait = ({target}) -> when_ target.firstChild and $find('.boximg', target), (img) ->
  574.         [dev, pub] = ["Developer", "Publisher"].map (s) -> $get("""//li[span = "#{s}: "]//a""", target)?.innerText
  575.         lib_rightcol_info.removeEventListener 'DOMNodeInserted', wait
  576.         setTimeout -> resolve [img.src, dev, pub]
  577.       lib_rightcol_info.addEventListener 'DOMNodeInserted', wait
  578.       Library.loadinfo 'game', "sku=#{id}&tab=details"
  579.     scrape = ->
  580.       $find_('.mygame_item').map((e) -> $find_('a.ttl', e))
  581.         .reduce ((p, [icon, name]) -> p.then (o) -> do (id = query(icon.href).sku) =>
  582.                   loadImage(id).then ([image, developer, publisher]) ->
  583.                     Object.assign(o, [id]: {
  584.                       image, developer, publisher,
  585.                       name:    name.title,
  586.                       icon:    $find('img', icon).src,
  587.                       worksOn: worksOn(name).map((s) -> PLATFORMS[s]).join('')
  588.                    })
  589.                 ), Promise.resolve {}
  590.     GM_addStyle "#syncBackloggery {cursor: pointer;  padding: 1ex}"
  591.     $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
  592.     $visibility loader, off
  593.     when_ $find('h1.icon'), (e) ->
  594.       $append e, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: ->
  595.         $visibility loader, on
  596.         scrape().then (o) ->
  597.           $update 'ggate', o
  598.           $visibility loader, off)
  599.       forever -> $visibility syncBackloggery, window.location.pathname is '/account/games' and
  600.                                               $find('[name=platform][value=""]')?.checked and not $find('[name=filter]').value
  601.  
  602.   else if PSN_ID and PAGE.match(RE.psnLibrary)?[1] is PSN_ID
  603.  
  604.     PANEL = $get "../../..", $find ".dropdown-toggle.completion"
  605.     GAMES = $find '#gamesTable'
  606.     if ['search', 'completion', 'pf'].every (s) -> s not of PARAMS
  607.       $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
  608.       $visibility loader, off
  609.       _loading = -> $find('#table-loading', GAMES)
  610.       load = -> new Promise (resolve) ->
  611.         unless $find '#load-more', GAMES
  612.           resolve()
  613.         else
  614.           loadMoreGames()
  615.           waiting = forever -> unless $find '#table-loading', GAMES
  616.             clearInterval waiting
  617.             resolve load()
  618.  
  619.       TROPHIES = ['gold', 'silver', 'bronze']
  620.       _achievements = (s) => (replace(s, /All (\d+)/, "$1 of $1") or s).match(/(\d+) of (\d+)/)[1..].join " / "
  621.       convert = (x) -> [$find('a', x).href.match(RE.psnDetails)[1],
  622.                         name: $find('.title', x).innerText,   icon: $find("picture source", x).srcset.match("^.*, (.*) 1.1x$")[1],
  623.                         rank: $find('.game-rank', x).innerText,   progress: $find('.progress-bar', x).innerText,
  624.                         achievements: _achievements($find('.small-info', x).innerText),
  625.                         platforms: $find_('.platform', x).map((y) -> _PSN_HW[y.innerText]).join(''),
  626.                         status: ['completion', 'platinum'].filter((s) -> $find ".#{s}.earned", x).join(", ") or undefined,
  627.                         trophies: $find('.trophy-count div', x).innerText.split('\n').map((s, i) -> "#{s} #{TROPHIES[i]}").join(", ")]
  628.  
  629.       $append PANEL.firstElementChild,
  630.               $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", style: "cursor: pointer;  color: white", title: "Sync Backloggery", onclick: ->
  631.                    $visibility loader, on
  632.                    load().then ->
  633.                      $visibility loader, off
  634.                      $update 'psn', fromPairs $find_('tr', GAMES).map convert)
  635.       forever -> $visibility syncBackloggery, GAMES.style.display isnt 'none'
  636.  
  637.   else if PSN_ID and PAGE.match(RE.psnDetails)?[2] is PSN_ID
  638.  
  639.     GAME_ID = PAGE.match(RE.psnDetails)[1]
  640.     $mergeData 'psn-img', [GAME_ID]: $find('.game-image-holder a').href
  641.  
  642. `;
  643. eval( CoffeeScript.compile(inline_src) );
  644.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement