Merge Mayhem X

This commit is contained in:
seaweedchan 2013-09-22 03:57:03 -07:00
commit 6d4da42eb2
16 changed files with 309 additions and 1157 deletions

View File

@ -1,3 +1,6 @@
**MayhemYDG**:
- /pol/ flag selector
**seaweedchan**: **seaweedchan**:
- Delete cooldown update - Delete cooldown update

View File

@ -1,21 +1,14 @@
module.exports = (grunt) -> module.exports = (grunt) ->
concatOptions =
process: Object.create(null, data:
get: -> grunt.config 'pkg'
enumerable: true
)
shellOptions =
stdout: true
stderr: true
failOnError: true
# Project configuration. # Project configuration.
grunt.initConfig grunt.initConfig
pkg: grunt.file.readJSON 'package.json' pkg: grunt.file.readJSON 'package.json'
concat: concat:
options: process: Object.create(null, data:
get: -> grunt.config 'pkg'
enumerable: true
)
coffee: coffee:
options: concatOptions
src: [ src: [
'src/General/Config.coffee' 'src/General/Config.coffee'
'src/General/Globals.coffee' 'src/General/Globals.coffee'
@ -41,13 +34,11 @@ module.exports = (grunt) ->
dest: 'tmp-<%= pkg.type %>/script.coffee' dest: 'tmp-<%= pkg.type %>/script.coffee'
meta: meta:
options: concatOptions
files: files:
'LICENSE': 'src/General/meta/banner.js', 'LICENSE': 'src/General/meta/banner.js',
'latest.js': 'src/General/meta/latest.js' 'latest.js': 'src/General/meta/latest.js'
crx: crx:
options: concatOptions
files: files:
'builds/crx/manifest.json': 'src/General/meta/manifest.json' 'builds/crx/manifest.json': 'src/General/meta/manifest.json'
'builds/crx/script.js': [ 'builds/crx/script.js': [
@ -57,7 +48,6 @@ module.exports = (grunt) ->
'tmp-<%= pkg.type %>/script.js' 'tmp-<%= pkg.type %>/script.js'
] ]
userscript: userscript:
options: concatOptions
files: files:
'builds/<%= pkg.name %>.meta.js': 'src/General/meta/metadata.js' 'builds/<%= pkg.name %>.meta.js': 'src/General/meta/metadata.js'
'builds/<%= pkg.name %>.user.js': [ 'builds/<%= pkg.name %>.user.js': [
@ -96,22 +86,23 @@ module.exports = (grunt) ->
push: false push: false
shell: shell:
options:
stdout: true
stderr: true
failOnError: true
commit: commit:
options: shellOptions command: """
command: [ git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."' git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."
'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."' git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."
'git tag -af stable -m "<%= pkg.meta.name %> v<%= pkg.version %>."' """
].join ' && '
push: push:
options: shellOptions
command: 'git push origin --tags -f && git push origin --all' command: 'git push origin --tags -f && git push origin --all'
watch: watch:
options:
interrupt: true
all: all:
options:
interrupt: true
files: [ files: [
'Gruntfile.coffee' 'Gruntfile.coffee'
'package.json' 'package.json'

View File

@ -1,5 +1,5 @@
/* /*
* 4chan X - Version 1.2.39 - 2013-09-21 * 4chan X - Version 1.2.39 - 2013-09-22
* *
* Licensed under the MIT license. * Licensed under the MIT license.
* https://github.com/seaweedchan/4chan-x/blob/master/LICENSE * https://github.com/seaweedchan/4chan-x/blob/master/LICENSE

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,14 +23,14 @@
"font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb", "font-awesome": "git://github.com/MayhemYDG/Font-Awesome.git#df4285951124f9ca1f3907438462e5ba9e464bcb",
"grunt": "~0.4.1", "grunt": "~0.4.1",
"grunt-bump": "~0.0.11", "grunt-bump": "~0.0.11",
"grunt-concurrent": "~0.3.0", "grunt-concurrent": "~0.3.1",
"grunt-contrib-clean": "~0.5.0", "grunt-contrib-clean": "~0.5.0",
"grunt-contrib-coffee": "~0.7.0", "grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-compress": "~0.5.2", "grunt-contrib-compress": "~0.5.2",
"grunt-contrib-concat": "~0.3.0", "grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.4.1", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-watch": "~0.5.0", "grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.3.1", "grunt-shell": "~0.4.0",
"load-grunt-tasks": "~0.1.0" "load-grunt-tasks": "~0.1.0"
}, },
"repository": { "repository": {

View File

@ -25,6 +25,7 @@ Redirect =
'4plebs': '4plebs':
domain: 'archive.4plebs.org' domain: 'archive.4plebs.org'
http: true http: true
https: true
software: 'foolfuuka' software: 'foolfuuka'
boards: ['hr', 'tg', 'tv', 'x'] boards: ['hr', 'tg', 'tv', 'x']
files: ['hr', 'tg', 'tv', 'x'] files: ['hr', 'tg', 'tv', 'x']
@ -34,8 +35,8 @@ Redirect =
http: true http: true
https: false https: false
software: 'foolfuuka' software: 'foolfuuka'
boards: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] boards: ['b', 'e', 'h', 'hc', 'p', 's', 'soc', 'sp', 'u']
files: ['b', 'e', 'h', 'hc', 'p', 's', 'u'] files: ['b', 'e', 'h', 'hc', 'p', 's', 'soc', 'sp', 'u']
'Foolz': 'Foolz':
domain: 'archive.foolz.us' domain: 'archive.foolz.us'
@ -146,12 +147,7 @@ Redirect =
post: (archive, {boardID, postID}) -> post: (archive, {boardID, postID}) ->
# For fuuka-based archives: # For fuuka-based archives:
# https://github.com/eksopl/fuuka/issues/27 # https://github.com/eksopl/fuuka/issues/27
protocol = Redirect.protocol archive URL = new String "#{Redirect.protocol archive}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
# XXX foolz had HSTS set for 120 days, which broke XHR+CORS+Redirection when on HTTP.
# Remove necessary HTTPS procotol in September 2013.
if ['Foolz', 'NSFW Foolz'].contains archive.name
protocol = 'https://'
URL = new String "#{protocol}#{archive.domain}/_/api/chan/post/?board=#{boardID}&num=#{postID}"
URL.archive = archive URL.archive = archive
URL URL

View File

@ -118,17 +118,18 @@ Get =
Build.spoilerRange[boardID] = posts[0].custom_spoiler Build.spoilerRange[boardID] = posts[0].custom_spoiler
for post in posts for post in posts
break if post.no is postID # we found it! break if post.no is postID # we found it!
if post.no > postID
# The post can be deleted by the time we check a quote. if post.no isnt postID
if url = Redirect.to 'post', {boardID, postID} # The post can be deleted by the time we check a quote.
$.cache url, if url = Redirect.to 'post', {boardID, postID}
-> Get.archivedPost @, boardID, postID, root, context $.cache url,
, -> Get.archivedPost @, boardID, postID, root, context
withCredentials: url.archive.withCredentials ,
else withCredentials: url.archive.withCredentials
$.addClass root, 'warning' else
root.textContent = "Post No.#{postID} was not found." $.addClass root, 'warning'
return root.textContent = "Post No.#{postID} was not found."
return
board = g.boards[boardID] or board = g.boards[boardID] or
new Board boardID new Board boardID

View File

@ -19,18 +19,6 @@ Main =
Conf['CachedTitles'] = [] Conf['CachedTitles'] = []
$.get Conf, (items) -> $.get Conf, (items) ->
$.extend Conf, items $.extend Conf, items
<% if (type === 'crx') { %>
unless items
new Notice 'error', $.el 'span',
innerHTML: """
It seems like your <%= meta.name %> settings became corrupted due to a <a href="https://code.google.com/p/chromium/issues/detail?id=261623" target=_blank>Chrome bug</a>.<br>
Unfortunately, you'll have to <a href="https://github.com/MayhemYDG/4chan-x/wiki/FAQ#known-problems" target=_blank>fix it yourself</a>.
"""
# Track resolution of this bug.
Main.logError
message: 'Chrome Storage API bug'
error: new Error '~'
<% } %>
Main.initFeatures() Main.initFeatures()
$.on d, '4chanMainInit', Main.initStyle $.on d, '4chanMainInit', Main.initStyle

View File

@ -122,8 +122,7 @@ UI = do ->
findNextEntry: (entry, direction) -> findNextEntry: (entry, direction) ->
entries = [entry.parentNode.children...] entries = [entry.parentNode.children...]
entries.sort (first, second) -> entries.sort (first, second) -> first.style.order - second.style.order
+(first.style.order or first.style.webkitOrder) - +(second.style.order or second.style.webkitOrder)
entries[entries.indexOf(entry) + direction] entries[entries.indexOf(entry) + direction]
keybinds: (e) => keybinds: (e) =>
@ -197,8 +196,7 @@ UI = do ->
e.stopPropagation() e.stopPropagation()
@focus el @focus el
).bind @ ).bind @
{style} = el el.style.order = entry.order or 100
style.webkitOrder = style.order = entry.order or 100
return unless subEntries return unless subEntries
$.addClass el, 'has-submenu' $.addClass el, 'has-submenu'
for subEntry in subEntries for subEntry in subEntries

View File

@ -675,7 +675,7 @@ a.hide-announcement {
:root.hide-original-post-form .postingMode, :root.hide-original-post-form .postingMode,
:root.hide-original-post-form #togglePostForm, :root.hide-original-post-form #togglePostForm,
#qr.autohide:not(.has-focus):not(:hover) > form, #qr.autohide:not(.has-focus):not(:hover) > form,
.postingMode ~ #qr select, .postingMode ~ #qr select[data-name=thread],
#file-n-submit:not(.has-file) #qr-filerm { #file-n-submit:not(.has-file) #qr-filerm {
display: none; display: none;
} }
@ -837,7 +837,7 @@ input#qr-filename:not(.edit) {
position: absolute; position: absolute;
} }
/* Thread Select / Spoiler Label */ /* Thread Select / Spoiler Label */
#qr select { #qr select[data-name=thread] {
float: right; float: right;
} }
#qr.has-spoiler .has-file #qr-spoiler-label { #qr.has-spoiler .has-file #qr-spoiler-label {

View File

@ -58,7 +58,7 @@ ImageExpand =
unless post.file.isExpanded or $.hasClass thumb, 'expanding' unless post.file.isExpanded or $.hasClass thumb, 'expanding'
ImageExpand.expand post ImageExpand.expand post
return return
ImageExpand.contract post
# Scroll back to the thumbnail when contracting the image # Scroll back to the thumbnail when contracting the image
# to avoid being left miles away from the relevant post. # to avoid being left miles away from the relevant post.
{root} = post.nodes {root} = post.nodes
@ -81,6 +81,7 @@ ImageExpand =
if rect.left < 0 if rect.left < 0
x = -window.scrollX x = -window.scrollX
window.scrollBy x, y if x or y window.scrollBy x, y if x or y
ImageExpand.contract post
contract: (post) -> contract: (post) ->
$.rmClass post.nodes.root, 'expanded-image' $.rmClass post.nodes.root, 'expanded-image'

View File

@ -31,14 +31,6 @@ ThreadWatcher =
ThreadWatcher.fetchAllStatus() ThreadWatcher.fetchAllStatus()
@db.save() @db.save()
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
for boardID, threads of ThreadWatcher.convert WatchedThreads
for threadID, data of threads
ThreadWatcher.db.set {boardID, threadID, val: data}
$.delete 'WatchedThreads'
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Watcher' name: 'Thread Watcher'
cb: @node cb: @node

View File

@ -104,9 +104,10 @@ Unread =
notif.onclick = -> notif.onclick = ->
Header.scrollToPost post.nodes.root Header.scrollToPost post.nodes.root
window.focus() window.focus()
setTimeout -> notif.onshow = ->
notif.close() setTimeout ->
, 7 * $.SECOND notif.close()
, 7 * $.SECOND
onUpdate: (e) -> onUpdate: (e) ->
if e.detail[404] if e.detail[404]

View File

@ -163,10 +163,11 @@ QR =
# Firefox automatically closes notifications # Firefox automatically closes notifications
# so we can't control the onclose properly. # so we can't control the onclose properly.
notif.onclose = -> notice.close() notif.onclose = -> notice.close()
setTimeout -> notif.onshow = ->
notif.onclose = null setTimeout ->
notif.close() notif.onclose = null
, 7 * $.SECOND notif.close()
, 7 * $.SECOND
<% } %> <% } %>
notifications: [] notifications: []
@ -267,13 +268,14 @@ QR =
name: post.name name: post.name
email: if /^sage$/.test post.email then persona.email else post.email email: if /^sage$/.test post.email then persona.email else post.email
sub: if Conf['Remember Subject'] then post.sub else undefined sub: if Conf['Remember Subject'] then post.sub else undefined
flag: post.flag
$.set 'QR.persona', persona $.set 'QR.persona', persona
cooldown: cooldown:
init: -> init: ->
return unless Conf['Cooldown'] return unless Conf['Cooldown']
setTimers = (e) => QR.cooldown.types = e.detail setTimers = (e) => QR.cooldown.types = e.detail
$.on window, 'cooldown:timers', setTimers $.on window, 'cooldown:timers', setTimers
$.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))' $.globalEval 'window.dispatchEvent(new CustomEvent("cooldown:timers", {detail: cooldowns}))'
QR.cooldown.types or= {} # XXX tmp workaround until all pages and the catalogs get the cooldowns var. QR.cooldown.types or= {} # XXX tmp workaround until all pages and the catalogs get the cooldowns var.
$.off window, 'cooldown:timers', setTimers $.off window, 'cooldown:timers', setTimers
@ -306,11 +308,10 @@ QR =
if delay if delay
cooldown = {delay} cooldown = {delay}
else else
if post.file if hasFile = !!post.file
upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND) upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2 QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
QR.cooldown.upSpd = upSpd QR.cooldown.upSpd = upSpd
hasFile = !!post.file
cooldown = {isReply, hasFile, threadID} cooldown = {isReply, hasFile, threadID}
QR.cooldown.cooldowns[start] = cooldown QR.cooldown.cooldowns[start] = cooldown
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns $.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
@ -357,24 +358,28 @@ QR =
if isReply is cooldown.isReply if isReply is cooldown.isReply
# Only cooldowns relevant to this post can set the seconds variable: # Only cooldowns relevant to this post can set the seconds variable:
# reply cooldown with a reply, thread cooldown with a thread # reply cooldown with a reply, thread cooldown with a thread
elapsed = Math.floor (now - start) / $.SECOND elapsed = Math.floor (now - start) / $.SECOND
continue if elapsed < 0 # clock changed since then? continue if elapsed < 0 # clock changed since then?
type = unless isReply unless isReply
'thread' type = 'thread'
else if hasFile else if hasFile
'image' # You can post an image reply immediately after a non-image reply.
unless cooldown.hasFile
seconds = Math.max seconds, 0
continue
type = 'image'
else else
'reply' type = 'reply'
maxTimer = Math.max types[type] or 0, types[type + '_intra'] or 0 maxTimer = Math.max types[type] or 0, types[type + '_intra'] or 0
unless start <= now <= start + maxTimer * $.SECOND unless start <= now <= start + maxTimer * $.SECOND
QR.cooldown.unset start QR.cooldown.unset start
type += '_intra' if isReply and +post.thread is cooldown.threadID type += '_intra' if isReply and +post.thread is cooldown.threadID
seconds = Math.max seconds, types[type] - elapsed seconds = Math.max seconds, types[type] - elapsed
if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
seconds = Math.max seconds, 0 seconds = Math.max seconds, 0
# Update the status when we change posting type. # Update the status when we change posting type.
# Don't get stuck at some random number. # Don't get stuck at some random number.
# Don't interfere with progress status updates. # Don't interfere with progress status updates.
@ -564,6 +569,12 @@ QR =
if prev then prev.sub else persona.sub if prev then prev.sub else persona.sub
else else
'' ''
if QR.nodes.flag
@flag = if prev
prev.flag
else
persona.flag
@load() if QR.selected is @ # load persona @load() if QR.selected is @ # load persona
@select() if select @select() if select
@unlock() @unlock()
@ -585,8 +596,9 @@ QR =
lock: (lock=true) -> lock: (lock=true) ->
@isLocked = lock @isLocked = lock
return unless @ is QR.selected return unless @ is QR.selected
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'fileButton', 'filename', 'spoiler', 'flag']
QR.nodes[name].disabled = lock continue unless node = QR.nodes[name]
node.disabled = lock
@nodes.rm.style.visibility = if lock then 'hidden' else '' @nodes.rm.style.visibility = if lock then 'hidden' else ''
(if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput (if lock then $.off else $.on) QR.nodes.filename.previousElementSibling, 'click', QR.openFileInput
@nodes.spoiler.disabled = lock @nodes.spoiler.disabled = lock
@ -611,8 +623,9 @@ QR =
load: -> load: ->
# Load this post's values. # Load this post's values.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
QR.nodes[name].value = @[name] or null continue unless node = QR.nodes[name]
node.value = @[name] or node.dataset.default or null
@showFileData() @showFileData()
QR.characterCount() QR.characterCount()
@ -621,7 +634,7 @@ QR =
@spoiler = input.checked @spoiler = input.checked
return return
{name} = input.dataset {name} = input.dataset
@[name] = input.value @[name] = input.value or input.dataset.default or null
switch name switch name
when 'thread' when 'thread'
QR.status() QR.status()
@ -646,8 +659,9 @@ QR =
return unless @ is QR.selected return unless @ is QR.selected
# Do this in case people use extensions # Do this in case people use extensions
# that do not trigger the `input` event. # that do not trigger the `input` event.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler'] for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler', 'flag']
@save QR.nodes[name] continue unless node = QR.nodes[name]
@save node
return return
setFile: (@file) -> setFile: (@file) ->
@ -947,7 +961,13 @@ QR =
<option value=5>Loop</option> <option value=5>Loop</option>
<option value=4 selected>Other</option> <option value=4 selected>Other</option>
""" """
nodes.flashTag.dataset.default = '4'
$.add nodes.form, nodes.flashTag $.add nodes.form, nodes.flashTag
if flagSelector = $ '.flagSelector'
nodes.flag = flagSelector.cloneNode true
nodes.flag.dataset.name = 'flag'
nodes.flag.dataset.default = '0'
$.add nodes.form, nodes.flag
# Make a list of threads. # Make a list of threads.
for thread of g.BOARD.threads for thread of g.BOARD.threads
@ -978,11 +998,11 @@ QR =
$.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click() $.on nodes.spoiler, 'change', -> QR.selected.nodes.spoiler.click()
$.on nodes.fileInput, 'change', QR.handleFiles $.on nodes.fileInput, 'change', QR.handleFiles
# save selected post's data # save selected post's data
items = ['name', 'email', 'sub', 'com', 'filename'] save = -> QR.selected.save @
i = 0 for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'flag']
while name = items[i++] continue unless node = nodes[name]
$.on nodes[name], 'input', -> QR.selected.save @ event = if node.nodeName is 'SELECT' then 'change' else 'input'
$.on nodes.thread, 'change', -> QR.selected.save @ $.on nodes[name], event, save
<% if (type === 'userscript') { %> <% if (type === 'userscript') { %>
if Conf['Remember QR Size'] if Conf['Remember QR Size']
@ -1064,7 +1084,7 @@ QR =
post.lock() post.lock()
postData = formData =
resto: threadID resto: threadID
name: post.name name: post.name
email: post.email email: post.email
@ -1073,6 +1093,7 @@ QR =
upfile: post.file upfile: post.file
filetag: filetag filetag: filetag
spoiler: post.spoiler spoiler: post.spoiler
flag: post.flag
textonly: textOnly textonly: textOnly
mode: 'regist' mode: 'regist'
pwd: QR.persona.pwd pwd: QR.persona.pwd
@ -1096,7 +1117,7 @@ QR =
[<a href="//4chan.org/banned" target=_blank>Banned?</a>] [<a href="https://github.com/seaweedchan/4chan-x/wiki/Frequently-Asked-Questions#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target=_blank>More info</a>] [<a href="//4chan.org/banned" target=_blank>Banned?</a>] [<a href="https://github.com/seaweedchan/4chan-x/wiki/Frequently-Asked-Questions#what-does-4chan-x-encountered-an-error-while-posting-please-try-again-mean" target=_blank>More info</a>]
""" """
extra = extra =
form: $.formData postData form: $.formData formData
upCallbacks: upCallbacks:
onload: -> onload: ->
# Upload done, waiting for server response. # Upload done, waiting for server response.
@ -1201,9 +1222,9 @@ QR =
postID postID
} }
# Enable auto-posting if we have stuff to post, disable it otherwise. # Enable auto-posting if we have stuff left to post, disable it otherwise.
postsCount = QR.posts.length postsCount = QR.posts.length - 1
QR.cooldown.auto = postsCount > 1 and isReply QR.cooldown.auto = postsCount and isReply
if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount if QR.cooldown.auto and QR.captcha.isEnabled and (captchasCount = QR.captcha.captchas.length) < 3 and captchasCount < postsCount
notif = new Notification 'Quick reply warning', notif = new Notification 'Quick reply warning',
body: "You are running low on cached captchas. Cache count: #{captchasCount}." body: "You are running low on cached captchas. Cache count: #{captchasCount}."
@ -1212,9 +1233,10 @@ QR =
QR.open() QR.open()
QR.captcha.nodes.input.focus() QR.captcha.nodes.input.focus()
window.focus() window.focus()
setTimeout -> notif.onshow = ->
notif.close() setTimeout ->
, 7 * $.SECOND notif.close()
, 7 * $.SECOND
unless Conf['Persistent QR'] or QR.cooldown.auto unless Conf['Persistent QR'] or QR.cooldown.auto
QR.close() QR.close()