diff --git a/lib/$.coffee b/lib/$.coffee index 9c12aba7a..68d6be255 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -200,15 +200,21 @@ $.extend $, # Round to an integer otherwise. Math.round size "#{size} #{['B', 'KB', 'MB', 'GB'][unit]}" - + item: (key, val) -> + item = {} + item[key] = val + item <% if (type === 'crx') { %> + # https://developer.chrome.com/extensions/storage.html delete: (keys) -> chrome.storage.sync.remove keys - get: (key, defaultVal) -> - if val = localStorage.getItem g.NAMESPACE + key - JSON.parse val + get: (key, val, cb) -> + if arguments.length is 2 + items = key + cb = val else - defaultVal + items = $.item key, val + chrome.storage.sync.get items, cb set: (key, val) -> item = {} item[key] = val @@ -231,11 +237,17 @@ do -> localStorage.removeItem key delete scriptStorage[key] return - $.get = (key, defaultVal) -> - if val = scriptStorage[g.NAMESPACE + key] - JSON.parse val + $.get = (key, val, cb) -> + if arguments.length is 2 + items = key + cb = val else - defaultVal + items = $.item key, val + $.queueTask -> + for key of items + if val = scriptStorage[g.NAMESPACE + key] + items[key] = JSON.parse val + cb items $.set = (key, val) -> key = g.NAMESPACE + key val = JSON.stringify val @@ -243,6 +255,7 @@ do -> localStorage.setItem key, val scriptStorage[key] = val <% } else { %> + # http://wiki.greasespot.net/Main_Page delete: (key) -> unless keys instanceof Array keys = [keys] @@ -251,11 +264,17 @@ do -> localStorage.removeItem key GM_deleteValue key return - get: (key, defaultVal) -> - if val = GM_getValue g.NAMESPACE + key - JSON.parse val + get: (key, val, cb) -> + if arguments.length is 2 + items = key + cb = val else - defaultVal + items = $.item key, val + $.queueTask -> + for key of items + if val = GM_getValue g.NAMESPACE + key + items[key] = JSON.parse val + cb items set: (key, val) -> key = g.NAMESPACE + key val = JSON.stringify val diff --git a/src/features.coffee b/src/features.coffee index 6df6b8de4..a6bcb8045 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -210,10 +210,11 @@ Settings = order: 110 open: -> Conf['Enable 4chan\'s Extension'] - if (prevVersion = $.get 'previousversion', null) isnt g.VERSION + $.get 'previousversion', null, (item) -> + return if item['previousversion'] is g.VERSION $.set 'lastupdate', Date.now() $.set 'previousversion', g.VERSION - $.on d, '4chanXInitFinished', Settings.open unless prevVersion + $.on d, '4chanXInitFinished', Settings.open unless item['previousversion'] Settings.addSection 'Main', Settings.main Settings.addSection 'Filter', Settings.filter @@ -309,37 +310,54 @@ Settings = $.on $('.import', section), 'click', Settings.import $.on $('input', section), 'change', Settings.onImport + items = {} + inputs = {} for key, obj of Config.main fs = $.el 'fieldset', innerHTML: "#{key}" for key, arr of obj - checked = if $.get(key, Conf[key]) then 'checked' else '' description = arr[1] div = $.el 'div', - innerHTML: ": #{description}" - $.on $('input', div), 'change', $.cb.checked + innerHTML: ": #{description}" + input = $ 'input', div + $.on input, 'change', $.cb.checked + items[key] = Conf[key] + inputs[key] = input $.add fs, div $.add section, fs - hiddenNum = 0 - for ID, thread of ThreadHiding.getHiddenThreads().threads - hiddenNum++ - for ID, thread of ReplyHiding.getHiddenPosts().threads - for ID, post of thread - hiddenNum++ + $.get items, (items) -> + for key, val of items + inputs[key].checked = val + return + div = $.el 'div', - innerHTML: ": Clear manually hidden threads and posts on /#{g.BOARD}/." - $.on $('button', div), 'click', -> + innerHTML: ": Clear manually hidden threads and posts on /#{g.BOARD}/." + button = $ 'button', div + hiddenNum = 0 + ThreadHiding.getHiddenThreads (hiddenThreads) -> + for ID, thread of hiddenThreads.threads + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + ReplyHiding.getHiddenPosts (hiddenPosts) -> + for ID, thread of hiddenPosts.threads + for ID, post of thread + hiddenNum++ + button.textContent = "Hidden: #{hiddenNum}" + $.on button, 'click', -> @textContent = 'Hidden: 0' $.delete ["hiddenThreads.#{g.BOARD}", "hiddenPosts.#{g.BOARD}"] $.after $('input[name="Stubs"]', section).parentNode.parentNode, div - export: -> - now = Date.now() - data = - version: g.VERSION - date: now - Conf: Conf - WatchedThreads: $.get('WatchedThreads', {}) + export: (now, data) -> + unless typeof now is 'number' + now = Date.now() + data = + version: g.VERSION + date: now + Conf: Conf + $.get 'WatchedThreads', {}, (item) -> + data.WatchedThreads = item.WatchedThreads + Settings.export now, data a = $.el 'a', className: 'warning' textContent: 'Save me!' @@ -478,8 +496,9 @@ Settings = ta = $.el 'textarea', name: name className: 'field' - value: $.get name, Conf[name] spellcheck: false + $.get name, Conf[name], (item) -> + ta.value = item[name] $.on ta, 'change', $.cb.value $.add div, ta return @@ -529,7 +548,8 @@ Settings = """ sauce = $ 'textarea', section - sauce.value = $.get 'sauces', Conf['sauces'] + $.get 'sauces', Conf['sauces'], (item) -> + sauce.value = item['sauces'] $.on sauce, 'change', $.cb.value rice: (section) -> @@ -592,17 +612,25 @@ Settings = """ + items = {} + inputs = {} for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss'] input = $ "[name=#{name}]", section - input.value = $.get name, Conf[name] + items[name] = Conf[name] + inputs[name] = input event = if name in ['favicon', 'usercss'] 'change' else 'input' $.on input, event, $.cb.value - unless name in ['usercss'] - $.on input, event, Settings[name] - Settings[name].call input + $.get items, (items) -> + for key, val of items + input = inputs[key] + input.value = val + unless key in ['usercss'] + $.on input, event, Settings[key] + Settings[key].call input + return $.on $('input[name="Custom CSS"]', section), 'change', Settings.togglecss $.on $.id('apply-css'), 'click', Settings.usercss boardnav: -> @@ -652,17 +680,23 @@ Settings = ActionsKeybinds """ - tbody = $ 'tbody', section + tbody = $ 'tbody', section + items = {} + inputs = {} for key, arr of Config.hotkeys tr = $.el 'tr', innerHTML: "#{arr[1]}" input = $ 'input', tr - input.name = key - input.value = $.get key, Conf[key] + input.name = key input.spellcheck = false + items[key] = Conf[key] + inputs[key] = input $.on input, 'keydown', Settings.keybind $.add tbody, tr - return + $.get items, (items) -> + for key, val of items + inputs[key].value = val + return keybind: (e) -> return if e.keyCode is 9 # tab e.preventDefault() @@ -990,13 +1024,14 @@ Filter = re += ';op:yes' # Add a new line before the regexp unless the text is empty. - save = $.get type, '' - save = - if save - "#{save}\n#{re}" - else - re - $.set type, save + $.get type, '', (item) -> + save = item[type] + save = + if save + "#{save}\n#{re}" + else + re + $.set type, save # Open the settings and display & focus the relevant filter textarea. Settings.open 'Filter' @@ -1026,8 +1061,10 @@ ThreadHiding = return unless Conf['Thread Hiding'] $.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' - getHiddenThreads: -> - ThreadHiding.hiddenThreads = $.get "hiddenThreads.#{g.BOARD}", threads: {} + getHiddenThreads: (cb) -> + $.get "hiddenThreads.#{g.BOARD}", threads: {}, (item) -> + ThreadHiding.hiddenThreads = item["hiddenThreads.#{g.BOARD}"] + cb ThreadHiding.hiddenThreads if cb syncFromCatalog: -> # Sync hidden threads from the catalog into the index. @@ -1093,16 +1130,16 @@ ThreadHiding = saveHiddenState: (thread, makeStub) -> # Get fresh hidden threads. - hiddenThreads = ThreadHiding.getHiddenThreads() - hiddenThreadsCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} - if thread.isHidden - hiddenThreads.threads[thread] = {makeStub} - hiddenThreadsCatalog[thread] = true - else - delete hiddenThreads.threads[thread] - delete hiddenThreadsCatalog[thread] - $.set "hiddenThreads.#{g.BOARD}", hiddenThreads - localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsCatalog + ThreadHiding.getHiddenThreads (hiddenThreads) -> + hiddenThreadsCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} + if thread.isHidden + hiddenThreads.threads[thread] = {makeStub} + hiddenThreadsCatalog[thread] = true + else + delete hiddenThreads.threads[thread] + delete hiddenThreadsCatalog[thread] + $.set "hiddenThreads.#{g.BOARD}", hiddenThreads + localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsCatalog toggle: (thread) -> unless thread instanceof Thread @@ -1173,8 +1210,10 @@ ReplyHiding = return unless Conf['Reply Hiding'] $.replace $('.sideArrows', @nodes.root), ReplyHiding.makeButton @, 'hide' - getHiddenPosts: -> - ReplyHiding.hiddenPosts = $.get "hiddenPosts.#{g.BOARD}", threads: {} + getHiddenPosts: (cb) -> + $.get "hiddenPosts.#{g.BOARD}", threads: {}, (item) -> + ReplyHiding.hiddenPosts = item["hiddenPosts.#{g.BOARD}"] + cb ReplyHiding.hiddenPosts if cb menu: init: -> @@ -1230,7 +1269,7 @@ ReplyHiding = open: (post) -> if !post.isReply or post.isClone return false - thread = ReplyHiding.getHiddenPosts().threads[post.thread] + thread = ReplyHiding.hiddenPosts.threads[post.thread] unless post.isHidden or data = thread?[post] return false ReplyHiding.menu.post = post @@ -1258,7 +1297,7 @@ ReplyHiding = thisPost = $('input[name=thisPost]', parent).checked replies = $('input[name=replies]', parent).checked {post} = ReplyHiding.menu - thread = ReplyHiding.getHiddenPosts().threads[post.thread] + thread = ReplyHiding.hiddenPosts.threads[post.thread] data = thread?[post] if thisPost ReplyHiding.show post, replies @@ -1281,20 +1320,20 @@ ReplyHiding = saveHiddenState: (post, isHiding, thisPost, makeStub, hideRecursively) -> # Get fresh hidden posts. - hiddenPosts = ReplyHiding.getHiddenPosts() - if isHiding - unless thread = hiddenPosts.threads[post.thread] - thread = hiddenPosts.threads[post.thread] = {} - thread[post] = - thisPost: thisPost isnt false # undefined -> true - makeStub: makeStub - hideRecursively: hideRecursively - else - thread = hiddenPosts.threads[post.thread] - delete thread[post] - unless Object.keys(thread).length - delete hiddenPosts.threads[post.thread] - $.set "hiddenPosts.#{g.BOARD}", hiddenPosts + ReplyHiding.getHiddenPosts (hiddenPosts) -> + if isHiding + unless thread = hiddenPosts.threads[post.thread] + thread = hiddenPosts.threads[post.thread] = {} + thread[post] = + thisPost: thisPost isnt false # undefined -> true + makeStub: makeStub + hideRecursively: hideRecursively + else + thread = hiddenPosts.threads[post.thread] + delete thread[post] + unless Object.keys(thread).length + delete hiddenPosts.threads[post.thread] + $.set "hiddenPosts.#{g.BOARD}", hiddenPosts toggle: -> post = Get.postFromNode @ @@ -2461,8 +2500,13 @@ Get = Get.insert post, root, context Misc = # super semantic - clearThreads: (key) -> - return unless data = $.get key + clearThreads: (key, data) -> + unless data + $.get key, null, (item) -> + data = item[key] + return unless data + Misc.clearThreads key, data + return unless Object.keys(data.threads).length $.delete key @@ -3571,20 +3615,21 @@ Unread = node: -> Unread.thread = @ - Unread.lastReadPost = $.get("lastReadPosts.#{@board}", threads: {}).threads[@] or 0 Unread.posts = [] Unread.postsQuotingYou = [] Unread.title = d.title posts = [] for ID, post of @posts posts.push post if post.isReply - Unread.addPosts posts - if Unread.posts.length - # Scroll to before the first unread post. - $.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false - else if posts.length - # Scroll to the last read post. - posts[posts.length - 1].nodes.root.scrollIntoView() + $.get "lastReadPosts.#{@board}", threads: {}, (item) => + Unread.lastReadPost = item["lastReadPosts.#{@board}"].threads[@] or 0 + Unread.addPosts posts + if Unread.posts.length + # Scroll to before the first unread post. + $.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false + else if posts.length + # Scroll to the last read post. + posts[posts.length - 1].nodes.root.scrollIntoView() $.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'scroll visibilitychange', Unread.read $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] @@ -3636,11 +3681,11 @@ Unread = Unread.postsQuotingYou = Unread.postsQuotingYou[i..] Unread.update() if e - saveLastReadPost: $.debounce($.SECOND, -> - lastReadPosts = $.get "lastReadPosts.#{Unread.thread.board}", threads: {} - lastReadPosts.threads[Unread.thread] = Unread.lastReadPost - $.set "lastReadPosts.#{Unread.thread.board}", lastReadPosts - ) + saveLastReadPost: $.debounce $.SECOND, -> + $.get "lastReadPosts.#{Unread.thread.board}", threads: {}, (item) -> + lastReadPosts = item["lastReadPosts.#{Unread.thread.board}"] + lastReadPosts.threads[Unread.thread] = Unread.lastReadPost + $.set "lastReadPosts.#{Unread.thread.board}", lastReadPosts setLine: (force) -> return unless d.hidden or force is true @@ -4068,7 +4113,9 @@ ThreadWatcher = className: 'favicon' $.on favicon, 'click', ThreadWatcher.cb.toggle $.before $('input', @OP.nodes.post), favicon - if g.VIEW is 'thread' and @ID is $.get 'AutoWatch', 0 + return if g.VIEW isnt 'thread' + $.get 'AutoWatch', 0, (item) => + return if item['AutoWatch'] isnt @ID ThreadWatcher.watch @ $.delete 'AutoWatch' @@ -4078,7 +4125,10 @@ ThreadWatcher = $.add d.body, ThreadWatcher.dialog refresh: (watched) -> - watched or= $.get 'WatchedThreads', {} + unless watched + $.get 'WatchedThreads', {}, (item) -> + ThreadWatcher.refresh item['WatchedThreads'] + return nodes = [$('.move', ThreadWatcher.dialog)] for board of watched for id, props of watched[board] @@ -4126,17 +4176,19 @@ ThreadWatcher = ThreadWatcher.unwatch thread.board, thread.ID unwatch: (board, threadID) -> - watched = $.get 'WatchedThreads', {} - delete watched[board][threadID] - delete watched[board] unless Object.keys(watched[board]).length - ThreadWatcher.refresh watched - $.set 'WatchedThreads', watched + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + delete watched[board][threadID] + delete watched[board] unless Object.keys(watched[board]).length + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched watch: (thread) -> - watched = $.get 'WatchedThreads', {} - watched[thread.board] or= {} - watched[thread.board][thread] = - href: "/#{thread.board}/res/#{thread}" - textContent: Get.threadExcerpt thread - ThreadWatcher.refresh watched - $.set 'WatchedThreads', watched + $.get 'WatchedThreads', {}, (item) -> + watched = item['WatchedThreads'] + watched[thread.board] or= {} + watched[thread.board][thread] = + href: "/#{thread.board}/res/#{thread}" + textContent: Get.threadExcerpt thread + ThreadWatcher.refresh watched + $.set 'WatchedThreads', watched diff --git a/src/main.coffee b/src/main.coffee index 25639e6a6..66ff56550 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -281,21 +281,24 @@ class Clone extends Post Main = - init: -> - # flatten Config into Conf - # and get saved or default values - flatten = (parent, obj) -> - if obj instanceof Array - Conf[parent] = obj[0] - else if typeof obj is 'object' - for key, val of obj - flatten key, val - else # string or number - Conf[parent] = obj + init: (items) -> + unless items + # flatten Config into Conf + # and get saved or default values + flatten = (parent, obj) -> + if obj instanceof Array + Conf[parent] = obj[0] + else if typeof obj is 'object' + for key, val of obj + flatten key, val + else # string or number + Conf[parent] = obj + return + flatten null, Config + $.get Conf, Main.init return - flatten null, Config - for key, val of Conf - Conf[key] = $.get key, val + + Conf = items pathname = location.pathname.split '/' g.BOARD = new Board pathname[1] @@ -499,20 +502,24 @@ Main = # After that, check for updates every day if we still haven't updated. now = Date.now() freq = <% if (type === 'userjs') { %>6 * $.HOUR<% } else { %>7 * $.DAY<% } %> - if $.get('lastupdate', 0) > now - freq or $.get('lastchecked', 0) > now - $.DAY - return - $.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> - return unless @status is 200 - version = @response - return unless /^\d\.\d+\.\d+$/.test version - if g.VERSION is version - # Don't check for updates too frequently if there wasn't one in a 'long' time. - $.set 'lastupdate', now + items = + lastupdate: 0 + lastchecked: 0 + $.get items, (items) -> + if items.lastupdate > now - freq or items.lastchecked > now - $.DAY return - $.set 'lastchecked', now - el = $.el 'span', - innerHTML: "Update: <%= meta.name %> v#{version} is out, get it target=_blank>here." - new Notification 'info', el, 2 * $.MINUTE + $.ajax '<%= meta.page %><%= meta.buildsPath %>version', onload: -> + return unless @status is 200 + version = @response + return unless /^\d\.\d+\.\d+$/.test version + if g.VERSION is version + # Don't check for updates too frequently if there wasn't one in a 'long' time. + $.set 'lastupdate', now + return + $.set 'lastchecked', now + el = $.el 'span', + innerHTML: "Update: <%= meta.name %> v#{version} is out, get it target=_blank>here." + new Notification 'info', el, 2 * $.MINUTE handleErrors: (errors) -> unless 'length' of errors diff --git a/src/qr.coffee b/src/qr.coffee index b52dffe9a..9341f3314 100644 --- a/src/qr.coffee +++ b/src/qr.coffee @@ -92,7 +92,8 @@ QR = if yourPosts QR.yourPosts = yourPosts return - QR.yourPosts = $.get "yourPosts.#{g.BOARD}", threads: {} + $.get "yourPosts.#{g.BOARD}", threads: {}, (item) -> + QR.syncYourPosts item["yourPosts.#{g.BOARD}"] $.sync "yourPosts.#{g.BOARD}", QR.syncYourPosts error: (err) -> @@ -145,10 +146,11 @@ QR = sage: if board is 'q' then 600 else 60 file: if board is 'q' then 300 else 30 post: if board is 'q' then 60 else 30 - QR.cooldown.cooldowns = $.get "cooldown.#{board}", {} QR.cooldown.upSpd = 0 QR.cooldown.upSpdAccuracy = .5 - QR.cooldown.start() + $.get "cooldown.#{board}", {}, (item) -> + QR.cooldown.cooldowns = item["cooldown.#{board}"] + QR.cooldown.start() $.sync "cooldown.#{board}", QR.cooldown.sync start: -> return if QR.cooldown.isCounting @@ -358,15 +360,6 @@ QR = posts: [] post: class constructor: -> - # set values, or null, to avoid 'undefined' values in inputs - prev = QR.posts[QR.posts.length - 1] - persona = $.get 'QR.persona', {} - @name = if prev then prev.name else persona.name or null - @email = if prev and !/^sage$/.test prev.email then prev.email else persona.email or null - @sub = if prev and Conf['Remember Subject'] then prev.sub else if Conf['Remember Subject'] then persona.sub else null - @spoiler = if prev and Conf['Remember Spoiler'] then prev.spoiler else false - @com = null - el = $.el 'a', className: 'qr-preview' draggable: true @@ -393,8 +386,29 @@ QR = for event in ['dragStart', 'dragEnter', 'dragLeave', 'dragOver', 'dragEnd', 'drop'] $.on el, event.toLowerCase(), @[event] - @unlock() QR.posts.push @ + # set values, or null, to avoid 'undefined' values in inputs + @com = null + @spoiler = if prev and Conf['Remember Spoiler'] + prev.spoiler + else + false + prev = QR.posts[QR.posts.length - 1] + $.get 'QR.persona', {}, (item) => + persona = item['QR.persona'] + @name = if prev + prev.name + else + persona.name or null + @email = if prev and !/^sage$/.test prev.email + prev.email + else + persona.email or null + @sub = if Conf['Remember Subject'] + if prev then prev.sub else persona.sub + else + null + @unlock() rm: -> $.rm @nodes.el index = QR.posts.indexOf @ @@ -606,8 +620,9 @@ QR = $.on imgContainer, 'click', @reload.bind @ $.on input, 'keydown', @keydown.bind @ + $.get 'captchas', [], (item) => + @sync item['captchas'] $.sync 'captchas', @sync - @sync $.get 'captchas', [] # start with an uncached captcha @reload() @@ -960,12 +975,13 @@ QR = QR.cleanNotifications() QR.notifications.push new Notification 'success', h1.textContent, 5 - persona = $.get 'QR.persona', {} - persona = - name: post.name - email: if /^sage$/.test post.email then persona.email else post.email - sub: if Conf['Remember Subject'] then post.sub else null - $.set 'QR.persona', persona + $.get 'QR.persona', {}, (item) -> + persona = item['QR.persona'] + persona = + name: post.name + email: if /^sage$/.test post.email then persona.email else post.email + sub: if Conf['Remember Subject'] then post.sub else null + $.set 'QR.persona', persona [_, threadID, postID] = h1.nextSibling.textContent.match /thread:(\d+),no:(\d+)/ postID = +postID