Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Backloggery interop
- // @namespace http://tampermonkey.net/
- // @version 0.8.10
- // @description Backloggery integration with game library websites
- // @author LeXofLeviafan
- // @include *://www.backloggery.com/games.php?*
- // @include *://www.backloggery.com/update.php?*
- // @include *://www.backloggery.com/newgame.php?*
- // @include *://steamcommunity.com/id/star_fang/games/*
- // @exclude *://steamcommunity.com/id/<username>/games/*
- // @include *://steamcommunity.com/id/star_fang/stats/*
- // @exclude *://steamcommunity.com/id/<username>/stats/*
- // @include *://steamcommunity.com/stats/star_fang/achievements
- // @include *://steamcommunity.com/stats/*/achievements/*
- // @include *://store.steampowered.com/app/*
- // @include *://steamdb.info/app/*
- // @include *://astats.astats.nl/astats/User_Games.php?*
- // @include *://www.gog.com/account
- // @include *://www.humblebundle.com/home/*
- // @include *://www.humblebundle.com/subscription/trove
- // @include *://*.gamersgate.com/account/*
- // @include *://psnprofiles.com/<username>
- // @include *://psnprofiles.com/*?*
- // @include *://psnprofiles.com/trophies/*
- // @exclude *://psnprofiles.com/<username>
- // @require https://cdnjs.cloudflare.com/ajax/libs/mithril/2.0.4/mithril.min.js
- // @require https://cdn.jsdelivr.net/npm/[email protected]/lib/coffeescript-browser-compiler-legacy/coffeescript.js
- // @grant GM_info
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_addStyle
- // ==/UserScript==
- var inline_src = String.raw`
- ROMANS = {Ⅰ: 'I', Ⅱ: 'II', Ⅲ: 'III', Ⅳ: 'IV', Ⅴ: 'V', Ⅵ: 'VI', Ⅶ: 'VII', Ⅷ: 'VIII', Ⅸ: 'IX', Ⅹ: 'X', Ⅺ: 'XI', Ⅻ: 'XII', Ⅼ: 'L', Ⅽ: 'C', Ⅾ: 'D', Ⅿ: 'M'}
- roman = RegExp("[#{Object.keys(ROMANS).join('')}]", 'g')
- identity = (x) -> x
- merge = (os...) -> Object.assign {}, os...
- fromPairs = (pairs) -> merge ([k]: v for [k, v] in pairs.filter identity)...
- keymap = (ks, f) -> fromPairs ([k, f k] for k in ks)
- objmap = (o, f) -> fromPairs ([k, f(v, k, o)] for k, v of o)
- objfilter = (o, f) -> fromPairs ([k, v] for k, v of o when f(v, k, o))
- pick = (o, keys...) -> fromPairs ([k, o[k]] for k in keys when k of o)
- method = (o, k, def=->) -> o?[k]?.bind?(o) or def
- setFn = (xs) -> method (new Set xs), 'has'
- last = (l) -> l[l.length - 1]
- when_ = (x, f) -> x and f(x)
- replace = (s, re, pattern) -> s.match(re) and s.replace(re, pattern)
- qstr = (s) -> if not s.includes('?') then "" else s[1 + s.indexOf '?'..]
- query = (s) -> fromPairs (l[1..] for l in qstr(s).split('&').map((s) -> s.match /([^=]+)=(.*)/) when l)
- slugify = (s) -> s.replace(roman, (c) -> ROMANS[c]).toLowerCase().replace(/[.]/g, '').replace(/[^a-z0-9+]+/g, '-').replace(/(^-*|-*$)/g, '')
- capitalize = (s) -> do (z = "#{s}") -> z[...1].toUpperCase() + z[1..]
- statStr = (o, ks...) -> ("#{capitalize k}: #{o[k]}" for k in ks when k of o).join '\n'
- forever = (f) -> setInterval f, 100
- PAGE = location.href
- PARAMS = query location.search
- RE =
- backloggeryUpdate: "backloggery\\.com/update\\.php"
- backloggeryCreate: "backloggery\\.com/newgame\\.php"
- backloggeryLibrary: "backloggery\\.com/games\\.php"
- steamLibrary: "steamcommunity\\.com/id/[^/]+/games/\\?tab=all"
- steamRecent: "steamcommunity\\.com/id/[^/]+/games($|/$|/\\?tab=recent)"
- steamAchievements: "steamcommunity\\.com/id/[^/]+/stats/[^/]+"
- steamAchievements2: "steamcommunity\\.com/stats/[^/]+/achievements"
- steamDetails: "store\\.steampowered\\.com/app/([^/]+)"
- steamDbDetails: "steamdb\\.info/app/[^/]+"
- steamStats: "astats\\.astats\\.nl/astats/User_Games\\.php"
- gogLibrary: "gog\\.com/account"
- humbleLibrary: "humblebundle\\.com/home/(library|purchases|keys|coupons)"
- humbleTrove: "humblebundle\\.com/subscription/trove"
- ggateLibrary: "gamersgate\\.com/account/(games|wishlist|achievements)" # they share a page and can switch without reload
- psnLibrary: "psnprofiles\\.com/([^/?]+)/?($|\\?)"
- psnDetails: "psnprofiles\\.com/trophies/([^/?]+)/([^/?]+)$"
- PSN_ID = (GM_info.script.options.override.use_includes or []).reduce ((x, s) -> x or s.match(RE.psnLibrary)?[1]), null
- _PSN_HW = {PS3: '3', PS4: '4', VITA: 'V'}
- _psnData = (images) -> (id) -> objmap(objfilter(GM_getValue('psn', {}), (x) -> _PSN_HW[id] in x.platforms),
- (x, k) -> merge x, image: images[k], url: "https://psnprofiles.com/trophies/#{k}/#{PSN_ID}")
- DATA = do (TROVE = objmap(GM_getValue('humble-trove', {}), (o) -> merge o, url: "https://www.humblebundle.com/monthly/trove")
- STATS = GM_getValue('steam-stats', {}), PLATFORMS = GM_getValue('steam-platforms', {}),
- psnData = _psnData(GM_getValue 'psn-img', {})) ->
- steam: objmap GM_getValue('steam', {}), (x, k) -> merge(x, url: x.link, achievements: STATS[k] or '?', worksOn: PLATFORMS[k] or 's')
- gog: objmap GM_getValue('gog', {}), (x) -> merge(x, url: "https://gog.com#{x.url}", completed: if x.completed then 'yes' else 'no')
- humble: objmap merge(TROVE, GM_getValue 'humble'), (x, id) -> merge(TROVE[id], x)
- ggate: objmap GM_getValue('ggate', {}), (x, id) -> merge(x, url: "https://gamersgate.com/#{id}")
- ps3: psnData('PS3'), ps4: psnData('PS4'), psvita: psnData('VITA')
- OS = w: ["Windows", 'fa-windows'], l: ["Linux", 'fa-linux'], m: ["MacOS", 'fa-apple'], a: ["Android", 'fa-android'], s: ["Steam", 'fa-steam']
- CUSTOM_ICONS = {steam: "fab fa-steam", windows: "fab fa-windows", linux: "fab fa-linux", mac: "fab fa-apple", android: "fab fa-android", \
- console: "fas fa-gamepad", xbox: "fab fa-xbox", playstation: "fab fa-playstation", web: "fab fa-html5", \
- nodejs: "fab fa-node-js", flash: "fab fa-adobe", dice: "fas fa-dice", d20: "fas fa-dice-d20", trophy: "fas fa-trophy"}
- slugs = (o) -> fromPairs ([slugify(v.name), k] for k, v of o)
- $clear = (e) -> e.removeChild e.firstChild while e.firstChild; e
- $append = (parent, children...) -> parent.appendChild e for e in children; parent
- $before = (neighbour, children...) -> neighbour.parentElement.insertBefore(e, neighbour) for e in children; neighbour
- $after = (neighbour, children...) -> $before(neighbour.nextSibling, children...); neighbour
- $e = (tag, options, children...) -> $append Object.assign(document.createElement(tag), options), children...
- $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
- $hasClass = (e, clss) -> do (l = e.classList) -> l and clss.split(' ').every((s) -> not s or l.contains s)
- $visibility = (e, x) -> e.style.visibility = if x then 'visible' else 'hidden'
- $markUpdate = (k) -> GM_setValue 'updated', merge(GM_getValue('updated'), [k]: +new Date)
- $assertEq = (a, b, err) -> a is b or alert(if typeof err isnt 'function' then err else err a, b)
- $stop = (f=->) -> (e) -> f e; e.stopPropagation(); no
- $keepScroll = (e, f) -> do (x = e.scrollTop) -> f(); m.redraw(); e.scrollTop = x
- $query = (url) -> new Promise (resolve, reject) -> do (xhr = new XMLHttpRequest) ->
- xhr.open 'GET', url
- [xhr.onerror, xhr.onload] = [reject, -> resolve JSON.parse xhr.response]
- xhr.send()
- words = (s) -> slugify(s).split('-').sort().reverse()
- matching = (ss, zs) -> do (res = 0, i = 0, j = 0) ->
- while i < ss.length and j < zs.length
- [s, z] = [ss[i], zs[j]]
- if s is z
- i++; j++; res += 2
- else if z.startsWith s
- i++; j++; res += 1
- else
- if s < z then j++ else i++
- res
- order = (sets, exclude, text, k) -> do (d = DATA[k], l = words(text), f = (s) -> not exclude["#{k}##{s}"]) ->
- o = objmap(sets[k] or {}, (ss) -> matching l, ss)
- 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
- $addChanges = (newChanges) -> do (changes = GM_getValue 'changes', []) ->
- oldChanges = new Set changes
- GM_setValue 'changes', [changes..., (id for id in newChanges when not oldChanges.has id)...]
- WATCH_FIELDS = "name worksOn completed achievements platforms status trophies".split ' '
- WATCH_META = {'steam-stats': 'steam', 'steam-platforms': 'steam'}
- WATCH_LIBRARY = {'humble-trove': 'humble'}
- $update = (library, games1) -> do (library_ = WATCH_LIBRARY[library] or library, games0 = GM_getValue library, {}) ->
- [ids1, ids0] = [games1, games0].map Object.keys
- removed = (id for id in ids0 when id not of games1)
- added = (id for id in ids1 when id not of games0)
- updated = (id for id in ids0 when id of games1 and WATCH_FIELDS.some (k) -> games0[id][k] isnt games1[id][k])
- $markUpdate library
- $addChanges ("#{library_}##{id}" for id in [removed..., updated...])
- GM_setValue library, games1
- setTimeout -> alert "Backloggery interop: added #{added.length} games, removed #{removed.length} games"
- $mergeData = (k, o) -> do (library = WATCH_META[k], old = GM_getValue k, {}) ->
- library and $addChanges ("#{library}##{id}" for id in Object.keys o when id of old and old[id] isnt o[id])
- GM_setValue k, merge(old, o)
- $logo = (k, id) -> do (o = if k is 'custom' then id else DATA[k][id]) -> switch k
- when 'steam' then [o.logo, "https://steamcdn-a.akamaihd.net/steam/apps/#{id}/header.jpg"]
- when 'gog' then [196, 392].map((x) -> "https:#{o.image}_#{x}.jpg")
- else [o.icon or o.image, o.image or o.icon]
- $append document.head, $e('link', rel: 'stylesheet', href: "https://use.fontawesome.com/releases/v5.7.0/css/all.css")
- GM_addStyle "#loader {position: fixed; top: 50%; left: 50%; z-index: 10000; transform: translate(-50%, -50%);
- font-size: 300px; text-shadow: -1px 0 grey, 0 1px grey, 1px 0 grey, 0 -1px grey}"
- GM_addStyle "@-webkit-keyframes rotation {from {-webkit-transform:rotate(0deg)}
- to {-webkit-transform:rotate(360deg)}}
- @keyframes rotation {from {transform:rotate(0deg) translate(-50%, -50%); -webkit-transform:rotate(0deg)}
- to {transform:rotate(360deg) translate(-50%, -50%); -webkit-transform:rotate(360deg)}}"
- GM_addStyle ".rotating {animation: rotation 2s linear infinite}"
- LOGO = ".logo {height: 0; width: 0; display: flex; flex-direction: row-reverse}
- .logo img {border: 1px solid darkorchid; background: #1b222f}"
- if PAGE.match RE.backloggeryUpdate
- SETS = objmap DATA, (o) -> objmap(o, (x) -> words x.name)
- legend = $get '//*[@id="content-wide"]/section/form/fieldset[1]/legend'
- systemDropdown = $get '//*[@id="content-wide"]/section/form/fieldset[1]/div[2]'
- delBtns = $get '//*[@id="content-wide"]/section/form/div[2]/div'
- status = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]'
- _system = when_ systemDropdown, (e) -> $find('select', e)
- swap = -> do ([a, b] = [_system, $find '#detail2 select']) -> [a.value, b.value] = [b.value, a.value]
- do (_bl = GM_getValue('backlog', {})[PARAMS.gameid]) ->
- k = Object.keys(DATA).find (k) -> _bl?[k+'Id']
- changeId = k and "#{k}##{_bl[k+'Id']}"
- GM_setValue 'changes', (id for id in GM_getValue('changes', []) when id isnt changeId)
- unless legend and systemDropdown and delBtns # deleted/doesn't exist
- backlog = GM_getValue('backlog', {})
- if PARAMS.gameid in backlog
- delete backlog[PARAMS.gameid]
- GM_setValue('backlog', backlog)
- else
- for es in $find("[name=#{s}console]").children for s in ['', 'orig_']
- e.innerText = "Windows" for e in es when e.value is "PC"
- $append systemDropdown, $e('tt', innerText: "⇄", onclick: swap, style: "cursor: pointer; padding-left: 8px; font-size: large")
- $before delBtns, $e('input', type: 'submit', name: 'submit2', className: 'greengray', value: "Stealth Save ⇄", onclick: swap)
- $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex"))
- $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex"))
- GM_addStyle ".overlay {position: relative; max-height: 0; top: -40px; z-index: 2; margin-right: 10px; display: flex; flex-direction: row-reverse}
- .overlay input {height: 20px; background: #4b4b4b; color: white; width: 500px; border: 1px solid black; padding-left: 1ex; margin-bottom: 0}
- .overlay .options {display: flex; flex-direction: column; max-height: 500px; overflow-y: auto; background: grey}
- .overlay button {height: 28px; background: #4b4b4b; color: white; border-radius: 10px 8px 8px 10px; padding: 5px}
- .overlay .option {white-space: nowrap; display: flex; margin: .5px} .overlay .trash {cursor: pointer}
- .overlay * {flex-shrink: 0} .overlay button b {flex: 1; padding-left: 1ex; text-align: left; overflow: hidden; text-overflow: ellipsis}
- .oslist {display: flex; position: absolute; width: 505px; padding-top: 7.5px; pointer-events: none} .oslist.shift {padding-right: 20px}
- .os {padding-left: .75ex; font-size: 20px; color: white} .oslist .action {padding-left: 1ex; pointer-events: all}
- .action {color: white; cursor: pointer} .anchor .action {position: absolute; top: 10px; right: 7.5px}
- .iconlist {display: flex; position: absolute; width: 505px; padding-top: 7.5px; pointer-events: none; color: white !important}
- fieldset, .anchor {position: relative} .tooltip {background: rgba(0, 0, 0, 0.8)}
- .icons {padding: 1ex; padding-bottom: unset} .icons .btn {margin: .25em} .btn.selected {border-color: white}
- .btn {background: #4b4b4b; color: white; border: 1px solid black; cursor: pointer; font-size: 20px; padding: 2px; border-radius: 5px}
- .btn.fa-eye {margin-left: 1.25px} .done, .preview {display: block; margin: 1ex auto} .done {width: 90%; cursor: pointer}
- #{LOGO} .logo img {height: 100px} .preview {border: 1px solid darkorchid; max-width: calc(100% - 2ex)}"
- gameName = $find '[name=name]'
- _bl = GM_getValue('backlog')?[PARAMS.gameid] or {}
- excluded = GM_getValue('exclude', {})
- data = (k=state.list) -> DATA[k]
- id = (s=state.list) -> "#{s}Id"
- id$ = (s=state.list) -> _bl[ id(s) ]
- eId$ = (k=id$(s), s=state.list) -> "#{s}##{k}"
- data$ = (s=state.list) -> data(s)?[ id$(s) ]
- title = (k=state.list) -> data$(k)?.name or gameName.value
- _icons = -> (_bl.custom?.icons or "").split ' '
- _order = (s=state.title, k=state.list) -> order(SETS, excluded, s, k)
- state = do (list = (_system.value or '').toLowerCase()) ->
- list: list
- title: title list
- active: no
- order: _order(title(list), list)
- when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "", statStr(o, 'completed') or o.status or '']
- _setBl = (o) -> $mergeData 'backlog', [PARAMS.gameid]: Object.assign(_bl, o)
- _delBl = (...ks) -> delete _bl[k] for k in ks; _setBl()
- _setExcl = (k, x) ->
- excluded[ eId$(k) ] = x
- $mergeData('exclude', [eId$(k)]: x)
- state.order = _order()
- if id$() and not data$() then _delBl id(), 'ignore'
- section2 = $find_('fieldset')[1]
- $append section2, $e('div', id: 'ignore', style: "position: absolute; top: -25px; left: 110px")
- m.mount ignore, view: -> id$() and [
- 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))
- ]
- section1 = $find_('fieldset')[0]
- $before section1, $e('div', id: 'logo', className: 'logo')
- m.mount logo, view: -> switch
- when _bl.custom then m('a', {target: '_blank', href: _bl.custom.url}, m('img', src: $logo('custom', _bl.custom)[1]))
- when id$() then m('a', {target: '_blank', href: data$().url}, m('img', src: $logo(state.list, id$())[1]))
- $append section1, $e('div', id: 'custom', style: "position: absolute; top: -25px; left: 170px")
- toggleCustom = (x) -> ->
- state.active = no
- if _bl.custom then _delBl('custom') else _delBl(id(), 'ignore'); _setBl custom: {}
- 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)
- preview = null
- document.addEventListener 'keydown', (e) -> if e.key is 'Escape'
- _delBl id(), 'ignore'
- Object.assign(state, active: no, title: title(), order: _order title())
- preview = achievements.innerText = completed.innerText = ""
- m.redraw()
- $reset = -> do (list = (_system.value or '').toLowerCase()) -> if list isnt state.list
- Object.assign(state, {list}, active: no, title: title(list), order: _order(title(list), list))
- m.redraw()
- $$ = (k) -> ->
- Object.assign(state, active: no, title: data()[k].name)
- state.order = _order()
- _setBl([id()]: k)
- when_ data$(), (o) -> [achievements.innerText, completed.innerText] = [o.achievements or "", statStr(o, 'completed') or o.status or '']
- no
- $custom = -> _setBl custom: Object.assign _bl.custom, updated: +new Date
- toggleIcon = (s) -> do (icons = _icons()) ->
- _bl.custom.icons = (if s not in icons then [icons..., s] else icons.filter (x) -> x isnt s).join(' ').trim()
- $custom()
- gameName.onchange = _system.onchange = $reset
- overlay = $e('div', style: "display: flex; flex-direction: column; width: calc(505px + 1ex); position: relative")
- $after(legend, $e('div', {className: 'overlay'}, overlay))
- worksOn = (o) -> (do ([s, cls] = OS[c]) -> m("i.fab.#{cls}.os", title: s)) for c in (o.worksOn or '').split ''
- m.mount overlay, view: -> do (x = _bl.custom, o = data()) -> switch
- when x then [
- m('input', type: 'url', value: x.url or "", title: "URL", onclick: (-> state.active = yes), oninput: (-> x.url = @value), onchange: $custom)
- m '.oslist', {class: x.url and 'shift'}, m('div', style: "flex: 1"),
- _icons().map((s) -> m "i.os", class: CUSTOM_ICONS[s], title: s)
- x.url and m('a.action', title: "Test", target: '_blank', href: x.url, m 'i.fas.fa-external-link-alt')
- state.active and m '.tooltip',
- m '.anchor', m('input', type: 'url', value: x.icon or "", title: "Icon URL", oninput: (-> x.icon = @value), onchange: $custom),
- x.icon and m 'i.action.fas.fa-eye', title: "Preview", onmouseenter: (-> preview = x.icon), onmouseleave: (-> preview = null)
- m '.anchor', m('input', type: 'url', value: x.image or "", title: "Poster URL", oninput: (-> x.image = @value), onchange: $custom),
- x.image and m 'i.action.fas.fa-eye', title: "Preview", onmouseenter: (-> preview = x.image), onmouseleave: (-> preview = null)
- m '.anchor.icons', Object.keys(CUSTOM_ICONS).map (s) ->
- m 'i.btn', class: CUSTOM_ICONS[s] + (if s in _icons() then " selected" else ""), title: s, onclick: (-> toggleIcon s)
- m '.anchor', m 'button.done', onclick: (-> [preview, state.active] = [null, no]; $custom(); no), "Done"
- preview and m '.anchor', m 'img.preview', src: preview
- ]
- when o then [
- m('input', value: state.title, title: id$() or "", onclick: (-> state.active = yes), \
- oninput: -> _delBl id(), 'ignore'; state.title = @value; state.order = _order())
- id$() and m('.oslist', m('div', style: "flex: 1"), worksOn o[ id$() ])
- state.active and m('.options', state.order.map (k) -> do (x = not excluded[ eId$(k) ]) -> [
- m('button.option', {key: k, disabled: not x, onclick: $$(k), title: o[k].name}
- m('i.trash', class: "fas fa-trash#{if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \
- onclick: $stop(-> _setExcl k, x))
- m('b', o[k].name), worksOn o[k])
- ])
- ]
- else if PAGE.match RE.backloggeryCreate
- BACKLOG = GM_getValue 'backlog', {}
- MATCHED = objmap DATA, (_, s) -> setFn (x["#{s}Id"] for k, x of BACKLOG when x["#{s}Id"])
- UNMATCHED = objmap DATA, (o, s) -> pick(o, (k for k of o when not MATCHED[s](k))...)
- SETS = objmap UNMATCHED, (o) -> objmap(o, (x) -> words x.name)
- excluded = GM_getValue 'exclude', {}
- GM_addStyle ".os {padding-left: 1ex; line-height: 0; font-size: 16px}
- #names {position: absolute; max-height: 500px; width: 730px; top: 50px; left: 9px; z-index: 2;
- display: flex; flex-direction: column; overflow-y: auto; background: grey}
- #names > button {flex-shrink: 0; height: 24px; border-radius: 10px; display: flex; flex-direction: row;
- margin-top: 1px; text-align: left; padding-left: 1ex;}
- #names > button > .name {flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis}
- #names > button > i {padding-right: .5em; color: black; cursor: pointer}
- #names > button > * {line-height: 1.9}
- #{LOGO} .logo img {height: 100px}"
- for es in $find("[name=#{s}console]").children for s in ['', 'orig_']
- e.innerText = "Windows" for e in es when e.value is "PC"
- status = $get '//*[@id="content-wide"]/section/form/fieldset[2]/div[1]'
- [name, system] = ['name', 'console'].map (s) -> $find "[name=#{s}]"
- name.autocomplete = 'off'
- eId = (k, s=system.value) -> "#{s.toLowerCase()}##{k}"
- for e in system.children
- when_(UNMATCHED[ e.value.toLowerCase() ], (o) -> e.innerText += " (+#{(k for k of o when not excluded[ eId(k, e.value) ]).length})")
- $after($find('.info.help', detail2), $e('span', id: 'oslist', style: "padding-left: 1ex"))
- $after($find('.info.help', detail5), $e('b', id: 'achievements', style: "padding: 3.5ex"))
- $after($find('.info.help', status), $e('b', id: 'completed', style: "padding: 1ex"))
- $find_('fieldset')[0].style.position = 'relative'
- $after($find('[name=name]'), $e('div', id: 'names'))
- data = (k=system.value) -> UNMATCHED[ k.toLowerCase() ] or {};
- _order = (text=name.value, k=system.value) -> order(SETS, excluded, text, k.toLowerCase())
- state = id: null, active: no, order: _order()
- _setExcl = (k, x) ->
- excluded[ eId(k) ] = x
- $mergeData('exclude', [eId(k)]: x)
- state.order = _order()
- _redraw = (id=state.id, o=data()[id]) ->
- $clear oslist
- o and $append oslist, ((do ([s, cls] = OS[c]) -> $e('i', className: "fab #{cls} os", title: s)) for c in (o.worksOn or '').split(''))...
- [achievements.innerText, completed.innerText] = [o?.achievements or "", statStr(o or {}, 'completed') or o?.status or '']
- $before($find_('fieldset')[0], $e('div', id: 'logo', className: 'logo'))
- m.mount logo, view: -> do (k = system.value.toLowerCase(), o = data()[state.id]) ->
- o and m('a', {target: '_blank', href: o.url}, m('img', src: $logo(k, state.id)[1]))
- $upd = (id) ->
- _redraw id
- when_ data()[id], (o) -> name.value = o.name
- Object.assign state, {id}, active: not id, order: _order()
- no
- name.oninput = name.onclick = -> $upd(); m.redraw()
- system.onchange = ->
- Object.assign state, id: null, order: _order()
- _redraw(); m.redraw()
- document.addEventListener 'keydown', (e) -> if e.key is 'Escape'
- Object.assign state, id: null, active: no
- _redraw(); m.redraw()
- m.mount names, view: -> state.active and
- state.order.map (k) -> do (x = not excluded[eId(k)]) ->
- m 'button', {key: k, disabled: not x, onclick: -> $upd(k)}, m('span.name', {title: data()[k].name}, data()[k].name),
- m 'i.fas', class: "fa-trash#{if x then '' else '-restore'}-alt", title: (if x then "Exclude" else "Restore"), \
- onclick: $stop(-> $keepScroll names, -> _setExcl(k, x))
- else if PAGE.match RE.backloggeryLibrary
- INCOMPLETE = ["(-)", "(u)", "(U)"]
- SLUGS = objmap DATA, slugs
- $assertEq(Object.keys(DATA.steam).length, Object.keys(SLUGS.steam).length, (n, m) -> "Steam names have #{n-m} collisions!")
- $assertEq(Object.keys(DATA.gog).length, Object.keys(SLUGS.gog).length, (n, m) -> "GOG names have #{n-m} collisions!")
- UPD = GM_getValue 'updated', {}
- LIBRARIES = Object.keys DATA
- CHANGES = do (backlog = GM_getValue('backlog', {}), changes = new Set GM_getValue('changes', [])) ->
- objfilter backlog, (x, k) -> LIBRARIES.some (s) -> changes.has "#{s}##{x[s+'Id']}"
- CHANGED = Object.keys(CHANGES).sort (a, b) -> CHANGES[a].name.localeCompare CHANGES[b].name
- $s = (e) -> e.innerText.trim()
- info = (e) -> $find '.gamerow', e
- name = (e) -> $find 'b', e
- id = (e) -> query($find('a', e).href).gameid
- $achievements = (e) -> $find '.info span', info e
- $completion = (e) -> $find 'img', $find_('h2 a', e)[1]
- $type = (s, e) -> $s(info e).match RegExp("\\b#{s}\\b", 'i')
- $slug = (k, e) -> SLUGS[k][ slugify($s name e) ]
- overlay = $e('div', style: "z-index:2; pointer-events:none; position:fixed; top:0; left:0; width:100%; height:100%; display:flex")
- $append document.body, overlay
- GM_addStyle "#{LOGO} .logo img {max-height: 62px} .logo.steam img {max-height: 67px} .logo.gog img {max-height: 64px}
- .os {font-weight: 100; padding-left: .75ex; line-height: 0 !important; font-size: 20px; position: relative; top: 2.5px}
- section.gamebox.processed .logo img {max-height: 64px}
- .tooltip {margin: auto; align-items: center; display: flex; flex-direction: column;
- background: rgba(0, 0, 0, 0.8); padding: 2em; transform: translateZ(0) translateX(-99px)}
- .changelist {position: absolute; top: 0; right: 0; pointer-events: all; background: rgba(0, 0, 0, 0.8);
- max-width: 33%; max-height: 50%; display: flex; flex-direction: column}
- .changelist.collapsed {opacity: .5} .changelist:hover {opacity: 1}
- .changelist .items {overflow-y: auto} .changelist .items > .item {margin: 1em}
- .changelist > h1 {cursor: pointer; position: relative; padding: 1em; padding-right: 3em}
- .changelist > h1 > .right {position: absolute; right: 0; margin-right: 1em}"
- changeListCollapsed = no
- overlayData = null
- $$ = (x) -> -> overlayData = x; m.redraw()
- m.mount overlay, view: -> switch
- when overlayData then m '.tooltip',
- m('img', src: overlayData.image, style: "max-width: 548px")
- m('pre', {style: "padding-top:1em; font-weight:bold"}, overlayData.stats)
- when CHANGED.length > 0 then m '.changelist', {class: if changeListCollapsed then 'collapsed' else ""},
- m 'h1', {onclick: -> changeListCollapsed = not changeListCollapsed}, "Unseen changes (#{CHANGED.length}) ",
- m 'span.right', "[#{if changeListCollapsed then '+' else '–'}]"
- unless changeListCollapsed then m '.items',
- CHANGED.map (k) -> m '.item', m 'a', {href: "https://www.backloggery.com/update.php?user=#{PARAMS.user}&gameid=#{k}"},
- CHANGES[k].name, (" [#{s}]" for s in LIBRARIES when CHANGES[k]["#{s}Id"])
- $tweak = (e, [k, k_=k], id, [ignore, markParam, markCond], [append, appendFmt=identity], [stats, statsUpdated]) ->
- [[icon, image], x] = [$logo(k, id), if k is 'custom' then id else DATA[k][id]]
- _data = {image, stats: (stats and "#{stats}\nUpdated: #{new Date(statsUpdated or UPD[k_])}")}
- e.style.background = if ignore then 'darkgrey' else unless markCond(markParam e) then '' else 'lightcoral'
- name(e).title = "#{x.name}\nUpdated: #{new Date if k is 'custom' then x.updated else UPD[k_]}"
- name(e).innerHTML += unless append then '' else " [#{appendFmt append}]"
- (do ([s, cls] = OS[c]) -> $append name(e), $e('i', className: "fab #{cls} os", title: s)) for c in (x.worksOn||'').split('')
- $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'
- $before e, $e('div', {className: "logo #{k}", onmouseleave: $$(), onmouseenter: $$ _data},
- $e('a', merge(target: '_blank', x.url and href: x.url), $e('img', src: icon)))
- _renameWindows = (s) -> replace(s, /^PC( \(.*\))?$/, "Windows$1") or replace(s, /^(.*)\(PC\)$/, "$1(Windows)") or s
- e.innerText = _renameWindows e.innerText for e in $find_ "aside .sysbox"
- content.addEventListener 'DOMNodeInserted', ({target}) -> if $hasClass(target, "system title")
- target.innerText = _renameWindows target.innerText
- content.addEventListener 'DOMNodeInserted', ({target}) -> if $hasClass(target, 'gamebox') and info target
- backlog = GM_getValue 'backlog', {}
- _id = id target
- _bl = backlog[_id] = Object.assign(backlog[_id] or {}, name: $s name target)
- $syncId = (k) -> _bl["#{k}Id"] = _bl["#{k}Id"] or $slug(k, target); DATA[k][ _bl["#{k}Id"] ]
- _type = $find 'b', info(target)
- _type.innerText = _renameWindows _type.innerText
- _psn = ['ps3', 'ps4', 'psvita'].find((s) -> $type s, target)
- if _bl.custom
- $tweak target, ['custom'], merge(_bl.custom, name: ""), [yes, (->''), (->'')], ["custom"], ["Custom", _bl.custom.updated]
- else if $type 'steam', target
- data = $syncId 'steam'
- stats = data?.achievements
- _markCond = (e) -> stats is '?' or (not e and stats isnt "0 / 0") or (e and not e.innerText.startsWith "Achievements: #{stats} (")
- data and $tweak target, ['steam'], _bl.steamId, [_bl.ignore, $achievements, _markCond],
- [data.hours_forever, (s) -> "#{s}h"], [statStr(data, 'achievements'), UPD['steam-stats']]
- else if $type 'gog', target
- data = $syncId 'gog'
- completed = data?.completed is 'yes'
- data and $tweak target, ['gog'], _bl.gogId, [_bl.ignore, $completion, (e) -> completed is INCOMPLETE.includes e.alt],
- [data.rating, (n) -> "#{n/10}/5"], [statStr(data, 'completed', 'category')]
- else if $type 'humble', target
- data = $syncId 'humble'
- data and $tweak target, ['humble'], _bl.humbleId, [_bl.ignore, (->''), (->'')],
- [not data.icon and "Humble Trove"], [statStr(data, 'developer', 'publisher')]
- else if $type 'ggate', target
- data = $syncId 'ggate'
- data and $tweak target, ['ggate'], _bl.ggateId, [_bl.ignore, (->''), (->'')], [], [statStr(data, 'developer', 'publisher')]
- else if _psn
- data = $syncId _psn
- stats = data?.achievements
- _markCond = (e) -> (not e and stats isnt "0 / 0") or (e and not e.innerText.startsWith "Achievements: #{stats} (")
- data and $tweak target, [_psn, 'psn'], _bl["#{_psn}Id"], [_bl.ignore, $achievements, _markCond],
- [data.rank, (s) -> "#{s} rank"], [statStr(data, 'achievements', 'status', 'trophies', 'progress')]
- GM_setValue 'backlog', backlog
- else if PAGE.match RE.steamLibrary
- $update 'steam', fromPairs ([o.appid, pick(o, 'link', 'logo', 'name', 'hours_forever')] for o in rgGames)
- else if PAGE.match RE.steamRecent
- stats = ([x.appid, "#{x.ach_completed} / #{x.ach_total}"] for x in rgGames when x.ach_completion)
- $markUpdate 'steam-stats'
- $mergeData 'steam-stats', fromPairs stats
- alert "Game library interop: updated #{stats.length} games"
- else if PAGE.match RE.steamAchievements # personal
- when_ $find('#topSummaryAchievements'), (e) -> do (id = $find('.gameLogo a').href.match(/\d+$/)[0]) ->
- $mergeData('steam-stats', [id]: e.innerText.match(/(\d+) of (\d+)/)[1..].join(" / "))
- else if PAGE.match RE.steamAchievements2 # global
- when_ $find('#compareAvatar') and $find('#headerContentLeft'), (e) -> do (id = $find('.gameLogo a').href.match(/\d+$/)[0]) ->
- $mergeData('steam-stats', [id]: e.innerText.match(/\d+ \/ \d+/)[0])
- else if PAGE.match RE.steamDetails
- ID = PAGE.match(RE.steamDetails)[1]
- if $find '.game_area_already_owned'
- platforms = $find_('.platform_img', $find '.game_area_purchase_game')
- worksOn = (s[0] for s in ['win', 'linux', 'mac'] when platforms.some (e) -> $hasClass(e, s)).join('')
- worksOn && $mergeData('steam-platforms', [ID]: worksOn)
- else if PAGE.match RE.steamDbDetails
- if $find '#js-app-install.btn-primary' # it's green when the game is owned
- info = $find('.span8')
- id = $find_('td', info)[1].innerText
- worksOn = (s[0] for s in ['windows', 'linux', 'macos'] when $find(".octicon-#{s}", info)).join('')
- worksOn and $mergeData('steam-platforms', [id]: worksOn)
- else if PAGE.match RE.steamStats
- _achievements = (ss) -> (s.match(/\d+/)?[0] or s for s in ss).join(" / ")
- stats = if PARAMS.DisplayType isnt '2' then do (_table = $find '.tablesorter') -> # list
- _header = $find_ 'th', $find('thead tr', _table)
- _body = ($find_('td', e) for e in $find_ 'tr', $find('tbody', _table))
- [_name$, _total$, _my$] = (_header.findIndex((e) -> e.innerText is s) for s in ["Name", "Total\nAch.", "Gained\nAch."])
- _body.map (l) -> [query($find("a[href^='Steam_Game_Info.php']", l[_name$]).href).AppID,
- _achievements(e.innerText for e in [l[_my$], l[_total$]])]
- else do (_body = $get '/html/body/center/center/center/center') -> # table
- _table = Array.from(_body.children).find((x) -> x.tagName is 'TABLE' and not x.classList.contains 'Pager')
- _ids = (query(e.href).AppID for e in $find_('a', _table))
- [_ids[i], _achievements( last($find_ 'p', e).innerText.match(/Achievements: (.*) of (.*)/)[1..] )] for e, i in $find_('table', _table)
- throw "Invalid update" if stats.length > 0 and not stats[0][0]? # ensuring that next layout change won't break updater
- $markUpdate 'steam-stats'
- $mergeData 'steam-stats', fromPairs stats
- alert "Game library interop: updated #{stats.length} games"
- else if PAGE.match RE.gogLibrary
- queryPage = (page=0) -> $query "/account/getFilteredProducts?mediaType=1&page=#{page+1}"
- worksOn = (o) -> o and worksOn: (k[0].toLowerCase() for k, v of o when v).join('')
- scrape = -> queryPage().then (o) -> do (completed = o.tags.find((x) -> x.name.toLowerCase() is 'completed').id) ->
- Promise.all([Promise.resolve(o), [1...o.totalPages].map(queryPage)...]).then (data) ->
- games = [].concat(data.map((x) => x.products)...)
- .map (o) => [o.id, merge(pick(o, 'image', 'rating', 'url'), worksOn(o.worksOn),
- name: o.title, category: o.category or undefined, completed: o.tags.includes completed)]
- $update 'gog', fromPairs games
- $append $find('.collection-header'),
- $e('i', className: "fas fa-sync-alt _clickable account__filters-option", title: "Sync Backloggery", onclick: scrape)
- else if PAGE.match RE.humbleLibrary
- PLATFORMS = windows: 'w', linux: 'l', osx: 'm', android: 'a'
- url = -> ($find('.details-heading a') or {}).href
- platformSelector = -> $find '.js-platform-select-holder'
- worksOn = -> do (e = platformSelector()) ->
- (PLATFORMS[k] for k of PLATFORMS when e.querySelector '.hb-'+k).join('')
- scrape = -> for e in $find_ '.subproduct-selector'
- e.click()
- name: $find('h2', e).innerText
- publisher: $find('p', e).innerText
- icon: $find('.icon', e).style.backgroundImage.match(/^url\("(.*)"\)$/)?[1]
- url: url()
- worksOn: worksOn()
- GM_addStyle "#syncBackloggery {position: absolute; top: 28px; left: 400px; cursor: pointer}"
- $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
- $visibility loader, off
- main = $find '.base-main-wrapper'
- main.style.position = 'relative'
- $append main, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: ->
- $visibility loader, on
- setTimeout ->
- $update 'humble', fromPairs scrape().map (x) -> x.worksOn and [slugify(x.name), x]
- $visibility loader, off)
- $visibility syncBackloggery, off
- forever -> when_ $find('#switch-platform'), (e) ->
- $visibility(syncBackloggery, (e.value is 'all') and not search.value and location.pathname is "/home/library")
- else if PAGE.match RE.humbleTrove
- name = -> $find('.product-human-name').innerText
- credits = (t) -> $find(".#{t}")?.innerText.trim() # t in {'dev', 'pub'}
- worksOn = -> (e.getAttribute('data-platform')[0] for e in $find('.platforms').children).join('')
- scrape = -> $find_('#trove-main .trove-grid-item').reduce ((p, e) -> p.then (l) ->
- e.click()
- l.push(name: name(), developer: credits('dev'), publisher: credits('pub'), image: $find('img', e).src, worksOn: worksOn())
- $find('.dismiss-action').click()
- return new Promise (resolve) -> setTimeout (-> resolve l), 200
- ), Promise.resolve []
- $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating", style: "color: white"))
- $visibility loader, off
- setTimeout (-> $before $find('.trove-sorter').firstElementChild,
- $e('i', className: "fas fa-sync-alt", style: "cursor: pointer", title: "Sync Backloggery", onclick: ->
- $visibility loader, on
- scrape().then (xs) ->
- $update 'humble-trove', fromPairs([slugify(x.name), x] for x in xs)
- $visibility loader, off)),
- 1000
- else if PAGE.match RE.ggateLibrary
- PLATFORMS = pc: 'w', linux: 'l', mac: 'm', android: 'a'
- worksOn = (e) -> x.src.match(/inline_(pc|linux|mac|android)\.png$/)?[1] for x in $find_ 'img', e
- loadImage = (id) -> new Promise (resolve) ->
- wait = ({target}) -> when_ target.firstChild and $find('.boximg', target), (img) ->
- [dev, pub] = ["Developer", "Publisher"].map (s) -> $get("""//li[span = "#{s}: "]//a""", target)?.innerText
- lib_rightcol_info.removeEventListener 'DOMNodeInserted', wait
- setTimeout -> resolve [img.src, dev, pub]
- lib_rightcol_info.addEventListener 'DOMNodeInserted', wait
- Library.loadinfo 'game', "sku=#{id}&tab=details"
- scrape = ->
- $find_('.mygame_item').map((e) -> $find_('a.ttl', e))
- .reduce ((p, [icon, name]) -> p.then (o) -> do (id = query(icon.href).sku) =>
- loadImage(id).then ([image, developer, publisher]) ->
- Object.assign(o, [id]: {
- image, developer, publisher,
- name: name.title,
- icon: $find('img', icon).src,
- worksOn: worksOn(name).map((s) -> PLATFORMS[s]).join('')
- })
- ), Promise.resolve {}
- GM_addStyle "#syncBackloggery {cursor: pointer; padding: 1ex}"
- $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
- $visibility loader, off
- when_ $find('h1.icon'), (e) ->
- $append e, $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", title: "Sync Backloggery", onclick: ->
- $visibility loader, on
- scrape().then (o) ->
- $update 'ggate', o
- $visibility loader, off)
- forever -> $visibility syncBackloggery, window.location.pathname is '/account/games' and
- $find('[name=platform][value=""]')?.checked and not $find('[name=filter]').value
- else if PSN_ID and PAGE.match(RE.psnLibrary)?[1] is PSN_ID
- PANEL = $get "../../..", $find ".dropdown-toggle.completion"
- GAMES = $find '#gamesTable'
- if ['search', 'completion', 'pf'].every (s) -> s not of PARAMS
- $append document.body, $e('span', {id: 'loader'}, $e('i', className: "fas fa-cog rotating"))
- $visibility loader, off
- _loading = -> $find('#table-loading', GAMES)
- load = -> new Promise (resolve) ->
- unless $find '#load-more', GAMES
- resolve()
- else
- loadMoreGames()
- waiting = forever -> unless $find '#table-loading', GAMES
- clearInterval waiting
- resolve load()
- TROPHIES = ['gold', 'silver', 'bronze']
- _achievements = (s) => (replace(s, /All (\d+)/, "$1 of $1") or s).match(/(\d+) of (\d+)/)[1..].join " / "
- convert = (x) -> [$find('a', x).href.match(RE.psnDetails)[1],
- name: $find('.title', x).innerText, icon: $find("picture source", x).srcset.match("^.*, (.*) 1.1x$")[1],
- rank: $find('.game-rank', x).innerText, progress: $find('.progress-bar', x).innerText,
- achievements: _achievements($find('.small-info', x).innerText),
- platforms: $find_('.platform', x).map((y) -> _PSN_HW[y.innerText]).join(''),
- status: ['completion', 'platinum'].filter((s) -> $find ".#{s}.earned", x).join(", ") or undefined,
- trophies: $find('.trophy-count div', x).innerText.split('\n').map((s, i) -> "#{s} #{TROPHIES[i]}").join(", ")]
- $append PANEL.firstElementChild,
- $e('i', id: 'syncBackloggery', className: "fas fa-sync-alt", style: "cursor: pointer; color: white", title: "Sync Backloggery", onclick: ->
- $visibility loader, on
- load().then ->
- $visibility loader, off
- $update 'psn', fromPairs $find_('tr', GAMES).map convert)
- forever -> $visibility syncBackloggery, GAMES.style.display isnt 'none'
- else if PSN_ID and PAGE.match(RE.psnDetails)?[2] is PSN_ID
- GAME_ID = PAGE.match(RE.psnDetails)[1]
- $mergeData 'psn-img', [GAME_ID]: $find('.game-image-holder a').href
- `;
- eval( CoffeeScript.compile(inline_src) );
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement