Merge branch 'v3' of git://github.com/MayhemYDG/4chan-x into v3
Conflicts: .gitignore Gruntfile.js src/config.coffee src/css/style.css src/features.coffee src/features/posting/qr.coffee
This commit is contained in:
commit
4856a6778d
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,7 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
tmp/
|
|
||||||
4chan_x.user.js
|
|
||||||
Cakefile
|
|
||||||
script.coffee
|
|
||||||
*~
|
*~
|
||||||
*.db
|
*.db
|
||||||
|
tmp-crx/
|
||||||
|
tmp-userjs/
|
||||||
|
tmp-userscript/
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
- Added the option `Hide Unread Count at (0)`, disabled by default.
|
||||||
|
|
||||||
### 3.1.4 - *2013-04-17*
|
### 3.1.4 - *2013-04-17*
|
||||||
|
|
||||||
- Fix QR remembering the file spoiler state when it shouldn't, for real this time.
|
- Fix QR remembering the file spoiler state when it shouldn't, for real this time.
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
## Reporting bugs
|
## Reporting bugs and suggestions
|
||||||
|
|
||||||
|
Reporting bugs:
|
||||||
|
|
||||||
1. Make sure both your **browser** and **4chan X** are up to date.
|
1. Make sure both your **browser** and **4chan X** are up to date.
|
||||||
2. Disable your other extensions & scripts to identify conflicts.
|
2. Disable your other extensions & scripts to identify conflicts.
|
||||||
@ -13,6 +15,12 @@ Open your console with:
|
|||||||
- `Ctrl + Shift + K` on Firefox.
|
- `Ctrl + Shift + K` on Firefox.
|
||||||
- `Ctrl + Shift + O` on Opera.
|
- `Ctrl + Shift + O` on Opera.
|
||||||
|
|
||||||
|
Respect these guidelines:
|
||||||
|
- Describe the issue clearly, put some effort into it. A one-liner isn't a good enough description.
|
||||||
|
- If you want to get your suggestion implemented sooner, make it convincing.
|
||||||
|
- If you want to criticize, make it convincing and constructive.
|
||||||
|
- Be mature. Act like an idiot and you will be blocked without warning.
|
||||||
|
|
||||||
## Development & Contribution
|
## Development & Contribution
|
||||||
|
|
||||||
### Get started
|
### Get started
|
||||||
|
|||||||
@ -5,6 +5,11 @@ module.exports = (grunt) ->
|
|||||||
process:
|
process:
|
||||||
data: pkg
|
data: pkg
|
||||||
|
|
||||||
|
shellOptions =
|
||||||
|
stdout: true
|
||||||
|
stderr: true
|
||||||
|
failOnError: true
|
||||||
|
|
||||||
# Project configuration.
|
# Project configuration.
|
||||||
grunt.initConfig
|
grunt.initConfig
|
||||||
pkg: pkg
|
pkg: pkg
|
||||||
@ -68,27 +73,25 @@ module.exports = (grunt) ->
|
|||||||
'build-userscript'
|
'build-userscript'
|
||||||
]
|
]
|
||||||
|
|
||||||
exec:
|
shell:
|
||||||
commit:
|
commit:
|
||||||
command: ->
|
options: shellOptions
|
||||||
release = "#{pkg.meta.name} v#{pkg.version}"
|
command: [
|
||||||
return [
|
'git checkout <%= pkg.meta.mainBranch %>',
|
||||||
'git checkout ' + pkg.meta.mainBranch,
|
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."',
|
||||||
'git commit -am "Release ' + release + '."',
|
'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."',
|
||||||
'git tag -a ' + pkg.version + ' -m "' + release + '."',
|
'git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
|
||||||
'git tag -af stable-v3 -m "' + release + '."'
|
].join(' && ')
|
||||||
].join(' && ');
|
|
||||||
stdout: true
|
stdout: true
|
||||||
|
|
||||||
push:
|
push:
|
||||||
command: 'git push origin --all && git push origin --tags'
|
options: shellOptions
|
||||||
stdout: true
|
command: 'git push origin --tags -f && git push origin --all'
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
all:
|
all:
|
||||||
options:
|
options:
|
||||||
interrupt: true
|
interrupt: true
|
||||||
nospawn: true
|
|
||||||
files: [
|
files: [
|
||||||
'Gruntfile.coffee'
|
'Gruntfile.coffee'
|
||||||
'package.json'
|
'package.json'
|
||||||
@ -120,7 +123,7 @@ module.exports = (grunt) ->
|
|||||||
grunt.loadNpmTasks 'grunt-contrib-concat'
|
grunt.loadNpmTasks 'grunt-contrib-concat'
|
||||||
grunt.loadNpmTasks 'grunt-contrib-copy'
|
grunt.loadNpmTasks 'grunt-contrib-copy'
|
||||||
grunt.loadNpmTasks 'grunt-contrib-watch'
|
grunt.loadNpmTasks 'grunt-contrib-watch'
|
||||||
grunt.loadNpmTasks 'grunt-exec'
|
grunt.loadNpmTasks 'grunt-shell'
|
||||||
|
|
||||||
grunt.registerTask 'default', [
|
grunt.registerTask 'default', [
|
||||||
'build'
|
'build'
|
||||||
@ -161,8 +164,8 @@ module.exports = (grunt) ->
|
|||||||
|
|
||||||
grunt.registerTask 'release', [
|
grunt.registerTask 'release', [
|
||||||
'default'
|
'default'
|
||||||
'exec:commit'
|
'shell:commit'
|
||||||
'exec:push'
|
'shell:push'
|
||||||
]
|
]
|
||||||
|
|
||||||
grunt.registerTask 'patch', [
|
grunt.registerTask 'patch', [
|
||||||
|
|||||||
194
Gruntfile.js
Normal file
194
Gruntfile.js
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
module.exports = function(grunt) {
|
||||||
|
|
||||||
|
var pkg = grunt.file.readJSON('package.json');
|
||||||
|
var concatOptions = {
|
||||||
|
process: {
|
||||||
|
data: pkg
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var shellOptions = {
|
||||||
|
stdout: true,
|
||||||
|
stderr: true,
|
||||||
|
failOnError: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Project configuration.
|
||||||
|
grunt.initConfig({
|
||||||
|
pkg: pkg,
|
||||||
|
concat: {
|
||||||
|
coffee: {
|
||||||
|
options: concatOptions,
|
||||||
|
src: [
|
||||||
|
'src/config.coffee',
|
||||||
|
'src/globals.coffee',
|
||||||
|
'lib/ui.coffee',
|
||||||
|
'lib/$.coffee',
|
||||||
|
'lib/polyfill.coffee',
|
||||||
|
'src/features.coffee',
|
||||||
|
'src/qr.coffee',
|
||||||
|
'src/report.coffee',
|
||||||
|
'src/databoard.coffee',
|
||||||
|
'src/main.coffee'
|
||||||
|
],
|
||||||
|
dest: 'tmp-<%= pkg.type %>/script.coffee'
|
||||||
|
},
|
||||||
|
crx: {
|
||||||
|
options: concatOptions,
|
||||||
|
files: {
|
||||||
|
'builds/crx/manifest.json': 'src/manifest.json',
|
||||||
|
'builds/crx/script.js': [
|
||||||
|
'src/banner.js',
|
||||||
|
'tmp-<%= pkg.type %>/script.js'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userjs: {
|
||||||
|
options: concatOptions,
|
||||||
|
src: [
|
||||||
|
'src/metadata.js',
|
||||||
|
'src/banner.js',
|
||||||
|
'tmp-<%= pkg.type %>/script.js'
|
||||||
|
],
|
||||||
|
dest: 'builds/<%= pkg.name %>.js'
|
||||||
|
},
|
||||||
|
userscript: {
|
||||||
|
options: concatOptions,
|
||||||
|
files: {
|
||||||
|
'builds/<%= pkg.name %>.meta.js': 'src/metadata.js',
|
||||||
|
'builds/<%= pkg.name %>.user.js': [
|
||||||
|
'src/metadata.js',
|
||||||
|
'src/banner.js',
|
||||||
|
'tmp-<%= pkg.type %>/script.js'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
copy: {
|
||||||
|
crx: {
|
||||||
|
src: 'img/*.png',
|
||||||
|
dest: 'builds/crx/',
|
||||||
|
expand: true,
|
||||||
|
flatten: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
coffee: {
|
||||||
|
script: {
|
||||||
|
src: 'tmp-<%= pkg.type %>/script.coffee',
|
||||||
|
dest: 'tmp-<%= pkg.type %>/script.js'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
concurrent: {
|
||||||
|
build: ['build-crx', 'build-userjs', 'build-userscript']
|
||||||
|
},
|
||||||
|
shell: {
|
||||||
|
commit: {
|
||||||
|
options: shellOptions,
|
||||||
|
command: [
|
||||||
|
'git checkout <%= pkg.meta.mainBranch %>',
|
||||||
|
'git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."',
|
||||||
|
'git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."',
|
||||||
|
'git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."'
|
||||||
|
].join(' && ')
|
||||||
|
},
|
||||||
|
push: {
|
||||||
|
options: shellOptions,
|
||||||
|
command: 'git push origin --tags -f && git push origin --all'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
all: {
|
||||||
|
options: {
|
||||||
|
interrupt: true
|
||||||
|
},
|
||||||
|
files: [
|
||||||
|
'Gruntfile.js',
|
||||||
|
'package.json',
|
||||||
|
'lib/**/*',
|
||||||
|
'src/**/*',
|
||||||
|
'css/**/*',
|
||||||
|
'img/**/*'
|
||||||
|
],
|
||||||
|
tasks: 'build'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
crx: {
|
||||||
|
options: {
|
||||||
|
archive: 'builds/<%= pkg.name %>.zip',
|
||||||
|
level: 9,
|
||||||
|
pretty: true
|
||||||
|
},
|
||||||
|
expand: true,
|
||||||
|
flatten: true,
|
||||||
|
src: 'builds/crx/*',
|
||||||
|
dest: '/'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clean: {
|
||||||
|
builds: 'builds',
|
||||||
|
tmpcrx: 'tmp-crx',
|
||||||
|
tmpuserjs: 'tmp-userjs',
|
||||||
|
tmpuserscript: 'tmp-userscript'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.loadNpmTasks('grunt-bump');
|
||||||
|
grunt.loadNpmTasks('grunt-concurrent');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-clean');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-coffee');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-compress');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-concat');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-copy');
|
||||||
|
grunt.loadNpmTasks('grunt-contrib-watch');
|
||||||
|
grunt.loadNpmTasks('grunt-shell');
|
||||||
|
|
||||||
|
grunt.registerTask('default', ['build']);
|
||||||
|
|
||||||
|
grunt.registerTask('set-build', 'Set the build type variable', function(type) {
|
||||||
|
pkg.type = type;
|
||||||
|
grunt.log.ok('pkg.type = %s', type);
|
||||||
|
});
|
||||||
|
grunt.registerTask('build', ['concurrent:build']);
|
||||||
|
grunt.registerTask('build-crx', [
|
||||||
|
'set-build:crx',
|
||||||
|
'concat:coffee',
|
||||||
|
'coffee:script',
|
||||||
|
'concat:crx',
|
||||||
|
'copy:crx',
|
||||||
|
'clean:tmpcrx'
|
||||||
|
]);
|
||||||
|
grunt.registerTask('build-userjs', [
|
||||||
|
'set-build:userjs',
|
||||||
|
'concat:coffee',
|
||||||
|
'coffee:script',
|
||||||
|
'concat:userjs',
|
||||||
|
'clean:tmpuserjs'
|
||||||
|
]);
|
||||||
|
grunt.registerTask('build-userscript', [
|
||||||
|
'set-build:userscript',
|
||||||
|
'concat:coffee',
|
||||||
|
'coffee:script',
|
||||||
|
'concat:userscript',
|
||||||
|
'clean:tmpuserscript'
|
||||||
|
]);
|
||||||
|
|
||||||
|
grunt.registerTask('release', ['shell:commit', 'shell:push', 'build-crx', 'compress:crx']);
|
||||||
|
grunt.registerTask('patch', ['bump', 'reloadPkg', 'updcl:3', 'release']);
|
||||||
|
grunt.registerTask('minor', ['bump:minor', 'reloadPkg', 'updcl:2', 'release']);
|
||||||
|
grunt.registerTask('major', ['bump:major', 'reloadPkg', 'updcl:1', 'release']);
|
||||||
|
|
||||||
|
grunt.registerTask('reloadPkg', 'Reload the package', function() {
|
||||||
|
// Update the `pkg` object with the new version.
|
||||||
|
pkg = grunt.file.readJSON('package.json');
|
||||||
|
concatOptions.process.data = pkg;
|
||||||
|
grunt.log.ok('pkg reloaded.');
|
||||||
|
});
|
||||||
|
|
||||||
|
grunt.registerTask('updcl', 'Update the changelog', function(i) {
|
||||||
|
// i is the number of #s for markdown.
|
||||||
|
var version = new Array(+i + 1).join('#') + ' ' + pkg.version + ' - *' + grunt.template.today('yyyy-mm-dd') + '*';
|
||||||
|
grunt.file.write('CHANGELOG.md', version + '\n\n' + grunt.file.read('CHANGELOG.md'));
|
||||||
|
grunt.log.ok('Changelog updated for v' + pkg.version + '.');
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10
package.json
10
package.json
@ -17,15 +17,15 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"grunt": "~0.4.1",
|
"grunt": "~0.4.1",
|
||||||
"grunt-bump": "~0.0.0",
|
"grunt-bump": "~0.0.2",
|
||||||
"grunt-concurrent": "~0.1.1",
|
"grunt-concurrent": "~0.2.0",
|
||||||
"grunt-contrib-clean": "~0.4.0",
|
"grunt-contrib-clean": "~0.4.1",
|
||||||
"grunt-contrib-coffee": "~0.6.6",
|
"grunt-contrib-coffee": "~0.6.7",
|
||||||
"grunt-contrib-compress": "~0.4.10",
|
"grunt-contrib-compress": "~0.4.10",
|
||||||
"grunt-contrib-concat": "~0.2.0",
|
"grunt-contrib-concat": "~0.2.0",
|
||||||
"grunt-contrib-copy": "~0.4.1",
|
"grunt-contrib-copy": "~0.4.1",
|
||||||
"grunt-contrib-watch": "~0.3.1",
|
"grunt-contrib-watch": "~0.3.1",
|
||||||
"grunt-exec": "~0.4.0"
|
"grunt-shell": "~0.2.2"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@ -97,10 +97,6 @@ Config =
|
|||||||
true
|
true
|
||||||
'Add buttons to hide single replies.'
|
'Add buttons to hide single replies.'
|
||||||
]
|
]
|
||||||
'Hiding Buttons': [
|
|
||||||
true
|
|
||||||
'Add buttons to hide threads / replies, in addition to menu links.'
|
|
||||||
]
|
|
||||||
'Stubs': [
|
'Stubs': [
|
||||||
true
|
true
|
||||||
'Show stubs of hidden threads / replies.'
|
'Show stubs of hidden threads / replies.'
|
||||||
@ -141,6 +137,10 @@ Config =
|
|||||||
true
|
true
|
||||||
'Add a drop-down menu to posts.'
|
'Add a drop-down menu to posts.'
|
||||||
]
|
]
|
||||||
|
'Report Link': [
|
||||||
|
true
|
||||||
|
'Add a report link to the menu.'
|
||||||
|
]
|
||||||
'Thread Hiding Link': [
|
'Thread Hiding Link': [
|
||||||
true
|
true
|
||||||
'Add a link to hide entire threads.'
|
'Add a link to hide entire threads.'
|
||||||
@ -148,19 +148,17 @@ Config =
|
|||||||
'Reply Hiding Link': [
|
'Reply Hiding Link': [
|
||||||
true
|
true
|
||||||
'Add a link to hide single replies.'
|
'Add a link to hide single replies.'
|
||||||
]
|
|
||||||
'Report Link': [
|
|
||||||
true
|
|
||||||
'Add a report link to the menu.'
|
|
||||||
]
|
]
|
||||||
'Delete Link': [
|
'Delete Link': [
|
||||||
true
|
true
|
||||||
'Add post and image deletion links to the menu.'
|
'Add post and image deletion links to the menu.'
|
||||||
]
|
]
|
||||||
|
<% if (type === 'crx') { %>
|
||||||
'Download Link': [
|
'Download Link': [
|
||||||
true
|
true
|
||||||
'Add a download with original filename link to the menu. Chrome-only currently.'
|
'Add a download with original filename link to the menu. Chrome-only currently.'
|
||||||
]
|
]
|
||||||
|
<% } %>
|
||||||
'Archive Link': [
|
'Archive Link': [
|
||||||
true
|
true
|
||||||
'Add an archive link to the menu.'
|
'Add an archive link to the menu.'
|
||||||
@ -175,6 +173,10 @@ Config =
|
|||||||
true
|
true
|
||||||
'Show the unread posts count in the tab title.'
|
'Show the unread posts count in the tab title.'
|
||||||
]
|
]
|
||||||
|
'Hide Unread Count at (0)': [
|
||||||
|
false
|
||||||
|
'Hide the unread posts count when it reaches 0.'
|
||||||
|
]
|
||||||
'Unread Tab Icon': [
|
'Unread Tab Icon': [
|
||||||
true
|
true
|
||||||
'Show a different favicon when there are unread posts.'
|
'Show a different favicon when there are unread posts.'
|
||||||
@ -183,6 +185,10 @@ Config =
|
|||||||
true
|
true
|
||||||
'Show a line to distinguish read posts from unread ones.'
|
'Show a line to distinguish read posts from unread ones.'
|
||||||
]
|
]
|
||||||
|
'Scroll to Last Read Post': [
|
||||||
|
true
|
||||||
|
'Scroll back to the last read post when reopening a thread.'
|
||||||
|
]
|
||||||
'Thread Excerpt': [
|
'Thread Excerpt': [
|
||||||
true
|
true
|
||||||
'Show an excerpt of the thread in the tab title.'
|
'Show an excerpt of the thread in the tab title.'
|
||||||
|
|||||||
@ -97,10 +97,6 @@ a[href="javascript:;"] {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
/* Header */
|
/* Header */
|
||||||
.fourchan-x body {
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.fixed.top body {
|
.fixed.top body {
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
}
|
}
|
||||||
@ -155,24 +151,18 @@ a[href="javascript:;"] {
|
|||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
#header-bar #toggle-header-bar {
|
#boardNavDesktop #toggle-header-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.fixed #header-bar #toggle-header-bar {
|
.fixed #boardNavDesktop #toggle-header-bar {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.fixed #header-bar #toggle-header-bar {
|
.fixed.top boardNavDesktop #toggle-header-bar {
|
||||||
cursor: n-resize;
|
|
||||||
}
|
|
||||||
.fixed.top header-bar #toggle-header-bar {
|
|
||||||
top: 100%;
|
top: 100%;
|
||||||
}
|
}
|
||||||
.fixed.bottom #header-bar #toggle-header-bar {
|
.fixed.bottom #boardNavDesktop #toggle-header-bar {
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
}
|
}
|
||||||
.fixed #header-bar #header-bar.autohide #toggle-header-bar {
|
|
||||||
cursor: s-resize;
|
|
||||||
}
|
|
||||||
#header-bar a:not(.entry) {
|
#header-bar a:not(.entry) {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
@ -203,16 +193,17 @@ a[href="javascript:;"] {
|
|||||||
|
|
||||||
/* Notifications */
|
/* Notifications */
|
||||||
#notifications {
|
#notifications {
|
||||||
height: 0;
|
|
||||||
text-align: center;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
height: 0;
|
||||||
|
text-align: center;
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
|
transition: all .8s .6s cubic-bezier(.55, .055, .675, .19);
|
||||||
}
|
}
|
||||||
.top:not(.autohide) ~ #notifications {
|
.fixed.top #header-bar #notifications {
|
||||||
top: 2em;
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
}
|
}
|
||||||
.notification {
|
.notification {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
@ -257,6 +248,10 @@ a[href="javascript:;"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Settings */
|
/* Settings */
|
||||||
|
:root.fourchan-x body {
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
#overlay {
|
#overlay {
|
||||||
background-color: rgba(0, 0, 0, .5);
|
background-color: rgba(0, 0, 0, .5);
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -274,6 +269,7 @@ a[href="javascript:;"] {
|
|||||||
width: 900px;
|
width: 900px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin: auto;
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -478,6 +474,8 @@ a.hide-announcement {
|
|||||||
#qp img {
|
#qp img {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
max-height: 80vh;
|
||||||
|
max-width: 50vw;
|
||||||
}
|
}
|
||||||
.qphl {
|
.qphl {
|
||||||
outline: 2px solid rgba(216, 94, 49, .7);
|
outline: 2px solid rgba(216, 94, 49, .7);
|
||||||
|
|||||||
4382
src/features.coffee
Normal file
4382
src/features.coffee
Normal file
File diff suppressed because it is too large
Load Diff
@ -127,7 +127,7 @@ ThreadHiding =
|
|||||||
ThreadHiding.saveHiddenState thread
|
ThreadHiding.saveHiddenState thread
|
||||||
|
|
||||||
hide: (thread, makeStub=Conf['Stubs']) ->
|
hide: (thread, makeStub=Conf['Stubs']) ->
|
||||||
return if thread.hidden
|
return if thread.isHidden
|
||||||
{OP} = thread
|
{OP} = thread
|
||||||
threadRoot = OP.nodes.root.parentNode
|
threadRoot = OP.nodes.root.parentNode
|
||||||
threadRoot.hidden = thread.isHidden = true
|
threadRoot.hidden = thread.isHidden = true
|
||||||
|
|||||||
@ -1,14 +1,7 @@
|
|||||||
DownloadLink =
|
DownloadLink =
|
||||||
init: ->
|
init: ->
|
||||||
<% if (type === 'userscript') { %>
|
|
||||||
# Firefox won't let us download cross-domain content.
|
|
||||||
return
|
|
||||||
<% } %>
|
|
||||||
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link']
|
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link']
|
||||||
|
|
||||||
# Test for download feature support.
|
|
||||||
return unless 'download' of $.el 'a'
|
|
||||||
|
|
||||||
a = $.el 'a',
|
a = $.el 'a',
|
||||||
className: 'download-link'
|
className: 'download-link'
|
||||||
textContent: 'Download file'
|
textContent: 'Download file'
|
||||||
|
|||||||
@ -5,6 +5,7 @@ PSAHiding =
|
|||||||
$.addClass doc, 'hide-announcement'
|
$.addClass doc, 'hide-announcement'
|
||||||
|
|
||||||
$.on d, '4chanXInitFinished', @setup
|
$.on d, '4chanXInitFinished', @setup
|
||||||
|
|
||||||
setup: ->
|
setup: ->
|
||||||
$.off d, '4chanXInitFinished', PSAHiding.setup
|
$.off d, '4chanXInitFinished', PSAHiding.setup
|
||||||
|
|
||||||
@ -24,11 +25,11 @@ PSAHiding =
|
|||||||
$.rmClass doc, 'hide-announcement'
|
$.rmClass doc, 'hide-announcement'
|
||||||
|
|
||||||
$.sync 'hiddenPSAs', PSAHiding.sync
|
$.sync 'hiddenPSAs', PSAHiding.sync
|
||||||
|
|
||||||
toggle: (e) ->
|
toggle: (e) ->
|
||||||
hide = $.hasClass @, 'hide-announcement'
|
hide = $.hasClass @, 'hide-announcement'
|
||||||
text = PSAHiding.trim $.id 'globalMessage'
|
text = PSAHiding.trim $.id 'globalMessage'
|
||||||
$.get 'hiddenPSAs', [], (item) ->
|
$.get 'hiddenPSAs', [], ({hiddenPSAs}) ->
|
||||||
{hiddenPSAs} = item
|
|
||||||
if hide
|
if hide
|
||||||
hiddenPSAs.push text
|
hiddenPSAs.push text
|
||||||
else
|
else
|
||||||
@ -37,12 +38,13 @@ PSAHiding =
|
|||||||
hiddenPSAs = hiddenPSAs[-5..]
|
hiddenPSAs = hiddenPSAs[-5..]
|
||||||
PSAHiding.sync hiddenPSAs
|
PSAHiding.sync hiddenPSAs
|
||||||
$.set 'hiddenPSAs', hiddenPSAs
|
$.set 'hiddenPSAs', hiddenPSAs
|
||||||
|
|
||||||
sync: (hiddenPSAs) ->
|
sync: (hiddenPSAs) ->
|
||||||
{btn} = PSAHiding
|
{btn} = PSAHiding
|
||||||
psa = $.id 'globalMessage'
|
psa = $.id 'globalMessage'
|
||||||
[psa.hidden, btn.innerHTML, btn.className] = if PSAHiding.trim(psa) in hiddenPSAs
|
[psa.hidden, btn.firstChild.textContent, btn.className] = if PSAHiding.trim(psa) in hiddenPSAs
|
||||||
[true, '<span>[ + ]</span>', 'show-announcement']
|
[true, '[\u00A0+\u00A0]', 'show-announcement']
|
||||||
else
|
else
|
||||||
[false, '<span>[ - ]</span>', 'hide-announcement']
|
[false, '[\u00A0-\u00A0]', 'hide-announcement']
|
||||||
trim: (psa) ->
|
trim: (psa) ->
|
||||||
psa.textContent.replace(/\W+/g, '').toLowerCase()
|
psa.textContent.replace(/\W+/g, '').toLowerCase()
|
||||||
@ -9,6 +9,7 @@ CatalogLinks =
|
|||||||
|
|
||||||
input = $ 'input', el
|
input = $ 'input', el
|
||||||
$.on input, 'change', @toggle
|
$.on input, 'change', @toggle
|
||||||
|
$.sync 'Header catalog links', CatalogLinks.set
|
||||||
|
|
||||||
$.event 'AddMenuEntry',
|
$.event 'AddMenuEntry',
|
||||||
type: 'header'
|
type: 'header'
|
||||||
@ -21,10 +22,15 @@ CatalogLinks =
|
|||||||
# it might be incomplete otherwise.
|
# it might be incomplete otherwise.
|
||||||
$.asap (-> $.id 'boardNavMobile'), ->
|
$.asap (-> $.id 'boardNavMobile'), ->
|
||||||
# Set links on load.
|
# Set links on load.
|
||||||
CatalogLinks.toggle.call input
|
CatalogLinks.set input.checked
|
||||||
|
|
||||||
toggle: ->
|
toggle: ->
|
||||||
|
$.event 'CloseMenu'
|
||||||
$.set 'Header catalog links', useCatalog = @checked
|
$.set 'Header catalog links', useCatalog = @checked
|
||||||
|
CatalogLinks.set useCatalog
|
||||||
|
|
||||||
|
set: (useCatalog) ->
|
||||||
|
path = if useCatalog then 'catalog' else ''
|
||||||
for a in $$ 'a', $.id('boardNavDesktop')
|
for a in $$ 'a', $.id('boardNavDesktop')
|
||||||
board = a.pathname.split('/')[1]
|
board = a.pathname.split('/')[1]
|
||||||
continue if ['f', 'status', '4chan'].contains(board) or !board
|
continue if ['f', 'status', '4chan'].contains(board) or !board
|
||||||
@ -34,7 +40,7 @@ CatalogLinks =
|
|||||||
else
|
else
|
||||||
"//boards.4chan.org/#{board}/"
|
"//boards.4chan.org/#{board}/"
|
||||||
else
|
else
|
||||||
a.pathname = "/#{board}/#{if useCatalog then 'catalog' else ''}"
|
a.pathname = "/#{board}/#{path}"
|
||||||
a.title = if useCatalog then "#{a.title} - Catalog" else a.title.replace(/\ -\ Catalog$/, '')
|
a.title = if useCatalog then "#{a.title} - Catalog" else a.title.replace(/\ -\ Catalog$/, '')
|
||||||
@title = "Turn catalog links #{if useCatalog then 'off' else 'on'}."
|
@title = "Turn catalog links #{if useCatalog then 'off' else 'on'}."
|
||||||
|
|
||||||
|
|||||||
@ -5,36 +5,32 @@ Header =
|
|||||||
innerHTML: '<i></i>'
|
innerHTML: '<i></i>'
|
||||||
|
|
||||||
@menu = new UI.Menu 'header'
|
@menu = new UI.Menu 'header'
|
||||||
$.on @menuButton, 'click', @menuToggle
|
|
||||||
$.on @toggle, 'mousedown', @toggleBarVisibility
|
|
||||||
$.on window, 'load hashchange', Header.hashScroll
|
|
||||||
|
|
||||||
@positionToggler = $.el 'span',
|
headerToggler = $.el 'label',
|
||||||
textContent: 'Header Position'
|
innerHTML: '<input type=checkbox name="Header auto-hide"> Auto-hide header'
|
||||||
className: 'header-position-link'
|
|
||||||
|
@headerToggler = headerToggler.firstElementChild
|
||||||
|
|
||||||
|
$.on @menuButton, 'click', @menuToggle
|
||||||
|
$.on window, 'load hashchange', Header.hashScroll
|
||||||
|
$.on @headerToggler, 'change', @toggleBarVisibility
|
||||||
|
|
||||||
{createSubEntry} = Header
|
{createSubEntry} = Header
|
||||||
subEntries = []
|
subEntries = []
|
||||||
for setting in ['sticky top', 'sticky bottom', 'top']
|
for setting in ['sticky top', 'sticky bottom', 'top']
|
||||||
subEntries.push createSubEntry setting
|
subEntries.push createSubEntry setting
|
||||||
|
|
||||||
|
subEntries.push {el: headerToggler}
|
||||||
|
|
||||||
@addShortcut Header.menuButton
|
@addShortcut Header.menuButton
|
||||||
|
|
||||||
$.event 'AddMenuEntry',
|
$.event 'AddMenuEntry',
|
||||||
type: 'header'
|
type: 'header'
|
||||||
el: @positionToggler
|
el: $.el 'span',
|
||||||
order: 108
|
textContent: 'Header'
|
||||||
|
order: 105
|
||||||
subEntries: subEntries
|
subEntries: subEntries
|
||||||
|
|
||||||
@headerToggler = $.el 'label',
|
|
||||||
innerHTML: "<input type=checkbox #{if Conf['Header auto-hide'] then 'checked' else ''}> Auto-hide header"
|
|
||||||
$.on @headerToggler.firstElementChild, 'change', @toggleBarVisibility
|
|
||||||
|
|
||||||
$.event 'AddMenuEntry',
|
|
||||||
type: 'header'
|
|
||||||
el: @headerToggler
|
|
||||||
order: 109
|
|
||||||
|
|
||||||
$.on d, 'CreateNotification', @createNotification
|
$.on d, 'CreateNotification', @createNotification
|
||||||
|
|
||||||
$.asap (-> d.body), ->
|
$.asap (-> d.body), ->
|
||||||
@ -57,11 +53,8 @@ Header =
|
|||||||
|
|
||||||
toggle: $.el 'div',
|
toggle: $.el 'div',
|
||||||
id: 'toggle-header-bar'
|
id: 'toggle-header-bar'
|
||||||
|
|
||||||
settings: $.el 'div',
|
|
||||||
id: 'settings-container'
|
|
||||||
|
|
||||||
createSubEntry: (setting)->
|
createSubEntry: (setting) ->
|
||||||
label = $.el 'label',
|
label = $.el 'label',
|
||||||
textContent: "#{setting}"
|
textContent: "#{setting}"
|
||||||
|
|
||||||
@ -85,7 +78,7 @@ Header =
|
|||||||
$.sync 'Header auto-hide', Header.setBarVisibility
|
$.sync 'Header auto-hide', Header.setBarVisibility
|
||||||
|
|
||||||
$.add fullBoardList, [nav.childNodes...]
|
$.add fullBoardList, [nav.childNodes...]
|
||||||
$.add nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle, Header.settings]
|
$.add nav, [fullBoardList, Header.shortcuts, Header.bar, Header.toggle]
|
||||||
|
|
||||||
if Conf['Custom Board Navigation']
|
if Conf['Custom Board Navigation']
|
||||||
fullBoardList.hidden = true
|
fullBoardList.hidden = true
|
||||||
@ -181,11 +174,27 @@ Header =
|
|||||||
$.addClass doc, 'top'
|
$.addClass doc, 'top'
|
||||||
|
|
||||||
setBarVisibility: (hide) ->
|
setBarVisibility: (hide) ->
|
||||||
Header.headerToggler.firstElementChild.checked = hide
|
Header.headerToggler.checked = hide
|
||||||
|
$.event 'CloseMenu'
|
||||||
(if hide then $.addClass else $.rmClass) Header.nav, 'autohide'
|
(if hide then $.addClass else $.rmClass) Header.nav, 'autohide'
|
||||||
|
|
||||||
|
toggleBarVisibility: (e) ->
|
||||||
|
return if e.type is 'mousedown' and e.button isnt 0 # not LMB
|
||||||
|
hide = if @nodeName is 'INPUT'
|
||||||
|
@checked
|
||||||
|
else
|
||||||
|
!$.hasClass Header.bar, 'autohide'
|
||||||
|
Conf['Header auto-hide'] = hide
|
||||||
|
$.set 'Header auto-hide', hide
|
||||||
|
Header.setBarVisibility hide
|
||||||
|
message = if hide
|
||||||
|
'The header bar will automatically hide itself.'
|
||||||
|
else
|
||||||
|
'The header bar will remain visible.'
|
||||||
|
new Notification 'info', message, 2
|
||||||
|
|
||||||
hashScroll: ->
|
hashScroll: ->
|
||||||
return unless post = @location.hash[1..]
|
return unless post = $.id @location.hash[1..]
|
||||||
return if (Get.postFromRoot post).isHidden
|
return if (Get.postFromRoot post).isHidden
|
||||||
Header.scrollToPost post
|
Header.scrollToPost post
|
||||||
|
|
||||||
@ -196,20 +205,6 @@ Header =
|
|||||||
top += - headRect.top - headRect.height
|
top += - headRect.top - headRect.height
|
||||||
(if $.engine is 'webkit' then d.body else doc).scrollTop += top
|
(if $.engine is 'webkit' then d.body else doc).scrollTop += top
|
||||||
|
|
||||||
toggleBarVisibility: (e) ->
|
|
||||||
return if e.type is 'mousedown' and e.button isnt 0 # not LMB
|
|
||||||
hide = if @nodeName is 'INPUT'
|
|
||||||
@checked
|
|
||||||
else
|
|
||||||
!$.hasClass Header.nav, 'autohide'
|
|
||||||
Header.setBarVisibility hide
|
|
||||||
message = if hide
|
|
||||||
'The header bar will automatically hide itself.'
|
|
||||||
else
|
|
||||||
'The header bar will remain visible.'
|
|
||||||
new Notification 'info', message, 2
|
|
||||||
$.set 'Header auto-hide', hide
|
|
||||||
|
|
||||||
addShortcut: (el) ->
|
addShortcut: (el) ->
|
||||||
shortcut = $.el 'span',
|
shortcut = $.el 'span',
|
||||||
className: 'shortcut'
|
className: 'shortcut'
|
||||||
|
|||||||
@ -8,6 +8,8 @@ Redirect =
|
|||||||
"//nsfw.foolz.us/#{boardID}/full_image/#{filename}"
|
"//nsfw.foolz.us/#{boardID}/full_image/#{filename}"
|
||||||
when 'po'
|
when 'po'
|
||||||
"//archive.thedarkcave.org/#{boardID}/full_image/#{filename}"
|
"//archive.thedarkcave.org/#{boardID}/full_image/#{filename}"
|
||||||
|
when 'hr', 'tv'
|
||||||
|
"http://archive.4plebs.org/#{boardID}/full_image/#{filename}"
|
||||||
when 'ck', 'fa', 'lit', 's4s'
|
when 'ck', 'fa', 'lit', 's4s'
|
||||||
"//fuuka.warosu.org/#{boardID}/full_image/#{filename}"
|
"//fuuka.warosu.org/#{boardID}/full_image/#{filename}"
|
||||||
when 'cgl', 'g', 'mu', 'w'
|
when 'cgl', 'g', 'mu', 'w'
|
||||||
@ -26,6 +28,8 @@ Redirect =
|
|||||||
"https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
|
"https://nsfw.foolz.us/_/api/chan/post/?board=#{boardID}&num=#{postID}"
|
||||||
when 'c', 'int', 'out', 'po'
|
when 'c', 'int', 'out', 'po'
|
||||||
"//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}"
|
"//archive.thedarkcave.org/_/api/chan/post/?board=#{boardID}&num=#{postID}"
|
||||||
|
when 'hr', 'x'
|
||||||
|
"http://archive.4plebs.org/_/api/chan/post/?board=#{boardID}&num=#{postID}"
|
||||||
# for fuuka-based archives:
|
# for fuuka-based archives:
|
||||||
# https://github.com/eksopl/fuuka/issues/27
|
# https://github.com/eksopl/fuuka/issues/27
|
||||||
to: (data) ->
|
to: (data) ->
|
||||||
@ -37,6 +41,8 @@ Redirect =
|
|||||||
Redirect.path '//nsfw.foolz.us', 'foolfuuka', data
|
Redirect.path '//nsfw.foolz.us', 'foolfuuka', data
|
||||||
when 'int', 'out', 'po'
|
when 'int', 'out', 'po'
|
||||||
Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data
|
Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data
|
||||||
|
when 'hr'
|
||||||
|
Redirect.path 'http://archive.4plebs.org', 'foolfuuka', data
|
||||||
when 'ck', 'fa', 'lit', 's4s'
|
when 'ck', 'fa', 'lit', 's4s'
|
||||||
Redirect.path '//fuuka.warosu.org', 'fuuka', data
|
Redirect.path '//fuuka.warosu.org', 'fuuka', data
|
||||||
when 'diy', 'g', 'sci'
|
when 'diy', 'g', 'sci'
|
||||||
|
|||||||
@ -6,7 +6,4 @@ ThreadExcerpt =
|
|||||||
name: 'Thread Excerpt'
|
name: 'Thread Excerpt'
|
||||||
cb: @node
|
cb: @node
|
||||||
node: ->
|
node: ->
|
||||||
d.title = if (excerpt = Get.threadExcerpt @).length > 80
|
d.title = Get.threadExcerpt @
|
||||||
"#{excerpt[...77]}..."
|
|
||||||
else
|
|
||||||
excerpt
|
|
||||||
@ -26,8 +26,9 @@ Unread =
|
|||||||
$.on d, 'ThreadUpdate', Unread.onUpdate
|
$.on d, 'ThreadUpdate', Unread.onUpdate
|
||||||
$.on d, 'scroll visibilitychange', Unread.read
|
$.on d, 'scroll visibilitychange', Unread.read
|
||||||
$.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
|
$.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
|
||||||
|
$.on window, 'load', Unread.scroll if Conf['Scroll to Last Read Post']
|
||||||
|
|
||||||
return unless Conf['Scroll to Last Read Post']
|
scroll: ->
|
||||||
# Let the header's onload callback handle it.
|
# Let the header's onload callback handle it.
|
||||||
return if (hash = location.hash.match /\d+/) and hash[0] of @posts
|
return if (hash = location.hash.match /\d+/) and hash[0] of @posts
|
||||||
if Unread.posts.length
|
if Unread.posts.length
|
||||||
@ -136,10 +137,7 @@ Unread =
|
|||||||
count = Unread.posts.length
|
count = Unread.posts.length
|
||||||
|
|
||||||
if Conf['Unread Count']
|
if Conf['Unread Count']
|
||||||
d.title = if g.DEAD
|
d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}"
|
||||||
"(#{Unread.posts.length}) /#{g.BOARD}/ - 404"
|
|
||||||
else
|
|
||||||
"(#{Unread.posts.length}) #{Unread.title}"
|
|
||||||
<% if (type === 'crx') { %>
|
<% if (type === 'crx') { %>
|
||||||
# XXX Chrome bug where it doesn't always update the tab title.
|
# XXX Chrome bug where it doesn't always update the tab title.
|
||||||
# crbug.com/124381
|
# crbug.com/124381
|
||||||
|
|||||||
@ -805,7 +805,7 @@ QR =
|
|||||||
QR.mimeTypes = mimeTypes.split ', '
|
QR.mimeTypes = mimeTypes.split ', '
|
||||||
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
|
# Add empty mimeType to avoid errors with URLs selected in Window's file dialog.
|
||||||
QR.mimeTypes.push ''
|
QR.mimeTypes.push ''
|
||||||
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
nodes.fileInput.max = $('input[name=MAX_FILE_SIZE]').value
|
||||||
<% if (type !== 'userjs') { %>
|
<% if (type !== 'userjs') { %>
|
||||||
# Opera's accept attribute is fucked up
|
# Opera's accept attribute is fucked up
|
||||||
nodes.fileInput.accept = "text/*, #{mimeTypes}"
|
nodes.fileInput.accept = "text/*, #{mimeTypes}"
|
||||||
@ -842,8 +842,8 @@ QR =
|
|||||||
$.on elm, 'blur', QR.focusout
|
$.on elm, 'blur', QR.focusout
|
||||||
$.on elm, 'focus', QR.focusin
|
$.on elm, 'focus', QR.focusin
|
||||||
<% } %>
|
<% } %>
|
||||||
$.on QR.nodes.el, 'focusin', QR.focusin
|
$.on dialog, 'focusin', QR.focusin
|
||||||
$.on QR.nodes.el, 'focusout', QR.focusout
|
$.on dialog, 'focusout', QR.focusout
|
||||||
$.on nodes.autohide, 'change', QR.toggleHide
|
$.on nodes.autohide, 'change', QR.toggleHide
|
||||||
$.on nodes.close, 'click', QR.close
|
$.on nodes.close, 'click', QR.close
|
||||||
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
|
$.on nodes.dumpButton, 'click', -> nodes.el.classList.toggle 'dump'
|
||||||
|
|||||||
@ -5,6 +5,8 @@ Get =
|
|||||||
OP.info.comment.replace(/\n+/g, ' // ') or
|
OP.info.comment.replace(/\n+/g, ' // ') or
|
||||||
Conf['Anonymize'] and 'Anonymous' or
|
Conf['Anonymize'] and 'Anonymous' or
|
||||||
$('.nameBlock', OP.nodes.info).textContent.trim()
|
$('.nameBlock', OP.nodes.info).textContent.trim()
|
||||||
|
if excerpt.length > 70
|
||||||
|
excerpt = "#{excerpt[...67]}..."
|
||||||
"/#{thread.board}/ - #{excerpt}"
|
"/#{thread.board}/ - #{excerpt}"
|
||||||
postFromRoot: (root) ->
|
postFromRoot: (root) ->
|
||||||
link = $ 'a[title="Highlight this post"]', root
|
link = $ 'a[title="Highlight this post"]', root
|
||||||
|
|||||||
@ -51,7 +51,7 @@ Settings =
|
|||||||
<div class=credits>
|
<div class=credits>
|
||||||
<a href='<%= meta.page %>' target=_blank><%= meta.name %></a> |
|
<a href='<%= meta.page %>' target=_blank><%= meta.name %></a> |
|
||||||
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> |
|
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CHANGELOG.md' target=_blank>#{g.VERSION}</a> |
|
||||||
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CONTRIBUTING.md#reporting-bugs' target=_blank>Issues</a> |
|
<a href='<%= meta.repo %>blob/<%= meta.mainBranch %>/CONTRIBUTING.md#reporting-bugs-and-suggestions' target=_blank>Issues</a> |
|
||||||
<a href=javascript:; class=close title=Close>×</a>
|
<a href=javascript:; class=close title=Close>×</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user