mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
243 Commits
Author | SHA1 | Date | |
---|---|---|---|
48c9001194 | |||
ea675d20cb | |||
6a95a66f12 | |||
deffe91fda | |||
8c01c7714f | |||
377fe52072 | |||
cd6683c2d8 | |||
2c6434b08d | |||
99a69333e6 | |||
08e62ec885 | |||
65202189e1 | |||
c60ec22b92 | |||
ed83e11552 | |||
db0c33bb14 | |||
1a8de9ef3a | |||
6cc2a91632 | |||
bd9284b7f8 | |||
4c78cf8c47 | |||
a616cf6987 | |||
e3401b3993 | |||
0e6427d8bc | |||
e19d7041d1 | |||
f1a09c21d4 | |||
72e104b145 | |||
af6eff9ff8 | |||
0ff9f9d5a2 | |||
dce7136f15 | |||
978a384d9e | |||
53ec25f4c4 | |||
0a5279c2c1 | |||
6f549cf2db | |||
80da6467f6 | |||
eb49aea683 | |||
4f5ea9c5ed | |||
73c53fa4e2 | |||
f4afb145d6 | |||
9c04400369 | |||
91f5a42459 | |||
c9eae00c8c | |||
d2a4e50669 | |||
4fe9c5f4ca | |||
6da18036a4 | |||
2af304b844 | |||
0c05330cfc | |||
1231469a35 | |||
edf9083552 | |||
dd56c287b5 | |||
fa3b6275b3 | |||
54eab0aa35 | |||
734e28e014 | |||
369ddaf2f8 | |||
83442b4977 | |||
9df090b4d9 | |||
48e7eb10f1 | |||
69922fccb6 | |||
9b02a0bd5e | |||
979d8409d5 | |||
7a42c7a69b | |||
9329717335 | |||
0839dafd34 | |||
9a9a475037 | |||
80d272d60b | |||
8f0835f27b | |||
b8699d59d2 | |||
2484aef492 | |||
e14f08ddc6 | |||
e0fc790822 | |||
7b236b02c9 | |||
bbde0ab9a0 | |||
e471d6ad2e | |||
765e1a711b | |||
26127eaaf5 | |||
4117f63375 | |||
0121b952d1 | |||
9edee46dcf | |||
f36cdc8719 | |||
940631d3bb | |||
9e7c77cd73 | |||
8e1e6af232 | |||
93910a1655 | |||
2ec6b978ac | |||
a4215e35dc | |||
a48116aa05 | |||
d69ef710b3 | |||
1d8cfd5a89 | |||
68bd168434 | |||
b18acf3982 | |||
065a466af8 | |||
03d768881e | |||
abc6e018b9 | |||
3e6b98df92 | |||
d7feb2792c | |||
2fdd8cb3ab | |||
a2dc964e52 | |||
6510d0750c | |||
5ed70b2ec4 | |||
14377933a7 | |||
e80c482891 | |||
987a3aa8f2 | |||
7081b5be90 | |||
116919d2a2 | |||
a5a06bf2d1 | |||
e6445b431f | |||
d3cabc4a36 | |||
8a10fc8ffd | |||
3879c2ec20 | |||
2235a72d2f | |||
c8fe0fcdff | |||
cbf67587e2 | |||
565027269c | |||
defada45ab | |||
b29bf8b37a | |||
b22c887e4b | |||
45b6df020a | |||
8da22cbd5e | |||
70385cfe3d | |||
b1a20a7134 | |||
6a6c4dc822 | |||
9730aa5c05 | |||
1fe22a4d0a | |||
c9cb9aa539 | |||
d85e746a65 | |||
b6a5be74cf | |||
320c16743d | |||
d43758bcc2 | |||
60ab9246c6 | |||
3972b902d8 | |||
2bf361c64a | |||
d39439d549 | |||
2a69f0193f | |||
e35e709927 | |||
a98ca55391 | |||
db9132432b | |||
23a28ce69c | |||
a962bb351a | |||
a08c7d65da | |||
7596f9042c | |||
9b10d2bebf | |||
e15dffa1dc | |||
4ce29cf222 | |||
26a1451ff6 | |||
c770ad8f28 | |||
3f52aceca4 | |||
7519e071e7 | |||
12ec43f098 | |||
4ff8be6a2f | |||
4b3529272e | |||
a1fbeb91a0 | |||
59d8b0d4c5 | |||
69421464f6 | |||
85cb3d4702 | |||
f8c7375b01 | |||
cdf454818c | |||
4848bee5e3 | |||
36698cddc2 | |||
1c4c5c5f91 | |||
253e28c1b5 | |||
6d78c5e55d | |||
795891767e | |||
234afc8dfe | |||
87735110aa | |||
674d6c35d7 | |||
4afece8d50 | |||
90b0d77147 | |||
043b182b5e | |||
3c138685ea | |||
a1b762c65f | |||
4bc58a3c95 | |||
fea9a94945 | |||
467b4a7630 | |||
8e5798ab8c | |||
e4aa38f159 | |||
ba4df16499 | |||
9814b132c3 | |||
0014721053 | |||
77bf3bdc3c | |||
c2be365b6e | |||
01e1641475 | |||
7044d2aaee | |||
49feb932f3 | |||
5681fd11ef | |||
e087b83082 | |||
87b3572ce5 | |||
5467ca6b7e | |||
d00d282bff | |||
1e58899b03 | |||
b27855523a | |||
34366b72fb | |||
5dfdfd49e9 | |||
33b49ebffd | |||
c01214e919 | |||
32d15a493c | |||
aa1f4d3ff8 | |||
1caf76b1b2 | |||
0dc7a4058e | |||
0e4e994431 | |||
eda6d6d02a | |||
fdad08e176 | |||
ba7ca0cd87 | |||
a3b3532ca4 | |||
7f09306dde | |||
74c583f11d | |||
72056e0cd2 | |||
ee6b66329b | |||
49e5975254 | |||
f40a8875c4 | |||
4caa980bf8 | |||
00c3a4320b | |||
0b21d98c9b | |||
1f14f2fc16 | |||
6cc18be68d | |||
e725f4f99c | |||
705967d0fb | |||
350e9dd331 | |||
e490080347 | |||
ad842ee8a5 | |||
abf1fc2b2d | |||
fd30675124 | |||
894cd29511 | |||
b21ffac820 | |||
f828c375e6 | |||
accdb51c0b | |||
f2fd769767 | |||
e92bd2fd80 | |||
cce543e0b6 | |||
af6c35ed6b | |||
07d0b43d4c | |||
8be0e731a7 | |||
ec9c70ba68 | |||
aa1faa3ccb | |||
f42fbbdc56 | |||
0cfc9bcafd | |||
9b27e113b3 | |||
783171729f | |||
2ab559c7e5 | |||
e5f250260d | |||
6b42d787a7 | |||
1acceb941d | |||
6714f05b49 | |||
b0e60a340b | |||
7414d1f7a6 | |||
eead1560ee | |||
8934b85c92 |
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,4 +1,15 @@
|
|||||||
|
# User-specific configuration
|
||||||
config.yaml
|
config.yaml
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Client Development Artifacts
|
||||||
*/*_modules/
|
*/*_modules/
|
||||||
|
client/public
|
||||||
|
|
||||||
|
# Server Development Artifacts
|
||||||
.coverage
|
.coverage
|
||||||
.cache
|
.cache
|
||||||
|
server/**/lib/
|
||||||
|
server/**/bin/
|
||||||
|
server/**/pyvenv.cfg
|
||||||
|
__pycache__/
|
||||||
|
16
README.md
16
README.md
@ -8,9 +8,11 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
||||||
|
- Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||||
- Post comments
|
- Post comments
|
||||||
- Post notes / annotations, including arbitrary polygons
|
- Post notes / annotations, including arbitrary polygons
|
||||||
- Rich JSON REST API ([see documentation](https://github.com/rr-/szurubooru/blob/master/API.md))
|
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||||
|
- Token based authentication for clients
|
||||||
- Rich search system
|
- Rich search system
|
||||||
- Rich privilege system
|
- Rich privilege system
|
||||||
- Autocomplete in search and while editing tags
|
- Autocomplete in search and while editing tags
|
||||||
@ -24,14 +26,12 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
|||||||
- Browser configurable endless paging
|
- Browser configurable endless paging
|
||||||
- Browser configurable backdrop grid for transparent images
|
- Browser configurable backdrop grid for transparent images
|
||||||
|
|
||||||
## Requirements
|
## Installation
|
||||||
|
|
||||||
- Python 3.5
|
It is recommended that you use Docker for deployment.
|
||||||
- Postgres
|
[See installation instructions.](doc/INSTALL.md)
|
||||||
- FFmpeg
|
|
||||||
- node.js
|
|
||||||
|
|
||||||
[See installation instructions.](https://github.com/rr-/szurubooru/blob/master/INSTALL.md)
|
Users who wish to avoid using Docker may find the [old installation instructions](doc/LEGACY_INSTALL.md) helpful.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
@ -45,4 +45,4 @@ Post view:
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[GPLv3](https://github.com/rr-/szurubooru/blob/master/LICENSE.md).
|
[GPLv3](LICENSE.md).
|
||||||
|
@ -1 +1 @@
|
|||||||
{ "presets": ["es2015"] }
|
{ "presets": ["env"] }
|
||||||
|
4
client/.dockerignore
Normal file
4
client/.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/*
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
**/.gitignore
|
44
client/Dockerfile
Normal file
44
client/Dockerfile
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
FROM node:9 as builder
|
||||||
|
WORKDIR /opt/app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . ./
|
||||||
|
|
||||||
|
ARG BUILD_INFO="docker-latest"
|
||||||
|
ARG CLIENT_BUILD_ARGS=""
|
||||||
|
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
|
||||||
|
|
||||||
|
|
||||||
|
FROM scratch as approot
|
||||||
|
|
||||||
|
COPY docker-start.sh /
|
||||||
|
|
||||||
|
WORKDIR /etc/nginx
|
||||||
|
COPY nginx.conf.docker ./nginx.conf
|
||||||
|
|
||||||
|
WORKDIR /var/www
|
||||||
|
COPY --from=builder /opt/app/public/ .
|
||||||
|
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
RUN apk --no-cache add dumb-init
|
||||||
|
COPY --from=approot / /
|
||||||
|
|
||||||
|
CMD ["/docker-start.sh"]
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
ARG DOCKER_REPO
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG SOURCE_COMMIT
|
||||||
|
LABEL \
|
||||||
|
maintainer="" \
|
||||||
|
org.opencontainers.image.title="${DOCKER_REPO}" \
|
||||||
|
org.opencontainers.image.url="https://github.com/rr-/szurubooru" \
|
||||||
|
org.opencontainers.image.documentation="https://github.com/rr-/szurubooru/blob/${SOURCE_COMMIT}/doc/INSTALL.md" \
|
||||||
|
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||||
|
org.opencontainers.image.source="https://github.com/rr-/szurubooru" \
|
||||||
|
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
|
||||||
|
org.opencontainers.image.licenses="GPL-3.0"
|
347
client/build.js
Normal file → Executable file
347
client/build.js
Normal file → Executable file
@ -1,81 +1,79 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
const webapp_icons = [
|
||||||
|
{name: 'android-chrome-192x192.png', size: 192},
|
||||||
|
{name: 'android-chrome-512x512.png', size: 512},
|
||||||
|
{name: 'apple-touch-icon.png', size: 180},
|
||||||
|
{name: 'mstile-150x150.png', size: 150}
|
||||||
|
];
|
||||||
|
|
||||||
|
const webapp_splash_screens = [
|
||||||
|
{w: 640, h: 1136, center: 320},
|
||||||
|
{w: 750, h: 1294, center: 375},
|
||||||
|
{w: 1125, h: 2436, center: 565},
|
||||||
|
{w: 1242, h: 2148, center: 625},
|
||||||
|
{w: 1536, h: 2048, center: 770},
|
||||||
|
{w: 1668, h: 2224, center: 820},
|
||||||
|
{w: 2048, h: 2732, center: 1024}
|
||||||
|
];
|
||||||
|
|
||||||
|
const external_js = [
|
||||||
|
'underscore',
|
||||||
|
'superagent',
|
||||||
|
'mousetrap',
|
||||||
|
'js-cookie',
|
||||||
|
'nprogress',
|
||||||
|
];
|
||||||
|
|
||||||
|
const app_manifest = {
|
||||||
|
name: 'szurubooru',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: baseUrl() + 'img/android-chrome-192x192.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '192x192'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: baseUrl() + 'img/android-chrome-512x512.png',
|
||||||
|
type: 'image/png',
|
||||||
|
sizes: '512x512'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
start_url: baseUrl(),
|
||||||
|
theme_color: '#24aadd',
|
||||||
|
background_color: '#ffffff',
|
||||||
|
display: 'standalone'
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const glob = require('glob');
|
const glob = require('glob');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const execSync = require('child_process').execSync;
|
const execSync = require('child_process').execSync;
|
||||||
const camelcase = require('camelcase');
|
|
||||||
|
|
||||||
function convertKeysToCamelCase(input) {
|
|
||||||
let result = {};
|
|
||||||
Object.keys(input).map((key, _) => {
|
|
||||||
const value = input[key];
|
|
||||||
if (value !== null && value.constructor == Object) {
|
|
||||||
result[camelcase(key)] = convertKeysToCamelCase(value);
|
|
||||||
} else {
|
|
||||||
result[camelcase(key)] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readTextFile(path) {
|
function readTextFile(path) {
|
||||||
return fs.readFileSync(path, 'utf-8');
|
return fs.readFileSync(path, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFile(path, content) {
|
function gzipFile(file) {
|
||||||
return fs.writeFileSync(path, content);
|
file = path.normalize(file);
|
||||||
|
execSync('gzip -6 -k ' + file);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersion() {
|
function baseUrl() {
|
||||||
return execSync('git describe --always --dirty --long --tags')
|
return process.env.BASE_URL ? process.env.BASE_URL : '/';
|
||||||
.toString()
|
|
||||||
.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getConfig() {
|
// -------------------------------------------------
|
||||||
const yaml = require('js-yaml');
|
|
||||||
const merge = require('merge');
|
|
||||||
const camelcaseKeys = require('camelcase-keys');
|
|
||||||
|
|
||||||
function parseConfigFile(path) {
|
function bundleHtml() {
|
||||||
let result = yaml.load(readTextFile(path, 'utf-8'));
|
const underscore = require('underscore');
|
||||||
return convertKeysToCamelCase(result);
|
const babelify = require('babelify');
|
||||||
}
|
|
||||||
|
|
||||||
let config = parseConfigFile('../config.yaml.dist');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const localConfig = parseConfigFile('../config.yaml');
|
|
||||||
config = merge.recursive(config, localConfig);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Local config does not exist, ignoring');
|
|
||||||
}
|
|
||||||
|
|
||||||
config.canSendMails = !!config.smtp.host;
|
|
||||||
delete config.secret;
|
|
||||||
delete config.smtp;
|
|
||||||
delete config.database;
|
|
||||||
config.meta = {
|
|
||||||
version: getVersion(),
|
|
||||||
buildDate: new Date().toUTCString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyFile(source, target) {
|
|
||||||
fs.createReadStream(source).pipe(fs.createWriteStream(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
function minifyJs(path) {
|
|
||||||
return require('uglify-js').minify(path, {compress: {unused: false}}).code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function minifyCss(css) {
|
|
||||||
return require('csso').minify(css);
|
|
||||||
}
|
|
||||||
|
|
||||||
function minifyHtml(html) {
|
function minifyHtml(html) {
|
||||||
return require('html-minifier').minify(html, {
|
return require('html-minifier').minify(html, {
|
||||||
@ -85,24 +83,20 @@ function minifyHtml(html) {
|
|||||||
}).trim();
|
}).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleHtml(config) {
|
const baseHtml = readTextFile('./html/index.htm')
|
||||||
const underscore = require('underscore');
|
.replace('<!-- Base HTML Placeholder -->', `<base href="${baseUrl()}"/>`);
|
||||||
const babelify = require('babelify');
|
fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml));
|
||||||
const baseHtml = readTextFile('./html/index.htm', 'utf-8');
|
|
||||||
const finalHtml = baseHtml
|
|
||||||
.replace(
|
|
||||||
/(<title>)(.*)(<\/title>)/,
|
|
||||||
util.format('$1%s$3', config.name));
|
|
||||||
writeFile('./public/index.htm', minifyHtml(finalHtml));
|
|
||||||
|
|
||||||
glob('./html/**/*.tpl', {}, (er, files) => {
|
let compiledTemplateJs = [
|
||||||
let compiledTemplateJs = '\'use strict\'\n';
|
`'use strict';`,
|
||||||
compiledTemplateJs += 'let _ = require(\'underscore\');';
|
`let _ = require('underscore');`,
|
||||||
compiledTemplateJs += 'let templates = {};';
|
`let templates = {};`
|
||||||
for (const file of files) {
|
];
|
||||||
|
|
||||||
|
for (const file of glob.sync('./html/**/*.tpl')) {
|
||||||
const name = path.basename(file, '.tpl').replace(/_/g, '-');
|
const name = path.basename(file, '.tpl').replace(/_/g, '-');
|
||||||
const placeholders = [];
|
const placeholders = [];
|
||||||
let templateText = readTextFile(file, 'utf-8');
|
let templateText = readTextFile(file);
|
||||||
templateText = templateText.replace(
|
templateText = templateText.replace(
|
||||||
/<%.*?%>/ig,
|
/<%.*?%>/ig,
|
||||||
(match) => {
|
(match) => {
|
||||||
@ -117,111 +111,204 @@ function bundleHtml(config) {
|
|||||||
|
|
||||||
const functionText = underscore.template(
|
const functionText = underscore.template(
|
||||||
templateText, {variable: 'ctx'}).source;
|
templateText, {variable: 'ctx'}).source;
|
||||||
compiledTemplateJs += `templates['${name}'] = ${functionText};`;
|
|
||||||
|
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
|
||||||
}
|
}
|
||||||
compiledTemplateJs += 'module.exports = templates;';
|
compiledTemplateJs.push('module.exports = templates;');
|
||||||
writeFile('./js/.templates.autogen.js', compiledTemplateJs);
|
|
||||||
|
fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n'));
|
||||||
console.info('Bundled HTML');
|
console.info('Bundled HTML');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleCss() {
|
function bundleCss() {
|
||||||
const stylus = require('stylus');
|
const stylus = require('stylus');
|
||||||
glob('./css/**/*.styl', {}, (er, files) => {
|
|
||||||
let css = '';
|
|
||||||
for (const file of files) {
|
|
||||||
css += stylus.render(
|
|
||||||
readTextFile(file), {filename: file});
|
|
||||||
}
|
|
||||||
writeFile('./public/css/app.min.css', minifyCss(css));
|
|
||||||
|
|
||||||
copyFile(
|
function minifyCss(css) {
|
||||||
|
return require('csso').minify(css).css;
|
||||||
|
}
|
||||||
|
|
||||||
|
let css = '';
|
||||||
|
for (const file of glob.sync('./css/**/*.styl')) {
|
||||||
|
css += stylus.render(readTextFile(file), {filename: file});
|
||||||
|
}
|
||||||
|
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
|
||||||
|
if (process.argv.includes('--gzip')) {
|
||||||
|
gzipFile('./public/css/app.min.css');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(
|
||||||
'./node_modules/font-awesome/css/font-awesome.min.css',
|
'./node_modules/font-awesome/css/font-awesome.min.css',
|
||||||
'./public/css/vendor.min.css');
|
'./public/css/vendor.min.css');
|
||||||
|
if (process.argv.includes('--gzip')) {
|
||||||
|
gzipFile('./public/css/vendor.min.css');
|
||||||
|
}
|
||||||
|
|
||||||
console.info('Bundled CSS');
|
console.info('Bundled CSS');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleJs(config) {
|
function bundleJs() {
|
||||||
const browserify = require('browserify');
|
const browserify = require('browserify');
|
||||||
const external = [
|
|
||||||
'underscore',
|
|
||||||
'superagent',
|
|
||||||
'mousetrap',
|
|
||||||
'js-cookie',
|
|
||||||
'nprogress',
|
|
||||||
];
|
|
||||||
|
|
||||||
function writeJsBundle(b, path, message, compress) {
|
function minifyJs(path) {
|
||||||
|
return require('terser').minify(
|
||||||
|
fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeJsBundle(b, path, compress, callback) {
|
||||||
let outputFile = fs.createWriteStream(path);
|
let outputFile = fs.createWriteStream(path);
|
||||||
b.bundle().pipe(outputFile);
|
b.bundle().pipe(outputFile);
|
||||||
outputFile.on('finish', function() {
|
outputFile.on('finish', () => {
|
||||||
if (compress) {
|
if (compress) {
|
||||||
writeFile(path, minifyJs(path));
|
fs.writeFileSync(path, minifyJs(path));
|
||||||
}
|
}
|
||||||
console.info(message);
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
glob('./js/**/*.js', {}, (er, files) => {
|
|
||||||
if (!process.argv.includes('--no-vendor-js')) {
|
if (!process.argv.includes('--no-vendor-js')) {
|
||||||
let b = browserify();
|
let b = browserify();
|
||||||
for (let lib of external) {
|
for (let lib of external_js) {
|
||||||
b.require(lib);
|
b.require(lib);
|
||||||
}
|
}
|
||||||
if (config.transpile) {
|
if (!process.argv.includes('--no-transpile')) {
|
||||||
b.add(require.resolve('babel-polyfill'));
|
b.add(require.resolve('babel-polyfill'));
|
||||||
}
|
}
|
||||||
writeJsBundle(
|
const file = './public/js/vendor.min.js';
|
||||||
b, './public/js/vendor.min.js', 'Bundled vendor JS', true);
|
writeJsBundle(b, file, true, () => {
|
||||||
|
if (process.argv.includes('--gzip')) {
|
||||||
|
gzipFile(file);
|
||||||
|
}
|
||||||
|
console.info('Bundled vendor JS');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!process.argv.includes('--no-app-js')) {
|
if (!process.argv.includes('--no-app-js')) {
|
||||||
let outputFile = fs.createWriteStream('./public/js/app.min.js');
|
let b = browserify({debug: process.argv.includes('--debug')});
|
||||||
let b = browserify({debug: config.debug});
|
if (!process.argv.includes('--no-transpile')) {
|
||||||
if (config.transpile) {
|
|
||||||
b = b.transform('babelify');
|
b = b.transform('babelify');
|
||||||
}
|
}
|
||||||
writeJsBundle(
|
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
|
||||||
b.external(external).add(files),
|
const compress = !process.argv.includes('--debug');
|
||||||
'./public/js/app.min.js',
|
const file = './public/js/app.min.js';
|
||||||
'Bundled app JS',
|
writeJsBundle(b, file, compress, () => {
|
||||||
!config.debug);
|
if (process.argv.includes('--gzip')) {
|
||||||
|
gzipFile(file);
|
||||||
}
|
}
|
||||||
|
console.info('Bundled app JS');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bundleConfig(config) {
|
function bundleConfig() {
|
||||||
writeFile(
|
function getVersion() {
|
||||||
'./js/.config.autogen.json', JSON.stringify(config));
|
let build_info = process.env.BUILD_INFO;
|
||||||
glob('./node_modules/font-awesome/fonts/*.*', {}, (er, files) => {
|
if (!build_info) {
|
||||||
for (let file of files) {
|
try {
|
||||||
if (fs.lstatSync(file).isDirectory()) {
|
build_info = execSync('git describe --always --dirty --long --tags').toString();
|
||||||
continue;
|
} catch (e) {
|
||||||
|
console.warn('Cannot find build version');
|
||||||
|
build_info = 'unknown';
|
||||||
}
|
}
|
||||||
copyFile(file, path.join('./public/fonts/', path.basename(file)));
|
|
||||||
}
|
}
|
||||||
});
|
return build_info.trim();
|
||||||
|
}
|
||||||
|
const config = {
|
||||||
|
meta: {
|
||||||
|
version: getVersion(),
|
||||||
|
buildDate: new Date().toUTCString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
|
||||||
|
console.info('Generated config file');
|
||||||
}
|
}
|
||||||
|
|
||||||
function bundleBinaryAssets() {
|
function bundleBinaryAssets() {
|
||||||
glob('./img/*.png', {}, (er, files) => {
|
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
|
||||||
for (let file of files) {
|
fs.copyFileSync('./img/transparency_grid.png', './public/img/transparency_grid.png');
|
||||||
copyFile(file, path.join('./public/img/', path.basename(file)));
|
console.info('Copied images');
|
||||||
|
|
||||||
|
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
|
||||||
|
for (let file of glob.sync('./node_modules/font-awesome/fonts/*.*')) {
|
||||||
|
if (fs.lstatSync(file).isDirectory()) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
fs.copyFileSync(file, path.join('./public/fonts/', path.basename(file)));
|
||||||
|
}
|
||||||
|
if (process.argv.includes('--gzip')) {
|
||||||
|
for (let file of glob.sync('./public/fonts/*.*')) {
|
||||||
|
if (file.endsWith('woff2')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gzipFile(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.info('Copied fonts')
|
||||||
|
}
|
||||||
|
|
||||||
|
function bundleWebAppFiles() {
|
||||||
|
const Jimp = require('jimp');
|
||||||
|
|
||||||
|
fs.writeFileSync('./public/manifest.json', JSON.stringify(app_manifest));
|
||||||
|
console.info('Generated app manifest');
|
||||||
|
|
||||||
|
Promise.all(webapp_icons.map(icon => {
|
||||||
|
return Jimp.read('./img/app.png')
|
||||||
|
.then(file => {
|
||||||
|
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||||
|
.write(path.join('./public/img/', icon.name));
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.then(() => {
|
||||||
|
console.info('Generated webapp icons');
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(webapp_splash_screens.map(dim => {
|
||||||
|
return Jimp.read('./img/splash.png')
|
||||||
|
.then(file => {
|
||||||
|
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||||
|
.background(0xFFFFFFFF)
|
||||||
|
.contain(dim.w, dim.center,
|
||||||
|
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||||
|
.contain(dim.w, dim.h,
|
||||||
|
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||||
|
.write(path.join('./public/img/',
|
||||||
|
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
.then(() => {
|
||||||
|
console.info('Generated splash screens');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = getConfig();
|
function makeOutputDirs() {
|
||||||
bundleConfig(config);
|
const dirs = [
|
||||||
|
'./public',
|
||||||
|
'./public/css',
|
||||||
|
'./public/fonts',
|
||||||
|
'./public/img',
|
||||||
|
'./public/js'
|
||||||
|
];
|
||||||
|
for (let dir of dirs) {
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, 0o755);
|
||||||
|
console.info('Created directory: ' + dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------
|
||||||
|
|
||||||
|
makeOutputDirs();
|
||||||
|
bundleConfig();
|
||||||
bundleBinaryAssets();
|
bundleBinaryAssets();
|
||||||
|
bundleWebAppFiles();
|
||||||
if (!process.argv.includes('--no-html')) {
|
if (!process.argv.includes('--no-html')) {
|
||||||
bundleHtml(config);
|
bundleHtml();
|
||||||
}
|
}
|
||||||
if (!process.argv.includes('--no-css')) {
|
if (!process.argv.includes('--no-css')) {
|
||||||
bundleCss();
|
bundleCss();
|
||||||
}
|
}
|
||||||
if (!process.argv.includes('--no-js')) {
|
if (!process.argv.includes('--no-js')) {
|
||||||
bundleJs(config);
|
bundleJs();
|
||||||
}
|
}
|
||||||
|
@ -55,3 +55,5 @@ $hovered-first-note-point-color = red
|
|||||||
$safety-safe = #88D488
|
$safety-safe = #88D488
|
||||||
$safety-sketchy = #F3D75F
|
$safety-sketchy = #F3D75F
|
||||||
$safety-unsafe = #F3985F
|
$safety-unsafe = #F3985F
|
||||||
|
$scrollbar-thumb-color = $main-color
|
||||||
|
$scrollbar-bg-color = $input-enabled-background-color
|
||||||
|
@ -3,7 +3,6 @@ $comment-header-background-color = $top-navigation-color
|
|||||||
$comment-border-color = #DDD
|
$comment-border-color = #DDD
|
||||||
|
|
||||||
.comment-container
|
.comment-container
|
||||||
margin: 0 0 1em 0
|
|
||||||
padding: 0 0 0 60px
|
padding: 0 0 0 60px
|
||||||
|
|
||||||
.avatar
|
.avatar
|
||||||
@ -124,7 +123,7 @@ $comment-border-color = #DDD
|
|||||||
font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
|
font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
|
||||||
background: #fbfbfb
|
background: #fbfbfb
|
||||||
color: #111
|
color: #111
|
||||||
font-size: 12pt
|
font-size: 1em
|
||||||
line-height: 1
|
line-height: 1
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 4px
|
padding: 4px
|
||||||
|
@ -2,3 +2,8 @@
|
|||||||
list-style-type: none
|
list-style-type: none
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
|
>li
|
||||||
|
margin-bottom: 1em
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
|
@import colors
|
||||||
|
$comment-border-color = $top-navigation-color
|
||||||
|
|
||||||
.global-comment-list
|
.global-comment-list
|
||||||
text-align: left
|
text-align: left
|
||||||
|
|
||||||
&>ul
|
&>ul
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
margin: 1em 0
|
margin: 1em 0 0
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
@media (max-width: 700px)
|
|
||||||
&>li
|
&>li
|
||||||
margin-bottom: 5em
|
margin-top: 2em
|
||||||
padding: 1vw
|
padding-top: 2em
|
||||||
|
border-top: 3px solid $comment-border-color
|
||||||
|
&:first-child
|
||||||
|
margin-top: 0
|
||||||
|
padding-top: 0
|
||||||
|
border-top: none
|
||||||
|
|
||||||
|
@media (max-width: 700px)
|
||||||
.post-thumbnail
|
.post-thumbnail
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
.thumbnail
|
.thumbnail
|
||||||
@ -19,7 +28,6 @@
|
|||||||
@media (min-width: 700px)
|
@media (min-width: 700px)
|
||||||
&>li
|
&>li
|
||||||
padding-left: 13em
|
padding-left: 13em
|
||||||
margin-bottom: 2em
|
|
||||||
.post-thumbnail
|
.post-thumbnail
|
||||||
float: left
|
float: left
|
||||||
margin: 0 0 1em -13em
|
margin: 0 0 1em -13em
|
||||||
|
@ -17,6 +17,8 @@ form
|
|||||||
.input li:first-child
|
.input li:first-child
|
||||||
padding-top: 0
|
padding-top: 0
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
|
||||||
|
form:not(.horizontal)
|
||||||
.hint
|
.hint
|
||||||
margin-top: 0.2em
|
margin-top: 0.2em
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
@ -29,13 +31,22 @@ form.horizontal
|
|||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
.input, .buttons, ul
|
.input, .buttons, ul
|
||||||
display: inline-block
|
display: inline-block
|
||||||
vertical-align: middle
|
vertical-align: top
|
||||||
margin: 0
|
margin: 0
|
||||||
padding: 0
|
padding: 0
|
||||||
input
|
input
|
||||||
vertical-align: middle
|
vertical-align: top
|
||||||
.buttons
|
.buttons
|
||||||
margin-right: 0.5em
|
margin-right: 0.5em
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
display: block
|
||||||
|
.input, .buttons, ul
|
||||||
|
display: block
|
||||||
|
margin-top: 0.5em
|
||||||
|
&:first-child
|
||||||
|
margin-top: 0
|
||||||
|
.buttons
|
||||||
|
margin-right: 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -126,6 +137,38 @@ input[type=checkbox]:focus + .checkbox:before
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Date and time inputs
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type=date],
|
||||||
|
input[type=time]
|
||||||
|
vertical-align: top
|
||||||
|
font-family: 'Droid Sans', sans-serif
|
||||||
|
font-size: 100%
|
||||||
|
padding: 0.2em 0.3em
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 2px solid $input-enabled-border-color
|
||||||
|
background: $input-enabled-background-color
|
||||||
|
color: $input-enabled-text-color
|
||||||
|
box-shadow: none /* :-moz-submit-invalid on FF */
|
||||||
|
transition: border-color 0.1s linear, background-color 0.1s linear
|
||||||
|
|
||||||
|
&:disabled
|
||||||
|
border: 2px solid $input-disabled-border-color
|
||||||
|
background: $input-disabled-background-color
|
||||||
|
color: $input-disabled-text-color
|
||||||
|
|
||||||
|
&:focus
|
||||||
|
border-color: $main-color
|
||||||
|
|
||||||
|
&[readonly]
|
||||||
|
border: 2px solid $input-disabled-border-color
|
||||||
|
background: $input-disabled-background-color
|
||||||
|
color: $input-disabled-text-color
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Regular inputs
|
* Regular inputs
|
||||||
*/
|
*/
|
||||||
@ -170,13 +213,25 @@ input:disabled
|
|||||||
cursor: not-allowed
|
cursor: not-allowed
|
||||||
|
|
||||||
label.color
|
label.color
|
||||||
|
white-space: nowrap
|
||||||
position: relative
|
position: relative
|
||||||
|
display: flex
|
||||||
input[type=text]
|
input[type=text]
|
||||||
|
margin-right: 0.25em
|
||||||
|
width: auto
|
||||||
|
.preview
|
||||||
|
display: inline-block
|
||||||
text-align: center
|
text-align: center
|
||||||
pointer-events: none
|
padding: 0 0.5em
|
||||||
input[type=color]
|
border: 2px solid black
|
||||||
position: absolute
|
&:after
|
||||||
opacity: 0
|
content: 'A'
|
||||||
|
.background-preview
|
||||||
|
border-right: 0
|
||||||
|
color: transparent
|
||||||
|
.text-preview
|
||||||
|
border-left: 0
|
||||||
|
|
||||||
|
|
||||||
form.show-validation .input
|
form.show-validation .input
|
||||||
input:invalid
|
input:invalid
|
||||||
@ -199,10 +254,13 @@ input[type=submit]
|
|||||||
cursor: pointer
|
cursor: pointer
|
||||||
font-size: 100%
|
font-size: 100%
|
||||||
padding: 0.2em 0.7em
|
padding: 0.2em 0.7em
|
||||||
|
border-radius: 0
|
||||||
border: 2px solid $button-enabled-background-color
|
border: 2px solid $button-enabled-background-color
|
||||||
background: $button-enabled-background-color
|
background: $button-enabled-background-color
|
||||||
color: $button-enabled-text-color
|
color: $button-enabled-text-color
|
||||||
outline: 0 /* something on Chrome */
|
outline: 0 /* something on Chrome */
|
||||||
|
-moz-appearance: none
|
||||||
|
-webkit-appearance: none
|
||||||
|
|
||||||
&:disabled
|
&:disabled
|
||||||
cursor: default
|
cursor: default
|
||||||
@ -231,25 +289,26 @@ input::-moz-focus-inner
|
|||||||
* File dropper
|
* File dropper
|
||||||
*/
|
*/
|
||||||
.file-dropper-holder
|
.file-dropper-holder
|
||||||
display: flex
|
|
||||||
flex-wrap: wrap
|
|
||||||
.file-dropper
|
.file-dropper
|
||||||
display: block
|
display: block
|
||||||
width: 100%
|
|
||||||
background: $window-color
|
background: $window-color
|
||||||
border: 3px dashed #eee
|
border: 3px dashed #eee
|
||||||
padding: 0.3em 0.5em
|
padding: 0.3em 0.5em
|
||||||
line-height: 140%
|
line-height: 140%
|
||||||
text-align: center
|
text-align: center
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
|
overflow: hidden
|
||||||
word-wrap: break-word
|
word-wrap: break-word
|
||||||
input
|
.url-holder
|
||||||
|
display: flex
|
||||||
margin-top: 0.5em
|
margin-top: 0.5em
|
||||||
width: auto
|
input, button
|
||||||
|
min-width: 0 /* firefox being sassy */
|
||||||
|
width: auto !important /* don't inherit anything weird */
|
||||||
|
input
|
||||||
flex: 1
|
flex: 1
|
||||||
button
|
button
|
||||||
margin-top: 0.5em
|
margin-left: 0.5em
|
||||||
width: 8em
|
|
||||||
|
|
||||||
input[type=file]:disabled+.file-dropper
|
input[type=file]:disabled+.file-dropper
|
||||||
cursor: default
|
cursor: default
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Open Sans'), local('OpenSans'), url(/fonts/open_sans.woff2) format('woff2');
|
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
|
|
||||||
/* make <body> cover entire viewport */
|
/* make <body> cover entire viewport */
|
||||||
@ -21,19 +21,28 @@ body
|
|||||||
margin: 0
|
margin: 0
|
||||||
color: $text-color
|
color: $text-color
|
||||||
font-family: 'Open Sans', sans-serif
|
font-family: 'Open Sans', sans-serif
|
||||||
font-size: 12pt
|
font-size: 1em
|
||||||
line-height: 18pt
|
line-height: 1.4
|
||||||
@media (max-width: 800px)
|
@media (max-width: 800px)
|
||||||
font-size: 10pt
|
font-size: 0.875em
|
||||||
line-height: 15pt
|
|
||||||
@media (max-width: 1200px)
|
@media (max-width: 1200px)
|
||||||
font-size: 11pt
|
font-size: 0.95em
|
||||||
line-height: 16.5pt
|
|
||||||
|
|
||||||
h1, h2, h3
|
h1, h2, h3
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-size: 2em
|
||||||
|
|
||||||
|
h2
|
||||||
|
font-size: 1.5em
|
||||||
|
|
||||||
|
p,
|
||||||
|
ol,
|
||||||
|
ul
|
||||||
|
margin: 1em 0
|
||||||
|
|
||||||
th
|
th
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
|
|
||||||
@ -61,8 +70,10 @@ form .fa-question-circle-o
|
|||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
|
|
||||||
#content-holder
|
#content-holder
|
||||||
padding: 1.5vw
|
padding: 1.5em
|
||||||
text-align: center
|
text-align: center
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
padding: 1em
|
||||||
>.content-wrapper
|
>.content-wrapper
|
||||||
box-sizing: border-box /* make max-width: 100% on this element include padding */
|
box-sizing: border-box /* make max-width: 100% on this element include padding */
|
||||||
text-align: left
|
text-align: left
|
||||||
@ -70,9 +81,26 @@ form .fa-question-circle-o
|
|||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
>*:first-child, form h1
|
>*:first-child, form h1
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
nav.buttons
|
||||||
|
ul
|
||||||
|
display: block
|
||||||
|
max-width: 100%
|
||||||
|
white-space: nowrap
|
||||||
|
overflow-x: auto
|
||||||
|
&::-webkit-scrollbar
|
||||||
|
height: 6px
|
||||||
|
background-color: $scrollbar-bg-color
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background-color: $scrollbar-thumb-color
|
||||||
>.content-wrapper:not(.transparent)
|
>.content-wrapper:not(.transparent)
|
||||||
background: $top-navigation-color
|
background: $top-navigation-color
|
||||||
padding: 2vw
|
padding: 1.8em
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
padding: 1.5em
|
||||||
|
.content,
|
||||||
|
.content .subcontent
|
||||||
|
>*:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
hr
|
hr
|
||||||
border: 0
|
border: 0
|
||||||
@ -125,6 +153,39 @@ nav
|
|||||||
li
|
li
|
||||||
display: inline-block
|
display: inline-block
|
||||||
float: left
|
float: left
|
||||||
|
a
|
||||||
|
padding: 0 1.5em
|
||||||
|
#mobile-navigation-toggle
|
||||||
|
display: none
|
||||||
|
width: 100%
|
||||||
|
padding: 0 1em
|
||||||
|
line-height: 2.3em
|
||||||
|
font-family: inherit
|
||||||
|
border: none
|
||||||
|
background: none
|
||||||
|
color: $active-tab-text-color
|
||||||
|
.site-name
|
||||||
|
display: block
|
||||||
|
float: left
|
||||||
|
max-width: 50vw
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
.toggle-icon
|
||||||
|
display: block
|
||||||
|
float: right
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
text-align: left
|
||||||
|
li
|
||||||
|
display: none
|
||||||
|
float: none
|
||||||
|
a
|
||||||
|
display: block
|
||||||
|
padding: 0 1em
|
||||||
|
#mobile-navigation-toggle
|
||||||
|
display: block
|
||||||
|
&.opened
|
||||||
|
li
|
||||||
|
display: block
|
||||||
ul li[data-name=account],
|
ul li[data-name=account],
|
||||||
ul li[data-name=register],
|
ul li[data-name=register],
|
||||||
ul li[data-name=login],
|
ul li[data-name=login],
|
||||||
@ -141,6 +202,8 @@ nav
|
|||||||
margin-right: 0.6em
|
margin-right: 0.6em
|
||||||
margin-left: calc(0.6em - 1.2em)
|
margin-left: calc(0.6em - 1.2em)
|
||||||
float: left
|
float: left
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
display: none
|
||||||
|
|
||||||
a .access-key
|
a .access-key
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
@ -176,7 +239,7 @@ a .access-key
|
|||||||
width: 20px
|
width: 20px
|
||||||
height: 20px
|
height: 20px
|
||||||
&.empty
|
&.empty
|
||||||
background-image: url('/img/transparency_grid.png')
|
background-image: url('../img/transparency_grid.png')
|
||||||
background-repeat: repeat
|
background-repeat: repeat
|
||||||
background-size: initial
|
background-size: initial
|
||||||
img
|
img
|
||||||
@ -194,6 +257,14 @@ a .access-key
|
|||||||
margin-top: 0 !important
|
margin-top: 0 !important
|
||||||
margin-bottom: 0 !important
|
margin-bottom: 0 !important
|
||||||
|
|
||||||
|
.table-wrap
|
||||||
|
overflow-x: auto
|
||||||
|
&::-webkit-scrollbar
|
||||||
|
height: 6px
|
||||||
|
background-color: $scrollbar-bg-color
|
||||||
|
&::-webkit-scrollbar-thumb
|
||||||
|
background-color: $scrollbar-thumb-color
|
||||||
|
|
||||||
/* hack to prevent text from being copied */
|
/* hack to prevent text from being copied */
|
||||||
[data-pseudo-content]:before {
|
[data-pseudo-content]:before {
|
||||||
content: attr(data-pseudo-content)
|
content: attr(data-pseudo-content)
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
color: mix($text-color, $inactive-link-color)
|
color: mix($text-color, $inactive-link-color)
|
||||||
font-size: 120%
|
font-size: 120%
|
||||||
i
|
i
|
||||||
font-size: 12pt
|
font-size: 1em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
float: right
|
float: right
|
||||||
line-height: 2em
|
line-height: 2em
|
||||||
|
@ -16,6 +16,10 @@
|
|||||||
font-size: 1.6em
|
font-size: 1.6em
|
||||||
&:first-child
|
&:first-child
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
margin-top: 1.5em
|
||||||
|
&:first-child
|
||||||
|
margin-top: 0
|
||||||
nav
|
nav
|
||||||
ul
|
ul
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
|
@ -6,13 +6,16 @@
|
|||||||
margin-bottom: 1em
|
margin-bottom: 1em
|
||||||
h1
|
h1
|
||||||
line-height: initial
|
line-height: initial
|
||||||
font-size: 30pt
|
font-size: 2.5em
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
|
.messages
|
||||||
|
text-align: center
|
||||||
.message
|
.message
|
||||||
margin-bottom: 2em
|
margin: 0 auto 2em auto
|
||||||
|
|
||||||
form
|
form
|
||||||
|
display: inline-block
|
||||||
width: auto
|
width: auto
|
||||||
vertical-align: middle
|
vertical-align: middle
|
||||||
margin: 0 0 2em 0
|
margin: 0 0 2em 0
|
||||||
@ -31,6 +34,8 @@
|
|||||||
display: flex
|
display: flex
|
||||||
align-items: center
|
align-items: center
|
||||||
justify-content: center
|
justify-content: center
|
||||||
|
&:empty
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
nav
|
nav
|
||||||
a
|
a
|
||||||
@ -50,6 +55,8 @@
|
|||||||
li
|
li
|
||||||
display: inline
|
display: inline
|
||||||
white-space: nowrap
|
white-space: nowrap
|
||||||
|
@media (max-width: 800px)
|
||||||
|
display: block
|
||||||
.sep
|
.sep
|
||||||
word-spacing: 1.1em
|
word-spacing: 1.1em
|
||||||
background-repeat: no-repeat
|
background-repeat: no-repeat
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
.page
|
.page
|
||||||
position: relative
|
position: relative
|
||||||
.page-header
|
.page-header
|
||||||
margin: 0.5em 0.5em 0.5em 0
|
margin: 0.5em 0
|
||||||
position: relative
|
position: relative
|
||||||
&:before
|
&:before
|
||||||
display: block
|
display: block
|
||||||
|
2
client/css/password-reset.styl
Normal file
2
client/css/password-reset.styl
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
#password-reset
|
||||||
|
max-width: 30em
|
@ -1,6 +1,6 @@
|
|||||||
.post-container
|
.post-container
|
||||||
.post-content.transparency-grid img
|
.post-content.transparency-grid img
|
||||||
background: url('/img/transparency_grid.png')
|
background: url('../img/transparency_grid.png')
|
||||||
|
|
||||||
text-align: center
|
text-align: center
|
||||||
.post-content
|
.post-content
|
||||||
@ -16,3 +16,6 @@
|
|||||||
bottom: 0
|
bottom: 0
|
||||||
width: 100%
|
width: 100%
|
||||||
height: 100%
|
height: 100%
|
||||||
|
|
||||||
|
img
|
||||||
|
image-orientation: from-image
|
||||||
|
@ -54,10 +54,12 @@
|
|||||||
.icon:not(:first-of-type)
|
.icon:not(:first-of-type)
|
||||||
margin-left: 1em
|
margin-left: 1em
|
||||||
|
|
||||||
.masstag
|
.edit-overlay
|
||||||
position: absolute
|
position: absolute
|
||||||
top: 0.5em
|
top: 0.5em
|
||||||
left: 0.5em
|
left: 0.5em
|
||||||
|
|
||||||
|
.tag-flipper
|
||||||
display: inline-block
|
display: inline-block
|
||||||
padding: 0.5em
|
padding: 0.5em
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
@ -68,7 +70,7 @@
|
|||||||
height: 1em
|
height: 1em
|
||||||
text-align: center
|
text-align: center
|
||||||
line-height: 1em
|
line-height: 1em
|
||||||
font-size: 20pt
|
font-size: 1.6em
|
||||||
&.tagged
|
&.tagged
|
||||||
background: rgba(0, 230, 0, 0.7)
|
background: rgba(0, 230, 0, 0.7)
|
||||||
&:after
|
&:after
|
||||||
@ -82,6 +84,37 @@
|
|||||||
&[data-disabled]
|
&[data-disabled]
|
||||||
background: rgba(200, 200, 200, 0.7)
|
background: rgba(200, 200, 200, 0.7)
|
||||||
|
|
||||||
|
.safety-flipper a
|
||||||
|
display: inline-block
|
||||||
|
margin: 0.1em
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 0
|
||||||
|
display: inline-block
|
||||||
|
width: 1.2em
|
||||||
|
height: 1.2em
|
||||||
|
text-align: center
|
||||||
|
line-height: 1em
|
||||||
|
font-size: 1.6em
|
||||||
|
border: 3px solid
|
||||||
|
&.safety-safe
|
||||||
|
background-color: darken($safety-safe, 5%)
|
||||||
|
border-color: @background-color
|
||||||
|
&:not(.active)
|
||||||
|
background-color: alpha(@background-color, 0.3)
|
||||||
|
&.safety-sketchy
|
||||||
|
background-color: $safety-sketchy
|
||||||
|
border-color: @background-color
|
||||||
|
&:not(.active)
|
||||||
|
background-color: alpha(@background-color, 0.3)
|
||||||
|
&.safety-unsafe
|
||||||
|
background-color: $safety-unsafe
|
||||||
|
border-color: @background-color
|
||||||
|
&:not(.active)
|
||||||
|
background-color: alpha(@background-color, 0.3)
|
||||||
|
&[data-disabled]
|
||||||
|
background: rgba(200, 200, 200, 0.7)
|
||||||
|
|
||||||
|
|
||||||
.thumbnail
|
.thumbnail
|
||||||
background-position: 50% 30%
|
background-position: 50% 30%
|
||||||
width: 100%
|
width: 100%
|
||||||
@ -112,29 +145,59 @@
|
|||||||
margin-bottom: 0.75em
|
margin-bottom: 0.75em
|
||||||
*
|
*
|
||||||
vertical-align: top
|
vertical-align: top
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
display: block
|
||||||
input
|
input
|
||||||
margin-bottom: 0.25em
|
margin-bottom: 0.25em
|
||||||
margin-right: 0.25em
|
margin-right: 0.25em
|
||||||
input[name=search-text]
|
input[name=search-text]
|
||||||
width: 25em
|
width: 25em
|
||||||
input[name=masstag]
|
@media (max-width: 1000px)
|
||||||
width: 12em
|
display: block
|
||||||
.masstag-hint, .open-masstag
|
width: 100%
|
||||||
margin-right: 1em
|
margin-bottom: 0.5em
|
||||||
.append
|
.append
|
||||||
|
vertical-align: middle
|
||||||
font-size: 0.95em
|
font-size: 0.95em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
.masstag
|
.bulk-edit
|
||||||
&:not(.active)
|
&:not(.opened)
|
||||||
|
.close
|
||||||
|
display: none
|
||||||
|
&.opened
|
||||||
|
.open
|
||||||
|
display: none
|
||||||
|
&.hidden
|
||||||
|
display: none
|
||||||
|
.bulk-edit-tags
|
||||||
|
&.opened
|
||||||
|
.hint
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
display: block
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
&:not(.opened)
|
||||||
[type=text],
|
[type=text],
|
||||||
.start-tagging,
|
.start
|
||||||
.stop-tagging
|
|
||||||
display: none
|
display: none
|
||||||
.masstag-hint
|
.hint
|
||||||
display: none
|
|
||||||
&.active
|
|
||||||
.open-masstag
|
|
||||||
display: none
|
display: none
|
||||||
|
input[name=tag]
|
||||||
|
width: 12em
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
display: block
|
||||||
|
width: 100%
|
||||||
|
margin-bottom: 0.5em
|
||||||
|
.append
|
||||||
|
&.open,
|
||||||
|
&.hint
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
margin-left: 0
|
||||||
|
.hint
|
||||||
|
margin-right: 1em
|
||||||
|
.bulk-edit-safety
|
||||||
|
.append
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
margin-left: 0
|
||||||
|
|
||||||
.safety
|
.safety
|
||||||
margin-right: 0.25em
|
margin-right: 0.25em
|
||||||
|
@ -33,6 +33,8 @@
|
|||||||
i
|
i
|
||||||
font-size: 140%
|
font-size: 140%
|
||||||
text-align: center
|
text-align: center
|
||||||
|
@media (max-width: 800px)
|
||||||
|
margin-top: 2em
|
||||||
|
|
||||||
>.content
|
>.content
|
||||||
width: 100%
|
width: 100%
|
||||||
@ -50,6 +52,7 @@
|
|||||||
order: 2
|
order: 2
|
||||||
min-width: 100%
|
min-width: 100%
|
||||||
max-width: 0
|
max-width: 0
|
||||||
|
margin-right: 0
|
||||||
>.content
|
>.content
|
||||||
order: 1
|
order: 1
|
||||||
|
|
||||||
@ -130,10 +133,24 @@
|
|||||||
display: inline-block
|
display: inline-block
|
||||||
|
|
||||||
.management
|
.management
|
||||||
|
ul
|
||||||
|
list-style-type: none
|
||||||
|
margin: 0
|
||||||
|
padding: 0
|
||||||
li
|
li
|
||||||
margin: 0
|
margin: 0
|
||||||
|
padding: 0
|
||||||
|
|
||||||
label
|
.post-source
|
||||||
|
textarea
|
||||||
|
white-space: pre
|
||||||
|
overflow-wrap: normal
|
||||||
|
overflow-x: scroll
|
||||||
|
|
||||||
|
form
|
||||||
|
width: auto
|
||||||
|
|
||||||
|
label:not(.file-dropper)
|
||||||
margin-bottom: 0.3em
|
margin-bottom: 0.3em
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ $cancel-button-color = tomato
|
|||||||
.file-dropper
|
.file-dropper
|
||||||
font-size: 150%
|
font-size: 150%
|
||||||
padding: 2em
|
padding: 2em
|
||||||
|
small
|
||||||
|
font-size: 60%
|
||||||
|
|
||||||
input[type=submit]
|
input[type=submit]
|
||||||
margin-top: 1em
|
margin-top: 1em
|
||||||
|
@ -8,11 +8,16 @@ $snapshot-merged-background-color = #FEC
|
|||||||
|
|
||||||
ul
|
ul
|
||||||
margin: 0 auto
|
margin: 0 auto
|
||||||
|
padding: 0
|
||||||
width: 100%
|
width: 100%
|
||||||
max-width: 35em
|
max-width: 35em
|
||||||
list-style-type: none
|
list-style-type: none
|
||||||
|
|
||||||
li
|
li
|
||||||
|
margin-bottom: 1em
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
|
||||||
.time
|
.time
|
||||||
float: right
|
float: right
|
||||||
|
|
||||||
@ -39,6 +44,3 @@ $snapshot-merged-background-color = #FEC
|
|||||||
background: $snapshot-merged-background-color
|
background: $snapshot-merged-background-color
|
||||||
&+.details
|
&+.details
|
||||||
background: lighten($snapshot-merged-background-color, 50%)
|
background: lighten($snapshot-merged-background-color, 50%)
|
||||||
|
|
||||||
div.details
|
|
||||||
margin-bottom: 2em
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
.content-wrapper.tag-categories
|
.content-wrapper.tag-categories
|
||||||
width: 100%
|
width: 100%
|
||||||
max-width: 40em
|
max-width: 45em
|
||||||
table
|
table
|
||||||
border-spacing: 0
|
border-spacing: 0
|
||||||
width: 100%
|
width: 100%
|
||||||
@ -11,11 +11,18 @@
|
|||||||
td, th
|
td, th
|
||||||
padding: .4em
|
padding: .4em
|
||||||
&.color
|
&.color
|
||||||
text-align: center
|
input[type=text]
|
||||||
|
width: 8em
|
||||||
&.usages
|
&.usages
|
||||||
text-align: center
|
text-align: center
|
||||||
&.remove, &.set-default
|
&.remove, &.set-default
|
||||||
white-space: pre
|
white-space: pre
|
||||||
|
th
|
||||||
|
white-space: nowrap
|
||||||
|
&:first-child
|
||||||
|
padding-left: 0
|
||||||
|
&:last-child
|
||||||
|
padding-right: 0
|
||||||
tfoot
|
tfoot
|
||||||
display: none
|
display: none
|
||||||
form
|
form
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
th, td
|
th, td
|
||||||
padding: 0.1em 0.5em
|
padding: 0.1em 0.5em
|
||||||
th
|
th
|
||||||
|
white-space: nowrap
|
||||||
background: $top-navigation-color
|
background: $top-navigation-color
|
||||||
.names
|
.names
|
||||||
width: 28%
|
width: 28%
|
||||||
@ -46,7 +47,10 @@
|
|||||||
form
|
form
|
||||||
width: auto
|
width: auto
|
||||||
input[name=search-text]
|
input[name=search-text]
|
||||||
max-width: 15em
|
width: 25em
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
width: 100%
|
||||||
.append
|
.append
|
||||||
|
vertical-align: middle
|
||||||
font-size: 0.95em
|
font-size: 0.95em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
|
@ -33,7 +33,10 @@
|
|||||||
form
|
form
|
||||||
width: auto
|
width: auto
|
||||||
input[name=search-text]
|
input[name=search-text]
|
||||||
max-width: 15em
|
width: 25em
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
width: 100%
|
||||||
.append
|
.append
|
||||||
|
vertical-align: middle
|
||||||
font-size: 0.95em
|
font-size: 0.95em
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
@import colors
|
||||||
|
$token-border-color = $active-tab-background-color
|
||||||
|
|
||||||
#user
|
#user
|
||||||
width: 100%
|
width: 100%
|
||||||
max-width: 35em
|
max-width: 35em
|
||||||
@ -37,7 +40,43 @@
|
|||||||
height: 1px
|
height: 1px
|
||||||
clear: both
|
clear: both
|
||||||
|
|
||||||
#user-delete form
|
#user-tokens
|
||||||
|
|
||||||
|
.token-flex-container
|
||||||
|
width: 100%
|
||||||
|
display: flex;
|
||||||
|
flex-direction column;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
|
||||||
|
.full-width
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
|
.token-flex-row
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.2em;
|
||||||
|
|
||||||
|
.no-wrap
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.token-input
|
||||||
|
min-height: 2em;
|
||||||
|
line-height: 2em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.token-flex-column
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.token-flex-labels
|
||||||
|
padding-right: 0.5em
|
||||||
|
|
||||||
|
hr
|
||||||
|
border-top: 3px solid $token-border-color
|
||||||
|
|
||||||
|
form
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
#user-delete form
|
||||||
|
width: 100%
|
||||||
|
11
client/docker-start.sh
Executable file
11
client/docker-start.sh
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/dumb-init /bin/sh
|
||||||
|
|
||||||
|
# Integrate environment variables
|
||||||
|
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||||
|
/etc/nginx/nginx.conf
|
||||||
|
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
|
||||||
|
/var/www/index.htm \
|
||||||
|
/var/www/manifest.json
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
exec nginx
|
16
client/hooks/build
Executable file
16
client/hooks/build
Executable file
@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
CLOSEST_VER=$(git describe --tags --abbrev=0 ${SOURCE_COMMIT})
|
||||||
|
if git describe --exact-match --abbrev=0 ${SOURCE_COMMIT} 2> /dev/null; then
|
||||||
|
BUILD_INFO="v${CLOSEST_VER}"
|
||||||
|
else
|
||||||
|
BUILD_INFO="v${CLOSEST_VER}-edge-$(git rev-parse --short ${SOURCE_COMMIT})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using BUILD_INFO=${BUILD_INFO}"
|
||||||
|
docker build \
|
||||||
|
--build-arg BUILD_INFO=${BUILD_INFO} \
|
||||||
|
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||||
|
--build-arg SOURCE_COMMIT \
|
||||||
|
--build-arg DOCKER_REPO \
|
||||||
|
-f $DOCKERFILE_PATH -t $IMAGE_NAME .
|
19
client/hooks/post_push
Executable file
19
client/hooks/post_push
Executable file
@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
add_tag() {
|
||||||
|
echo "Also tagging image as ${DOCKER_REPO}:${1}"
|
||||||
|
docker tag $IMAGE_NAME $DOCKER_REPO:$1
|
||||||
|
docker push $DOCKER_REPO:$1
|
||||||
|
}
|
||||||
|
|
||||||
|
CLOSEST_VER=$(git describe --tags --abbrev=0)
|
||||||
|
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
|
||||||
|
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
|
||||||
|
|
||||||
|
add_tag "${CLOSEST_MAJOR_VER}-edge"
|
||||||
|
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
|
||||||
|
|
||||||
|
if git describe --exact-match --abbrev=0 2> /dev/null; then
|
||||||
|
add_tag "${CLOSEST_MAJOR_VER}"
|
||||||
|
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
|
||||||
|
fi
|
@ -1,7 +1,7 @@
|
|||||||
<div class='comment-container'>
|
<div class='comment-container'>
|
||||||
<div class='avatar'>
|
<div class='avatar'>
|
||||||
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
|
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
|
||||||
<a href='/user/<%- encodeURIComponent(ctx.user.name) %>'>
|
<a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %>
|
<%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
<nav class='readonly'><%
|
<nav class='readonly'><%
|
||||||
%><strong><span class='nickname'><%
|
%><strong><span class='nickname'><%
|
||||||
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
|
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
|
||||||
%><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'><%
|
%><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'><%
|
||||||
%><% } %><%
|
%><% } %><%
|
||||||
|
|
||||||
%><%- ctx.user ? ctx.user.name : 'Deleted user' %><%
|
%><%- ctx.user ? ctx.user.name : 'Deleted user' %><%
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div class='global-comment-list'>
|
<div class='global-comment-list'>
|
||||||
<ul><!--
|
<ul><!--
|
||||||
--><% for (let post of ctx.results) { %><!--
|
--><% for (let post of ctx.response.results) { %><!--
|
||||||
--><li><!--
|
--><li><!--
|
||||||
--><div class='post-thumbnail'><!--
|
--><div class='post-thumbnail'><!--
|
||||||
--><% if (ctx.canViewPosts) { %><!--
|
--><% if (ctx.canViewPosts) { %><!--
|
||||||
--><a href='/post/<%- encodeURIComponent(post.id) %>'><!--
|
--><a href='<%- ctx.formatClientLink('post', post.id) %>'><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
|
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
|
||||||
--><% if (ctx.canViewPosts) { %><!--
|
--><% if (ctx.canViewPosts) { %><!--
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<div class='pager'>
|
<div class='pager'>
|
||||||
<div class='page-header-holder'></div>
|
<div class='page-header-holder'></div>
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
|
<div class='page-guard top'></div>
|
||||||
<div class='pages-holder'></div>
|
<div class='pages-holder'></div>
|
||||||
|
<div class='page-guard bottom'></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,9 +8,19 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
<br/>
|
<br/>
|
||||||
Or just click on this box.
|
Or just click on this box.
|
||||||
|
<% if (ctx.extraText) { %>
|
||||||
|
<br/>
|
||||||
|
<small><%= ctx.extraText %></small>
|
||||||
|
<% } %>
|
||||||
</label>
|
</label>
|
||||||
<% if (ctx.allowUrls) { %>
|
<% if (ctx.allowUrls) { %>
|
||||||
<input type='text' name='url' placeholder='Alternatively, paste an URL here.'/>
|
<div class='url-holder'>
|
||||||
|
<input type='text' name='url' placeholder='<%- ctx.urlPlaceholder %>'/>
|
||||||
|
<% if (ctx.lock) { %>
|
||||||
|
<button>Confirm</button>
|
||||||
|
<% } else { %>
|
||||||
<button>Add URL</button>
|
<button>Add URL</button>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<div class='content-wrapper' id='help'>
|
<div class='content-wrapper' id='help'>
|
||||||
<nav class='buttons primary'><!--
|
<nav class='buttons primary'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
--><li data-name='about'><a href='/help/about'>About</a></li><!--
|
--><li data-name='about'><a href='<%- ctx.formatClientLink('help', 'about') %>'>About</a></li><!--
|
||||||
--><li data-name='keyboard'><a href='/help/keyboard'>Keyboard</a></li><!--
|
--><li data-name='keyboard'><a href='<%- ctx.formatClientLink('help', 'keyboard') %>'>Keyboard</a></li><!--
|
||||||
--><li data-name='search'><a href='/help/search'>Search syntax</a></li><!--
|
--><li data-name='search'><a href='<%- ctx.formatClientLink('help', 'search') %>'>Search syntax</a></li><!--
|
||||||
--><li data-name='comments'><a href='/help/comments'>Comments</a></li><!--
|
--><li data-name='comments'><a href='<%- ctx.formatClientLink('help', 'comments') %>'>Comments</a></li><!--
|
||||||
--><li data-name='tos'><a href='/help/tos'>Terms of service</a></li><!--
|
--><li data-name='tos'><a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of service</a></li><!--
|
||||||
--></ul><!--
|
--></ul><!--
|
||||||
--></nav>
|
--></nav>
|
||||||
|
|
||||||
|
@ -33,10 +33,15 @@ shortcuts:</p>
|
|||||||
<td><kbd>P</kbd></td>
|
<td><kbd>P</kbd></td>
|
||||||
<td>Focus first post in post list</td>
|
<td>Focus first post in post list</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><kbd>Delete</kbd></td>
|
||||||
|
<td>Delete post (while in edit mode)</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p>Additionally, each item in top navigation can be accessed using feature
|
<p>Additionally, each item in the top navigation can be accessed using a
|
||||||
called “access keys”. Pressing underlined letter while holding
|
feature called “access keys”. Pressing the underlined letter while
|
||||||
Shfit or Alt+Shift (depending on your browser) will go to the desired page
|
holding Shift or Alt+Shift (depending on your browser) will go to the desired
|
||||||
(most browsers) or focus the link (IE).</p>
|
page (most browsers) or focus the link (IE).</p>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
<nav class='buttons secondary'><!--
|
<nav class='buttons secondary'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
--><li data-name='default'><a href='/help/search'>General</a></li><!--
|
--><li data-name='default'><a href='<%- ctx.formatClientLink('help', 'search') %>'>General</a></li><!--
|
||||||
--><li data-name='posts'><a href='/help/search/posts'>Posts</a></li><!--
|
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
|
||||||
--><li data-name='users'><a href='/help/search/users'>Users</a></li><!--
|
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
|
||||||
--><li data-name='tags'><a href='/help/search/tags'>Tags</a></li><!--
|
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
|
||||||
--></ul><!--
|
--></ul><!--
|
||||||
--></nav>
|
--></nav>
|
||||||
|
|
||||||
|
@ -80,6 +80,9 @@ take following form:</p>
|
|||||||
<code>,desc</code> to control the sort direction, which can be also controlled
|
<code>,desc</code> to control the sort direction, which can be also controlled
|
||||||
by negating the whole token.</p>
|
by negating the whole token.</p>
|
||||||
|
|
||||||
|
<p>You can escape special characters such as <code>:</code> and <code>-</code>
|
||||||
|
by prepending them with a backslash: <code>\\</code>.</p>
|
||||||
|
|
||||||
<h1>Example</h1>
|
<h1>Example</h1>
|
||||||
|
|
||||||
<p>Searching for posts with following query:</p>
|
<p>Searching for posts with following query:</p>
|
||||||
@ -89,3 +92,8 @@ by negating the whole token.</p>
|
|||||||
<p>will show flash files tagged as sea, that were liked by seven people at
|
<p>will show flash files tagged as sea, that were liked by seven people at
|
||||||
most, uploaded by user Pirate.</p>
|
most, uploaded by user Pirate.</p>
|
||||||
|
|
||||||
|
<p>Searching for posts with <code>re:zero</code> will show an error message
|
||||||
|
about unknown named token.</p>
|
||||||
|
|
||||||
|
<p>Searching for posts with <code>re\:zero</code> will show posts tagged with
|
||||||
|
<code>re:zero</code>.</p>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>tag</code></td>
|
<td><code>tag</code></td>
|
||||||
<td>having given tag</td>
|
<td>having given tag (accepts wildcards)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>score</code></td>
|
<td><code>score</code></td>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>uploader</code></td>
|
<td><code>uploader</code></td>
|
||||||
<td>uploaded by given user</td>
|
<td>uploaded by given use (accepts wildcards)r</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>upload</code></td>
|
<td><code>upload</code></td>
|
||||||
@ -32,11 +32,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>comment</code></td>
|
<td><code>comment</code></td>
|
||||||
<td>commented by given user</td>
|
<td>commented by given user (accepts wildcards)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>fav</code></td>
|
<td><code>fav</code></td>
|
||||||
<td>favorited by given user</td>
|
<td>favorited by given user (accepts wildcards)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>source</code></td>
|
||||||
|
<td>having given source URL (accepts wildcards)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>tag-count</code></td>
|
<td><code>tag-count</code></td>
|
||||||
@ -54,6 +58,10 @@
|
|||||||
<td><code>note-count</code></td>
|
<td><code>note-count</code></td>
|
||||||
<td>having given number of annotations</td>
|
<td>having given number of annotations</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>note-text</code></td>
|
||||||
|
<td>having given note text (accepts wildcards)</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>relation-count</code></td>
|
<td><code>relation-count</code></td>
|
||||||
<td>having given number of relations</td>
|
<td>having given number of relations</td>
|
||||||
@ -66,6 +74,10 @@
|
|||||||
<td><code>type</code></td>
|
<td><code>type</code></td>
|
||||||
<td>given type of posts. <code><value></code> can be either <code>image</code>, <code>animation</code> (or <code>animated</code> or <code>anim</code>), <code>flash</code> (or <code>swf</code>) or <code>video</code> (or <code>webm</code>).</td>
|
<td>given type of posts. <code><value></code> can be either <code>image</code>, <code>animation</code> (or <code>animated</code> or <code>anim</code>), <code>flash</code> (or <code>swf</code>) or <code>video</code> (or <code>webm</code>).</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>flag</code></td>
|
||||||
|
<td>having given flag. <code><value></code> can be either <code>loop</code> or <code>sound</code>.</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>content-checksum</code></td>
|
<td><code>content-checksum</code></td>
|
||||||
<td>having given SHA1 checksum</td>
|
<td>having given SHA1 checksum</td>
|
||||||
@ -86,6 +98,14 @@
|
|||||||
<td><code>image-area</code></td>
|
<td><code>image-area</code></td>
|
||||||
<td>having given number of pixels (image width * image height)</td>
|
<td>having given number of pixels (image width * image height)</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>image-aspect-ratio</code></td>
|
||||||
|
<td>having given aspect ratio (image width / image height)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>image-ar</code></td>
|
||||||
|
<td>alias of <code>image-aspect-ratio</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>width</code></td>
|
<td><code>width</code></td>
|
||||||
<td>alias of <code>image-width</code></td>
|
<td>alias of <code>image-width</code></td>
|
||||||
@ -98,6 +118,14 @@
|
|||||||
<td><code>area</code></td>
|
<td><code>area</code></td>
|
||||||
<td>alias of <code>image-area</code></td>
|
<td>alias of <code>image-area</code></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>aspect-ratio</code></td>
|
||||||
|
<td>alias of <code>image-aspect-ratio</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><code>ar</code></td>
|
||||||
|
<td>alias of <code>image-aspect-ratio</code></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>creation-date</code></td>
|
<td><code>creation-date</code></td>
|
||||||
<td>posted at given date</td>
|
<td>posted at given date</td>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>category</code></td>
|
<td><code>category</code></td>
|
||||||
<td>having given category</td>
|
<td>having given category (accepts wildcards)</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>creation-date</code></td>
|
<td><code>creation-date</code></td>
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %>
|
<%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %>
|
||||||
<input type='submit' value='Search'/>
|
<input type='submit' value='Search'/>
|
||||||
<span class=sep>or</span>
|
<span class=sep>or</span>
|
||||||
<a href='/posts'>browse all posts</a>
|
<a href='<%- ctx.formatClientLink('posts') %>'>browse all posts</a>
|
||||||
</form>
|
</form>
|
||||||
<% } %>
|
<% } %>
|
||||||
<div class='post-info-container'></div>
|
<div class='post-info-container'></div>
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
<li><%- ctx.postCount %> posts</li><span class='sep'>
|
<li><%- ctx.postCount %> posts</li><span class='sep'>
|
||||||
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
|
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
|
||||||
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
|
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
|
||||||
</span><% if (ctx.canListSnapshots) { %><li><a href='/history'>History</a></li><span class='sep'>
|
</span><% if (ctx.canListSnapshots) { %><li><a href='<%- ctx.formatClientLink('history') %>'>History</a></li><span class='sep'>
|
||||||
</span><% } %>
|
</span><% } %>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -2,16 +2,31 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'/>
|
<meta charset='utf-8'/>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
|
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
||||||
<title><!-- configured in the config file --></title>
|
<meta name='theme-color' content='#24aadd'/>
|
||||||
<link href='/css/app.min.css' rel='stylesheet' type='text/css'/>
|
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||||
<link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/>
|
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||||
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/>
|
<meta name='msapplication-TileColor' content='#ffffff'/>
|
||||||
|
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
|
||||||
|
<title>Loading...</title>
|
||||||
|
<!-- Base HTML Placeholder -->
|
||||||
|
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
|
||||||
|
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
|
||||||
|
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
|
||||||
|
<link rel='apple-touch-icon' sizes='180x180' href='img/apple-touch-icon.png'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-640x1136.png' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-750x1294.png' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1242x2148.png' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1125x2436.png' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1536x2048.png' media='(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1668x2224.png' media='(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
|
||||||
|
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-2048x2732.png' media='(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
|
||||||
|
<link rel='manifest' href='manifest.json'/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id='top-navigation-holder'></div>
|
<div id='top-navigation-holder'></div>
|
||||||
<div id='content-holder'></div>
|
<div id='content-holder'></div>
|
||||||
<script type='text/javascript' src='/js/vendor.min.js'></script>
|
<script type='text/javascript' src='js/vendor.min.js'></script>
|
||||||
<script type='text/javascript' src='/js/app.min.js'></script>
|
<script type='text/javascript' src='js/app.min.js'></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -30,9 +30,7 @@
|
|||||||
|
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<input type='submit' value='Log in'/>
|
<input type='submit' value='Log in'/>
|
||||||
<% if (ctx.canSendMails) { %>
|
<a class='append' href='<%- ctx.formatClientLink('password-reset') %>'>Forgot the password?</a>
|
||||||
<a class='append' href='/password-reset'>Forgot the password?</a>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
<nav class='buttons'>
|
<nav class='buttons'>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<% if (ctx.prevLinkActive) { %>
|
<% if (ctx.prevPage !== ctx.currentPage) { %>
|
||||||
<a class='prev' href='<%- ctx.prevLink %>'>
|
<a rel='prev' class='prev' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.prevPage).offset, ctx.pages.get(ctx.prevPage).limit) %>'>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a class='prev disabled'>
|
<a rel='prev' class='prev disabled'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<i class='fa fa-chevron-left'></i>
|
<i class='fa fa-chevron-left'></i>
|
||||||
<span class='vim-nav-hint'>< Previous page</span>
|
<span class='vim-nav-hint'>< Previous page</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<% for (let page of ctx.pages) { %>
|
<% for (let page of ctx.pages.values()) { %>
|
||||||
<% if (page.ellipsis) { %>
|
<% if (page.ellipsis) { %>
|
||||||
<li>…</li>
|
<li>…</li>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
@ -20,16 +20,16 @@
|
|||||||
<% } else { %>
|
<% } else { %>
|
||||||
<li>
|
<li>
|
||||||
<% } %>
|
<% } %>
|
||||||
<a href='<%- page.link %>'><%- page.number %></a>
|
<a href='<%- ctx.getClientUrlForPage(page.offset, page.limit) %>'><%- page.number %></a>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<% if (ctx.nextLinkActive) { %>
|
<% if (ctx.nextPage !== ctx.currentPage) { %>
|
||||||
<a class='next' href='<%- ctx.nextLink %>'>
|
<a rel='next' class='next' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.nextPage).offset, ctx.pages.get(ctx.nextPage).limit) %>'>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a class='next disabled'>
|
<a rel='next' class='next disabled'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<i class='fa fa-chevron-right'></i>
|
<i class='fa fa-chevron-right'></i>
|
||||||
<span class='vim-nav-hint'>Next page ></span>
|
<span class='vim-nav-hint'>Next page ></span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div class='not-found'>
|
<div class='not-found'>
|
||||||
<h1>Not found</h1>
|
<h1>Not found</h1>
|
||||||
<p><%- ctx.path %> is not a valid URL.</p>
|
<p><%- ctx.path %> is not a valid URL.</p>
|
||||||
<p><a href='/'>Back to main page</a></p>
|
<p><a href='<%- ctx.formatClientLink() %>'>Back to main page</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<div class='content-wrapper' id='password-reset'>
|
<div class='content-wrapper' id='password-reset'>
|
||||||
<h1>Password reset</h1>
|
<h1>Password reset</h1>
|
||||||
|
<% if (ctx.canSendMails) { %>
|
||||||
<form autocomplete='off'>
|
<form autocomplete='off'>
|
||||||
<ul class='input'>
|
<ul class='input'>
|
||||||
<li>
|
<li>
|
||||||
@ -20,4 +21,10 @@
|
|||||||
<input type='submit' value='Proceed'/>
|
<input type='submit' value='Proceed'/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<% } else { %>
|
||||||
|
<p>We do not support automatic password resetting.</p>
|
||||||
|
<% if (ctx.contactEmail) { %>
|
||||||
|
<p>Please send an e-mail to <a href='mailto:<%- ctx.contactEmail %>'><%- ctx.contactEmail %></a> to go through a manual procedure.</p>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
class: 'resize-listener',
|
class: 'resize-listener',
|
||||||
controls: true,
|
controls: true,
|
||||||
loop: (ctx.post.flags || []).includes('loop'),
|
loop: (ctx.post.flags || []).includes('loop'),
|
||||||
|
playsinline: true,
|
||||||
autoplay: ctx.autoplay,
|
autoplay: ctx.autoplay,
|
||||||
},
|
},
|
||||||
ctx.makeElement('source', {
|
ctx.makeElement('source', {
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
<h1>Post #<%- ctx.post.id %></h1>
|
<h1>Post #<%- ctx.post.id %></h1>
|
||||||
<nav class='buttons'><!--
|
<nav class='buttons'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
--><li><a href='/post/<%- ctx.post.id %>'><i class='fa fa-reply'></i> Main view</a></li><!--
|
--><li><a href='<%- ctx.formatClientLink('post', ctx.post.id) %>'><i class='fa fa-reply'></i> Main view</a></li><!--
|
||||||
--><% if (ctx.canMerge) { %><!--
|
--><% if (ctx.canMerge) { %><!--
|
||||||
--><li data-name='merge'><a href='/post/<%- ctx.post.id %>/merge'>Merge with…</a></li><!--
|
--><li data-name='merge'><a href='<%- ctx.formatClientLink('post', ctx.post.id, 'merge') %>'>Merge with…</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul><!--
|
--></ul><!--
|
||||||
--></nav>
|
--></nav>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
|
|
||||||
<% if (ctx.canEditPostSafety) { %>
|
<% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
|
||||||
<section class='safety'>
|
<section class='safety'>
|
||||||
<label>Safety</label>
|
<label>Safety</label>
|
||||||
<div class='radio-wrapper'>
|
<div class='radio-wrapper'>
|
||||||
@ -50,14 +50,26 @@
|
|||||||
name: 'loop',
|
name: 'loop',
|
||||||
checked: ctx.post.flags.includes('loop'),
|
checked: ctx.post.flags.includes('loop'),
|
||||||
}) %>
|
}) %>
|
||||||
|
<%= ctx.makeCheckbox({
|
||||||
|
text: 'Sound',
|
||||||
|
name: 'sound',
|
||||||
|
checked: ctx.post.flags.includes('sound'),
|
||||||
|
}) %>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% if (ctx.canEditPostSource) { %>
|
||||||
|
<section class='post-source'>
|
||||||
|
<%= ctx.makeTextarea({
|
||||||
|
text: 'Source',
|
||||||
|
value: ctx.post.source,
|
||||||
|
}) %>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (ctx.canEditPostTags) { %>
|
<% if (ctx.canEditPostTags) { %>
|
||||||
<section class='tags'>
|
<section class='tags'>
|
||||||
<%= ctx.makeTextInput({
|
<%= ctx.makeTextInput({}) %>
|
||||||
value: ctx.post.tags.join(' '),
|
|
||||||
}) %>
|
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
@ -66,6 +78,12 @@
|
|||||||
<a href class='add'>Add a note</a>
|
<a href class='add'>Add a note</a>
|
||||||
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
|
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
|
||||||
<a href class='delete inactive'>Delete selected note</a>
|
<a href class='delete inactive'>Delete selected note</a>
|
||||||
|
<% if (ctx.hasClipboard) { %>
|
||||||
|
<br/>
|
||||||
|
<a href class='copy'>Export notes to clipboard</a>
|
||||||
|
<br/>
|
||||||
|
<a href class='paste'>Import notes from clipboard</a>
|
||||||
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<article class='previous-post'>
|
<article class='previous-post'>
|
||||||
<% if (ctx.prevPostId) { %>
|
<% if (ctx.prevPostId) { %>
|
||||||
<% if (ctx.editMode) { %>
|
<% if (ctx.editMode) { %>
|
||||||
<a href='<%= ctx.getPostEditUrl(ctx.prevPostId, ctx.parameters) %>'>
|
<a rel='prev' href='<%= ctx.getPostEditUrl(ctx.prevPostId, ctx.parameters) %>'>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
|
<a rel='prev' href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a class='inactive'>
|
<a rel='prev' class='inactive'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<i class='fa fa-chevron-left'></i>
|
<i class='fa fa-chevron-left'></i>
|
||||||
<span class='vim-nav-hint'>< Previous post</span>
|
<span class='vim-nav-hint'>< Previous post</span>
|
||||||
@ -18,12 +18,12 @@
|
|||||||
<article class='next-post'>
|
<article class='next-post'>
|
||||||
<% if (ctx.nextPostId) { %>
|
<% if (ctx.nextPostId) { %>
|
||||||
<% if (ctx.editMode) { %>
|
<% if (ctx.editMode) { %>
|
||||||
<a href='<%= ctx.getPostEditUrl(ctx.nextPostId, ctx.parameters) %>'>
|
<a rel='next' href='<%= ctx.getPostEditUrl(ctx.nextPostId, ctx.parameters) %>'>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
|
<a rel='next' href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a class='inactive'>
|
<a rel='next' class='inactive'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<i class='fa fa-chevron-right'></i>
|
<i class='fa fa-chevron-right'></i>
|
||||||
<span class='vim-nav-hint'>Next post ></span>
|
<span class='vim-nav-hint'>Next post ></span>
|
||||||
|
@ -35,7 +35,9 @@
|
|||||||
'image/gif': 'GIF',
|
'image/gif': 'GIF',
|
||||||
'image/jpeg': 'JPEG',
|
'image/jpeg': 'JPEG',
|
||||||
'image/png': 'PNG',
|
'image/png': 'PNG',
|
||||||
|
'image/webp': 'WEBP',
|
||||||
'video/webm': 'WEBM',
|
'video/webm': 'WEBM',
|
||||||
|
'video/mp4': 'MPEG-4',
|
||||||
'application/x-shockwave-flash': 'SWF',
|
'application/x-shockwave-flash': 'SWF',
|
||||||
}[ctx.post.mimeType] +
|
}[ctx.post.mimeType] +
|
||||||
' (' +
|
' (' +
|
||||||
|
@ -8,11 +8,17 @@
|
|||||||
'image/gif': 'GIF',
|
'image/gif': 'GIF',
|
||||||
'image/jpeg': 'JPEG',
|
'image/jpeg': 'JPEG',
|
||||||
'image/png': 'PNG',
|
'image/png': 'PNG',
|
||||||
|
'image/webp': 'WEBP',
|
||||||
'video/webm': 'WEBM',
|
'video/webm': 'WEBM',
|
||||||
|
'video/mp4': 'MPEG-4',
|
||||||
'application/x-shockwave-flash': 'SWF',
|
'application/x-shockwave-flash': 'SWF',
|
||||||
}[ctx.post.mimeType] %>
|
}[ctx.post.mimeType] %>
|
||||||
</a>
|
</a>
|
||||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||||
|
<% if (ctx.post.flags.length) { %><!--
|
||||||
|
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||||
|
--><% if (ctx.post.flags.includes('sound')) { %><i class='fa fa-volume-up'></i><% } %>
|
||||||
|
<% } %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class='upload-info'>
|
<section class='upload-info'>
|
||||||
@ -20,10 +26,12 @@
|
|||||||
<%= ctx.makeRelativeTime(ctx.post.creationTime) %>
|
<%= ctx.makeRelativeTime(ctx.post.creationTime) %>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<% if (ctx.enableSafety) { %>
|
||||||
<section class='safety'>
|
<section class='safety'>
|
||||||
<i class='fa fa-circle safety-<%- ctx.post.safety %>'></i><!--
|
<i class='fa fa-circle safety-<%- ctx.post.safety %>'></i><!--
|
||||||
--><%- ctx.post.safety[0].toUpperCase() + ctx.post.safety.slice(1) %>
|
--><%- ctx.post.safety[0].toUpperCase() + ctx.post.safety.slice(1) %>
|
||||||
</section>
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<section class='zoom'>
|
<section class='zoom'>
|
||||||
<a href class='fit-original'>Original zoom</a> ·
|
<a href class='fit-original'>Original zoom</a> ·
|
||||||
@ -32,10 +40,19 @@
|
|||||||
<a href class='fit-both'>both</a>
|
<a href class='fit-both'>both</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<% if (ctx.post.source) { %>
|
||||||
|
<section class='source'>
|
||||||
|
Source: <% for (let i = 0; i < ctx.post.sourceSplit.length; i++) { %>
|
||||||
|
<% if (i != 0) { %>·<% } %>
|
||||||
|
<a href='<%- ctx.post.sourceSplit[i] %>' title='<%- ctx.post.sourceSplit[i] %>'><%- ctx.extractRootDomain(ctx.post.sourceSplit[i]) %></a>
|
||||||
|
<% } %>
|
||||||
|
</section>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<section class='search'>
|
<section class='search'>
|
||||||
Search on
|
Search on
|
||||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>IQDB</a> ·
|
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||||
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>Google Images</a>
|
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class='social'>
|
<section class='social'>
|
||||||
@ -67,20 +84,20 @@
|
|||||||
--><% for (let tag of ctx.post.tags) { %><!--
|
--><% for (let tag of ctx.post.tags) { %><!--
|
||||||
--><li><!--
|
--><li><!--
|
||||||
--><% if (ctx.canViewTags) { %><!--
|
--><% if (ctx.canViewTags) { %><!--
|
||||||
--><a href='/tag/<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
|
--><a href='<%- ctx.formatClientLink('tag', tag.names[0]) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||||
--><i class='fa fa-tag'></i><!--
|
--><i class='fa fa-tag'></i><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><% if (ctx.canViewTags) { %><!--
|
--><% if (ctx.canViewTags) { %><!--
|
||||||
--></a><!--
|
--></a><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--><a href='/posts/query=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
|
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><%- tag %> <!--
|
--><%- ctx.getPrettyTagName(tag.names[0]) %> <!--
|
||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--></a><!--
|
--></a><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><span class='tag-usages' data-pseudo-content='<%- ctx.getTagUsages(tag) %>'></span><!--
|
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||||
--></li><!--
|
--></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul>
|
--></ul>
|
||||||
|
@ -41,6 +41,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class='body'>
|
<div class='body'>
|
||||||
|
<% if (ctx.enableSafety) { %>
|
||||||
<div class='safety'>
|
<div class='safety'>
|
||||||
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
|
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
|
||||||
<%= ctx.makeRadio({
|
<%= ctx.makeRadio({
|
||||||
@ -51,6 +52,7 @@
|
|||||||
}) %>
|
}) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<div class='options'>
|
<div class='options'>
|
||||||
<% if (ctx.canUploadAnonymously) { %>
|
<% if (ctx.canUploadAnonymously) { %>
|
||||||
@ -62,16 +64,6 @@
|
|||||||
}) %>
|
}) %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (['video'].includes(ctx.uploadable.type)) { %>
|
|
||||||
<div class='loop-video'>
|
|
||||||
<%= ctx.makeCheckbox({
|
|
||||||
text: 'Loop video',
|
|
||||||
name: 'loop-video',
|
|
||||||
checked: ctx.uploadable.flags.includes('loop'),
|
|
||||||
}) %>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
|
@ -1,24 +1,31 @@
|
|||||||
<div class='post-list-header'><%
|
<div class='post-list-header'><%
|
||||||
%><form class='horizontal'><%
|
%><form class='horizontal search'><%
|
||||||
%><%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %><%
|
%><%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %><%
|
||||||
%><wbr/><%
|
%><wbr/><%
|
||||||
%><input class='mousetrap' type='submit' value='Search'/><%
|
%><input class='mousetrap' type='submit' value='Search'/><%
|
||||||
%><wbr/><%
|
%><wbr/><%
|
||||||
|
%><% if (ctx.enableSafety) { %><%
|
||||||
%><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><%
|
%><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><%
|
||||||
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
|
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
|
||||||
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
|
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
|
||||||
%><wbr/><%
|
|
||||||
%><a class='mousetrap button append' href='/help/search/posts'>Syntax help</a><%
|
|
||||||
%><% if (ctx.canMassTag) { %><%
|
|
||||||
%><wbr/><%
|
|
||||||
%><span class='masstag'><%
|
|
||||||
%><span class='append masstag-hint'>Tagging with:</span><%
|
|
||||||
%><a href class='mousetrap button append open-masstag'>Mass tag</a><%
|
|
||||||
%><wbr/><%
|
|
||||||
%><%= ctx.makeTextInput({name: 'masstag', value: ctx.parameters.tag}) %><%
|
|
||||||
%><input class='mousetrap start-tagging' type='submit' value='Start tagging'/><%
|
|
||||||
%><a href class='mousetrap button append stop-tagging'>Stop tagging</a><%
|
|
||||||
%></span><%
|
|
||||||
%><% } %><%
|
%><% } %><%
|
||||||
|
%><wbr/><%
|
||||||
|
%><a class='mousetrap button append' href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Syntax help</a><%
|
||||||
%></form><%
|
%></form><%
|
||||||
|
%><% if (ctx.canBulkEditTags) { %><%
|
||||||
|
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||||
|
%><span class='append hint'>Tagging with:</span><%
|
||||||
|
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||||
|
%><wbr/><%
|
||||||
|
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||||
|
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||||
|
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||||
|
%></form><%
|
||||||
|
%><% } %><%
|
||||||
|
%><% if (ctx.enableSafety && ctx.canBulkEditSafety) { %><%
|
||||||
|
%><form class='horizontal bulk-edit bulk-edit-safety'><%
|
||||||
|
%><a href class='mousetrap button append open'>Mass edit safety</a><%
|
||||||
|
%><a href class='mousetrap button append close'>Stop editing safety</a><%
|
||||||
|
%></form><%
|
||||||
|
%><% } %><%
|
||||||
%></div>
|
%></div>
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
<div class='post-list'>
|
<div class='post-list'>
|
||||||
<% if (ctx.results.length) { %>
|
<% if (ctx.response.results.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% for (let post of ctx.results) { %>
|
<% for (let post of ctx.response.results) { %>
|
||||||
<li>
|
<li data-post-id='<%= post.id %>'>
|
||||||
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
|
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
|
||||||
title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag).join(' ') || 'none' %>'
|
title='@<%- post.id %> (<%- post.type %>) Tags: <%- post.tags.map(tag => '#' + tag.names[0]).join(' ') || 'none' %>'
|
||||||
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : "" %>'>
|
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
|
||||||
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
|
||||||
<span class='type' data-type='<%- post.type %>'>
|
<span class='type' data-type='<%- post.type %>'>
|
||||||
|
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
|
||||||
|
<span class='icon'><i class='fa fa-film'></i></span>
|
||||||
|
<% } else { %>
|
||||||
<%- post.type %>
|
<%- post.type %>
|
||||||
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
<% if (post.score || post.favoriteCount || post.commentCount) { %>
|
<% if (post.score || post.favoriteCount || post.commentCount) { %>
|
||||||
<span class='stats'>
|
<span class='stats'>
|
||||||
@ -33,10 +37,20 @@
|
|||||||
</span>
|
</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
</a>
|
</a>
|
||||||
<% if (ctx.canMassTag && ctx.parameters && ctx.parameters.tag) { %>
|
<span class='edit-overlay'>
|
||||||
<a href data-post-id='<%= post.id %>' class='masstag'>
|
<% if (ctx.canBulkEditTags && ctx.parameters && ctx.parameters.tag) { %>
|
||||||
|
<a href class='tag-flipper'>
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (ctx.canBulkEditSafety && ctx.parameters && ctx.parameters.safety) { %>
|
||||||
|
<span class='safety-flipper'>
|
||||||
|
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
|
||||||
|
<a href data-safety='<%- safety %>' class='safety-<%- safety %><%- post.safety === safety ? ' active' : '' %>'>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
|
<% } %>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
<%= ctx.makeFlexboxAlign() %>
|
<%= ctx.makeFlexboxAlign() %>
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<ul class='input'>
|
<ul class='input'>
|
||||||
<li>
|
<li>
|
||||||
<%= ctx.makeCheckbox({
|
<%= ctx.makeCheckbox({
|
||||||
text: "Enable keyboard shortcuts <a class='append icon' href='/help/keyboard'><i class='fa fa-question-circle-o'></i></a>",
|
text: "Enable keyboard shortcuts <a class='append icon' href='" + ctx.formatClientLink('help', 'keyboard') + "'><i class='fa fa-question-circle-o'></i></a>",
|
||||||
name: 'keyboard-shortcuts',
|
name: 'keyboard-shortcuts',
|
||||||
checked: ctx.browsingSettings.keyboardShortcuts,
|
checked: ctx.browsingSettings.keyboardShortcuts,
|
||||||
}) %>
|
}) %>
|
||||||
@ -63,6 +63,15 @@
|
|||||||
checked: ctx.browsingSettings.autoplayVideos,
|
checked: ctx.browsingSettings.autoplayVideos,
|
||||||
}) %>
|
}) %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<%= ctx.makeCheckbox({
|
||||||
|
text: 'Display underscores as spaces in tags',
|
||||||
|
name: 'tag-underscores-as-spaces',
|
||||||
|
checked: ctx.browsingSettings.tagUnderscoresAsSpaces,
|
||||||
|
}) %>
|
||||||
|
<p class='hint'>Display all underscores as if they were spaces. This is only a visual change, which means that you'll still have to use underscores when searching or editing tags.</p>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class='messages'></div>
|
<div class='messages'></div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<div class='snapshot-list'>
|
<div class='snapshot-list'>
|
||||||
<% if (ctx.results.length) { %>
|
<% if (ctx.response.results.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% for (let item of ctx.results) { %>
|
<% for (let item of ctx.response.results) { %>
|
||||||
<li>
|
<li>
|
||||||
<div class='header operation-<%= item.operation %>'>
|
<div class='header operation-<%= item.operation %>'>
|
||||||
<span class='time'>
|
<span class='time'>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
<div class='content-wrapper' id='tag'>
|
<div class='content-wrapper' id='tag'>
|
||||||
<h1><%- ctx.tag.names[0] %></h1>
|
<h1><%- ctx.getPrettyTagName(ctx.tag.names[0]) %></h1>
|
||||||
<nav class='buttons'><!--
|
<nav class='buttons'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
--><li data-name='summary'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>'>Summary</a></li><!--
|
--><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--
|
||||||
--><% if (ctx.canEditAnything) { %><!--
|
--><% if (ctx.canEditAnything) { %><!--
|
||||||
--><li data-name='edit'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/edit'>Edit</a></li><!--
|
--><li data-name='edit'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'edit') %>'>Edit</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><% if (ctx.canMerge) { %><!--
|
--><% if (ctx.canMerge) { %><!--
|
||||||
--><li data-name='merge'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/merge'>Merge with…</a></li><!--
|
--><li data-name='merge'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'merge') %>'>Merge with…</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><% if (ctx.canDelete) { %><!--
|
--><% if (ctx.canDelete) { %><!--
|
||||||
--><li data-name='delete'><a href='/tag/<%- encodeURIComponent(ctx.tag.names[0]) %>/delete'>Delete</a></li><!--
|
--><li data-name='delete'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0], 'delete') %>'>Delete</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul><!--
|
--></ul><!--
|
||||||
--></nav>
|
--></nav>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<div class='content-wrapper tag-categories'>
|
<div class='content-wrapper tag-categories'>
|
||||||
<form>
|
<form>
|
||||||
<h1>Tag categories</h1>
|
<h1>Tag categories</h1>
|
||||||
|
<div class="table-wrap">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -12,6 +13,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% if (ctx.canCreate) { %>
|
<% if (ctx.canCreate) { %>
|
||||||
<p><a href class='add'>Add new category</a></p>
|
<p><a href class='add'>Add new category</a></p>
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class='usages'>
|
<td class='usages'>
|
||||||
<% if (ctx.tagCategory.name) { %>
|
<% if (ctx.tagCategory.name) { %>
|
||||||
<a href='/tags/query=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>
|
||||||
<%- ctx.tagCategory.tagCount %>
|
<%- ctx.tagCategory.tagCount %>
|
||||||
</a>
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class='tag-delete'>
|
<div class='tag-delete'>
|
||||||
<form>
|
<form>
|
||||||
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||||
|
|
||||||
<ul class='input'>
|
<ul class='input'>
|
||||||
<li>
|
<li>
|
||||||
|
@ -22,18 +22,12 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class='implications'>
|
<li class='implications'>
|
||||||
<% if (ctx.canEditImplications) { %>
|
<% if (ctx.canEditImplications) { %>
|
||||||
<%= ctx.makeTextInput({
|
<%= ctx.makeTextInput({text: 'Implications'}) %>
|
||||||
text: 'Implications',
|
|
||||||
value: ctx.tag.implications.join(' '),
|
|
||||||
}) %>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</li>
|
</li>
|
||||||
<li class='suggestions'>
|
<li class='suggestions'>
|
||||||
<% if (ctx.canEditSuggestions) { %>
|
<% if (ctx.canEditSuggestions) { %>
|
||||||
<%= ctx.makeTextInput({
|
<%= ctx.makeTextInput({text: 'Suggestions'}) %>
|
||||||
text: 'Suggestions',
|
|
||||||
value: ctx.tag.suggestions.join(' '),
|
|
||||||
}) %>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</li>
|
</li>
|
||||||
<li class='description'>
|
<li class='description'>
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
<form>
|
<form>
|
||||||
<ul class='input'>
|
<ul class='input'>
|
||||||
<li class='target'>
|
<li class='target'>
|
||||||
<%= ctx.makeTextInput({required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
|
<%= ctx.makeTextInput({name: 'target-tag', required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<p>Usages in posts, suggestions and implications will be
|
<p>Usages in posts, suggestions and implications will be
|
||||||
merged. Category and aliases need to be handled manually.</p>
|
merged. Category needs to be handled manually.</p>
|
||||||
|
|
||||||
|
<%= ctx.makeCheckbox({name: 'alias', text: 'Make this tag an alias of the target tag.'}) %>
|
||||||
|
|
||||||
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
|
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
|
||||||
</li>
|
</li>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
Aliases:<br/>
|
Aliases:<br/>
|
||||||
<ul><!--
|
<ul><!--
|
||||||
--><% for (let name of ctx.tag.names.slice(1)) { %><!--
|
--><% for (let name of ctx.tag.names.slice(1)) { %><!--
|
||||||
--><li><%= ctx.makeTagLink(name) %></li><!--
|
--><li><%= ctx.makeTagLink(name, false, false, ctx.tag) %></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul>
|
--></ul>
|
||||||
</section>
|
</section>
|
||||||
@ -18,7 +18,7 @@
|
|||||||
Implications:<br/>
|
Implications:<br/>
|
||||||
<ul><!--
|
<ul><!--
|
||||||
--><% for (let tag of ctx.tag.implications) { %><!--
|
--><% for (let tag of ctx.tag.implications) { %><!--
|
||||||
--><li><%= ctx.makeTagLink(tag) %></li><!--
|
--><li><%= ctx.makeTagLink(tag.names[0], false, false, tag) %></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul>
|
--></ul>
|
||||||
</section>
|
</section>
|
||||||
@ -27,7 +27,7 @@
|
|||||||
Suggestions:<br/>
|
Suggestions:<br/>
|
||||||
<ul><!--
|
<ul><!--
|
||||||
--><% for (let tag of ctx.tag.suggestions) { %><!--
|
--><% for (let tag of ctx.tag.suggestions) { %><!--
|
||||||
--><li><%= ctx.makeTagLink(tag) %></li><!--
|
--><li><%= ctx.makeTagLink(tag.names[0], false, false, tag) %></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul>
|
--></ul>
|
||||||
</section>
|
</section>
|
||||||
@ -36,6 +36,6 @@
|
|||||||
<section class='description'>
|
<section class='description'>
|
||||||
<hr/>
|
<hr/>
|
||||||
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
|
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
|
||||||
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<input type='submit' value='Search'/>
|
<input type='submit' value='Search'/>
|
||||||
<a class='button append' href='/help/search/tags'>Syntax help</a>
|
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Syntax help</a>
|
||||||
<% if (ctx.canEditTagCategories) { %>
|
<% if (ctx.canEditTagCategories) { %>
|
||||||
<a class='append' href='/tag-categories'>Tag categories</a>
|
<a class='append' href='<%- ctx.formatClientLink('tag-categories') %>'>Tag categories</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,58 +1,58 @@
|
|||||||
<div class='tag-list'>
|
<div class='tag-list table-wrap'>
|
||||||
<% if (ctx.results.length) { %>
|
<% if (ctx.response.results.length) { %>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<th class='names'>
|
<th class='names'>
|
||||||
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
|
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
|
||||||
<a href='/tags/query=-sort:name'>Tag name(s)</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/tags/query=sort:name'>Tag name(s)</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:name'}) %>'>Tag name(s)</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</th>
|
</th>
|
||||||
<th class='implications'>
|
<th class='implications'>
|
||||||
<% if (ctx.query == 'sort:implication-count') { %>
|
<% if (ctx.query == 'sort:implication-count') { %>
|
||||||
<a href='/tags/query=-sort:implication-count'>Implications</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/tags/query=sort:implication-count'>Implications</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:implication-count'}) %>'>Implications</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</th>
|
</th>
|
||||||
<th class='suggestions'>
|
<th class='suggestions'>
|
||||||
<% if (ctx.query == 'sort:suggestion-count') { %>
|
<% if (ctx.query == 'sort:suggestion-count') { %>
|
||||||
<a href='/tags/query=-sort:suggestion-count'>Suggestions</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/tags/query=sort:suggestion-count'>Suggestions</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:suggestion-count'}) %>'>Suggestions</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</th>
|
</th>
|
||||||
<th class='usages'>
|
<th class='usages'>
|
||||||
<% if (ctx.query == 'sort:usages') { %>
|
<% if (ctx.query == 'sort:usages') { %>
|
||||||
<a href='/tags/query=-sort:usages'>Usages</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/tags/query=sort:usages'>Usages</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:usages'}) %>'>Usages</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</th>
|
</th>
|
||||||
<th class='creation-time'>
|
<th class='creation-time'>
|
||||||
<% if (ctx.query == 'sort:creation-time') { %>
|
<% if (ctx.query == 'sort:creation-time') { %>
|
||||||
<a href='/tags/query=-sort:creation-time'>Created on</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<a href='/tags/query=sort:creation-time'>Created on</a>
|
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</th>
|
</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<% for (let tag of ctx.results) { %>
|
<% for (let tag of ctx.response.results) { %>
|
||||||
<tr>
|
<tr>
|
||||||
<td class='names'>
|
<td class='names'>
|
||||||
<ul>
|
<ul>
|
||||||
<% for (let name of tag.names) { %>
|
<% for (let name of tag.names) { %>
|
||||||
<li><%= ctx.makeTagLink(name) %></li>
|
<li><%= ctx.makeTagLink(name, false, false, tag) %></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</td>
|
</td>
|
||||||
<td class='implications'>
|
<td class='implications'>
|
||||||
<% if (tag.implications.length) { %>
|
<% if (tag.implications.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% for (let name of tag.implications) { %>
|
<% for (let relation of tag.implications) { %>
|
||||||
<li><%= ctx.makeTagLink(name) %></li>
|
<li><%= ctx.makeTagLink(relation.names[0], false, false, relation) %></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
@ -62,8 +62,8 @@
|
|||||||
<td class='suggestions'>
|
<td class='suggestions'>
|
||||||
<% if (tag.suggestions.length) { %>
|
<% if (tag.suggestions.length) { %>
|
||||||
<ul>
|
<ul>
|
||||||
<% for (let name of tag.suggestions) { %>
|
<% for (let relation of tag.suggestions) { %>
|
||||||
<li><%= ctx.makeTagLink(name) %></li>
|
<li><%= ctx.makeTagLink(relation.names[0], false, false, relation) %></li>
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
<nav id='top-navigation' class='buttons'><!--
|
<nav id='top-navigation' class='buttons'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
|
--><button id="mobile-navigation-toggle"><!--
|
||||||
|
--><span class="site-name"><%- ctx.name %></span><!--
|
||||||
|
--><span class="toggle-icon"><i class="fa fa-bars"></i></span><!--
|
||||||
|
--></button><!--
|
||||||
--><% for (let item of ctx.items) { %><!--
|
--><% for (let item of ctx.items) { %><!--
|
||||||
--><% if (item.available) { %><!--
|
--><% if (item.available) { %><!--
|
||||||
--><li data-name='<%- item.key %>'><!--
|
--><li data-name='<%- item.key %>'><!--
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
<h1><%- ctx.user.name %></h1>
|
<h1><%- ctx.user.name %></h1>
|
||||||
<nav class='buttons'><!--
|
<nav class='buttons'><!--
|
||||||
--><ul><!--
|
--><ul><!--
|
||||||
--><li data-name='summary'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'>Summary</a></li><!--
|
--><li data-name='summary'><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>Summary</a></li><!--
|
||||||
--><% if (ctx.canEditAnything) { %><!--
|
--><% if (ctx.canEditAnything) { %><!--
|
||||||
--><li data-name='edit'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/edit'>Account settings</a></li><!--
|
--><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Settings</a></li><!--
|
||||||
|
--><% } %><!--
|
||||||
|
--><% if (ctx.canListTokens) { %><!--
|
||||||
|
--><li data-name='list-tokens'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'list-tokens') %>'>Login tokens</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><% if (ctx.canDelete) { %><!--
|
--><% if (ctx.canDelete) { %><!--
|
||||||
--><li data-name='delete'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/delete'>Account deletion</a></li><!--
|
--><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Delete</a></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--></ul><!--
|
--></ul><!--
|
||||||
--></nav>
|
--></nav>
|
||||||
|
@ -51,6 +51,6 @@
|
|||||||
<li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li>
|
<li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr/>
|
<hr/>
|
||||||
<p>By creating an account, you are agreeing to the <a href='/help/tos'>Terms of Service</a>.</p>
|
<p>By creating an account, you are agreeing to the <a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of Service</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<p><strong>Quick links</strong></p>
|
<p><strong>Quick links</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href='/posts/query=submit:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
|
<li><a href='<%- ctx.formatClientLink('posts', {query: 'submit:' + ctx.user.name}) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
|
||||||
<li><a href='/posts/query=fav:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
|
<li><a href='<%- ctx.formatClientLink('posts', {query: 'fav:' + ctx.user.name}) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
|
||||||
<li><a href='/posts/query=comment:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.commentCount %> comments</a></li>
|
<li><a href='<%- ctx.formatClientLink('posts', {query: 'comment:' + ctx.user.name}) %>'><%- ctx.user.commentCount %> comments</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -20,8 +20,8 @@
|
|||||||
<nav>
|
<nav>
|
||||||
<p><strong>Only visible to you</strong></p>
|
<p><strong>Only visible to you</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href='/posts/query=special:liked'><%- ctx.user.likedPostCount %> liked posts</a></li>
|
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:liked'}) %>'><%- ctx.user.likedPostCount %> liked posts</a></li>
|
||||||
<li><a href='/posts/query=special:disliked'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
|
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:disliked'}) %>'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
74
client/html/user_tokens.tpl
Normal file
74
client/html/user_tokens.tpl
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<div id='user-tokens'>
|
||||||
|
<div class='messages'></div>
|
||||||
|
<% if (ctx.tokens.length > 0) { %>
|
||||||
|
<div class='token-flex-container'>
|
||||||
|
<% _.each(ctx.tokens, function(token, index) { %>
|
||||||
|
<div class='token-flex-row'>
|
||||||
|
<div class='token-flex-column token-flex-labels'>
|
||||||
|
<div class='token-flex-row'>Token:</div>
|
||||||
|
<div class='token-flex-row'>Note:</div>
|
||||||
|
<div class='token-flex-row'>Created:</div>
|
||||||
|
<div class='token-flex-row'>Expires:</div>
|
||||||
|
<div class='token-flex-row no-wrap'>Last used:</div>
|
||||||
|
</div>
|
||||||
|
<div class='token-flex-column full-width'>
|
||||||
|
<div class='token-flex-row'><%= token.token %></div>
|
||||||
|
<div class='token-flex-row'>
|
||||||
|
<% if (token.note !== null) { %>
|
||||||
|
<%= token.note %>
|
||||||
|
<% } else { %>
|
||||||
|
No note
|
||||||
|
<% } %>
|
||||||
|
<a class='token-change-note' data-token-id='<%= index %>' href='#'>(change)</a>
|
||||||
|
</div>
|
||||||
|
<div class='token-flex-row'><%= ctx.makeRelativeTime(token.creationTime) %></div>
|
||||||
|
<div class='token-flex-row'>
|
||||||
|
<% if (token.expirationTime) { %>
|
||||||
|
<%= ctx.makeRelativeTime(token.expirationTime) %>
|
||||||
|
<% } else { %>
|
||||||
|
No expiration
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
<div class='token-flex-row'><%= ctx.makeRelativeTime(token.lastUsageTime) %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='token-flex-row'>
|
||||||
|
<div class='token-flex-column full-width'>
|
||||||
|
<div class='token-flex-row'>
|
||||||
|
<form class='token' data-token-id='<%= index %>'>
|
||||||
|
<% if (token.isCurrentAuthToken) { %>
|
||||||
|
<input type='submit' value='Delete and logout'
|
||||||
|
title='This token is used to authenticate this client, deleting it will force a logout.'/>
|
||||||
|
<% } else { %>
|
||||||
|
<input type='submit' value='Delete'/>
|
||||||
|
<% } %>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<h2>No Registered Tokens</h2>
|
||||||
|
<% } %>
|
||||||
|
<form id='create-token-form'>
|
||||||
|
<ul class='input'>
|
||||||
|
<li class='note'>
|
||||||
|
<%= ctx.makeTextInput({
|
||||||
|
text: 'Note',
|
||||||
|
id: 'note',
|
||||||
|
}) %>
|
||||||
|
</li>
|
||||||
|
<li class='expirationTime'>
|
||||||
|
<%= ctx.makeDateInput({
|
||||||
|
text: 'Expires',
|
||||||
|
id: 'expirationTime',
|
||||||
|
}) %>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class='buttons'>
|
||||||
|
<input type='submit' value='Create token'/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div class='buttons'>
|
<div class='buttons'>
|
||||||
<input type='submit' value='Search'/>
|
<input type='submit' value='Search'/>
|
||||||
<a class='append' href='/help/search/users'>Syntax help</a>
|
<a class='append' href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Syntax help</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<div class='user-list'>
|
<div class='user-list'>
|
||||||
<ul><!--
|
<ul><!--
|
||||||
--><% for (let user of ctx.results) { %><!--
|
--><% for (let user of ctx.response.results) { %><!--
|
||||||
--><li>
|
--><li>
|
||||||
<div class='wrapper'>
|
<div class='wrapper'>
|
||||||
<% if (ctx.canViewUsers) { %>
|
<% if (ctx.canViewUsers) { %>
|
||||||
<a class='image' href='/user/<%- encodeURIComponent(user.name) %>'>
|
<a class='image' href='<%- ctx.formatClientLink('user', user.name) %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<%= ctx.makeThumbnail(user.avatarUrl) %>
|
<%= ctx.makeThumbnail(user.avatarUrl) %>
|
||||||
<% if (ctx.canViewUsers) { %>
|
<% if (ctx.canViewUsers) { %>
|
||||||
@ -12,7 +12,7 @@
|
|||||||
<% } %>
|
<% } %>
|
||||||
<div class='details'>
|
<div class='details'>
|
||||||
<% if (ctx.canViewUsers) { %>
|
<% if (ctx.canViewUsers) { %>
|
||||||
<a href='/user/<%- encodeURIComponent(user.name) %>'>
|
<a href='<%- ctx.formatClientLink('user', user.name) %>'>
|
||||||
<% } %>
|
<% } %>
|
||||||
<%- user.name %>
|
<%- user.name %>
|
||||||
<% if (ctx.canViewUsers) { %>
|
<% if (ctx.canViewUsers) { %>
|
||||||
|
BIN
client/img/app.png
Normal file
BIN
client/img/app.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
BIN
client/img/splash.png
Normal file
BIN
client/img/splash.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 113 KiB |
150
client/js/api.js
150
client/js/api.js
@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
const cookies = require('js-cookie');
|
const cookies = require('js-cookie');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
const config = require('./config.js');
|
|
||||||
const events = require('./events.js');
|
const events = require('./events.js');
|
||||||
const progress = require('./util/progress.js');
|
const progress = require('./util/progress.js');
|
||||||
|
const uri = require('./util/uri.js');
|
||||||
|
|
||||||
let fileTokens = {};
|
let fileTokens = {};
|
||||||
|
let remoteConfig = null;
|
||||||
|
|
||||||
class Api extends events.EventTarget {
|
class Api extends events.EventTarget {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -14,6 +15,7 @@ class Api extends events.EventTarget {
|
|||||||
this.user = null;
|
this.user = null;
|
||||||
this.userName = null;
|
this.userName = null;
|
||||||
this.userPassword = null;
|
this.userPassword = null;
|
||||||
|
this.token = null;
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
this.allRanks = [
|
this.allRanks = [
|
||||||
'anonymous',
|
'anonymous',
|
||||||
@ -63,14 +65,53 @@ class Api extends events.EventTarget {
|
|||||||
return this._wrappedRequest(url, request.delete, data, {}, options);
|
return this._wrappedRequest(url, request.delete, data, {}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchConfig() {
|
||||||
|
if (remoteConfig === null) {
|
||||||
|
return this.get(uri.formatApiLink('info'))
|
||||||
|
.then(response => {
|
||||||
|
remoteConfig = response.config;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return remoteConfig.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTagNameRegex() {
|
||||||
|
return remoteConfig.tagNameRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPasswordRegex() {
|
||||||
|
return remoteConfig.passwordRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserNameRegex() {
|
||||||
|
return remoteConfig.userNameRegex;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContactEmail() {
|
||||||
|
return remoteConfig.contactEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
canSendMails() {
|
||||||
|
return !!remoteConfig.canSendMails;
|
||||||
|
}
|
||||||
|
|
||||||
|
safetyEnabled() {
|
||||||
|
return !!remoteConfig.enableSafety;
|
||||||
|
}
|
||||||
|
|
||||||
hasPrivilege(lookup) {
|
hasPrivilege(lookup) {
|
||||||
let minViableRank = null;
|
let minViableRank = null;
|
||||||
for (let privilege of Object.keys(config.privileges)) {
|
for (let p of Object.keys(remoteConfig.privileges)) {
|
||||||
if (!privilege.startsWith(lookup)) {
|
if (!p.startsWith(lookup)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const rankName = config.privileges[privilege];
|
const rankIndex = this.allRanks.indexOf(
|
||||||
const rankIndex = this.allRanks.indexOf(rankName);
|
remoteConfig.privileges[p]);
|
||||||
if (minViableRank === null || rankIndex < minViableRank) {
|
if (minViableRank === null || rankIndex < minViableRank) {
|
||||||
minViableRank = rankIndex;
|
minViableRank = rankIndex;
|
||||||
}
|
}
|
||||||
@ -86,11 +127,76 @@ class Api extends events.EventTarget {
|
|||||||
|
|
||||||
loginFromCookies() {
|
loginFromCookies() {
|
||||||
const auth = cookies.getJSON('auth');
|
const auth = cookies.getJSON('auth');
|
||||||
return auth && auth.user && auth.password ?
|
return auth && auth.user && auth.token ?
|
||||||
this.login(auth.user, auth.password, true) :
|
this.loginWithToken(auth.user, auth.token, true) :
|
||||||
Promise.resolve();
|
Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginWithToken(userName, token, doRemember) {
|
||||||
|
this.cache = {};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.userName = userName;
|
||||||
|
this.token = token;
|
||||||
|
this.get('/user/' + userName + '?bump-login=true')
|
||||||
|
.then(response => {
|
||||||
|
const options = {};
|
||||||
|
if (doRemember) {
|
||||||
|
options.expires = 365;
|
||||||
|
}
|
||||||
|
cookies.set(
|
||||||
|
'auth',
|
||||||
|
{'user': userName, 'token': token},
|
||||||
|
options);
|
||||||
|
this.user = response;
|
||||||
|
resolve();
|
||||||
|
this.dispatchEvent(new CustomEvent('login'));
|
||||||
|
}, error => {
|
||||||
|
reject(error);
|
||||||
|
this.logout();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
createToken(userName, options) {
|
||||||
|
let userTokenRequest = {
|
||||||
|
enabled: true,
|
||||||
|
note: 'Web Login Token'
|
||||||
|
};
|
||||||
|
if (typeof options.expires !== 'undefined') {
|
||||||
|
userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.post('/user-token/' + userName, userTokenRequest)
|
||||||
|
.then(response => {
|
||||||
|
cookies.set(
|
||||||
|
'auth',
|
||||||
|
{'user': userName, 'token': response.token},
|
||||||
|
options);
|
||||||
|
this.userName = userName;
|
||||||
|
this.token = response.token;
|
||||||
|
this.userPassword = null;
|
||||||
|
}, error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteToken(userName, userToken) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.delete('/user-token/' + userName + '/' + userToken, {})
|
||||||
|
.then(response => {
|
||||||
|
const options = {};
|
||||||
|
cookies.set(
|
||||||
|
'auth',
|
||||||
|
{'user': userName, 'token': null},
|
||||||
|
options);
|
||||||
|
resolve();
|
||||||
|
}, error => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
login(userName, userPassword, doRemember) {
|
login(userName, userPassword, doRemember) {
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -102,10 +208,7 @@ class Api extends events.EventTarget {
|
|||||||
if (doRemember) {
|
if (doRemember) {
|
||||||
options.expires = 365;
|
options.expires = 365;
|
||||||
}
|
}
|
||||||
cookies.set(
|
this.createToken(this.userName, options);
|
||||||
'auth',
|
|
||||||
{'user': userName, 'password': userPassword},
|
|
||||||
options);
|
|
||||||
this.user = response;
|
this.user = response;
|
||||||
resolve();
|
resolve();
|
||||||
this.dispatchEvent(new CustomEvent('login'));
|
this.dispatchEvent(new CustomEvent('login'));
|
||||||
@ -117,9 +220,20 @@ class Api extends events.EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
|
let self = this;
|
||||||
|
this.deleteToken(this.userName, this.token)
|
||||||
|
.then(response => {
|
||||||
|
self._logout();
|
||||||
|
}, error => {
|
||||||
|
self._logout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logout() {
|
||||||
this.user = null;
|
this.user = null;
|
||||||
this.userName = null;
|
this.userName = null;
|
||||||
this.userPassword = null;
|
this.userPassword = null;
|
||||||
|
this.token = null;
|
||||||
this.dispatchEvent(new CustomEvent('logout'));
|
this.dispatchEvent(new CustomEvent('logout'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,9 +250,13 @@ class Api extends events.EventTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isCurrentAuthToken(userToken) {
|
||||||
|
return userToken.token === this.token;
|
||||||
|
}
|
||||||
|
|
||||||
_getFullUrl(url) {
|
_getFullUrl(url) {
|
||||||
const fullUrl =
|
const fullUrl =
|
||||||
(config.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/');
|
('api/' + url).replace(/([^:])\/+/g, '$1/');
|
||||||
const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
|
const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
|
||||||
const baseUrl = matches[1];
|
const baseUrl = matches[1];
|
||||||
const request = matches[2];
|
const request = matches[2];
|
||||||
@ -209,7 +327,7 @@ class Api extends events.EventTarget {
|
|||||||
let abortFunction = () => {};
|
let abortFunction = () => {};
|
||||||
let returnedPromise = new Promise((resolve, reject) => {
|
let returnedPromise = new Promise((resolve, reject) => {
|
||||||
let uploadPromise = this._rawRequest(
|
let uploadPromise = this._rawRequest(
|
||||||
'/uploads', request.post, {}, {content: file}, options);
|
'uploads', request.post, {}, {content: file}, options);
|
||||||
abortFunction = () => uploadPromise.abort();
|
abortFunction = () => uploadPromise.abort();
|
||||||
return uploadPromise.then(
|
return uploadPromise.then(
|
||||||
response => {
|
response => {
|
||||||
@ -257,7 +375,11 @@ class Api extends events.EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.userName && this.userPassword) {
|
if (this.userName && this.token) {
|
||||||
|
req.auth = null;
|
||||||
|
req.set('Authorization', 'Token '
|
||||||
|
+ new Buffer(this.userName + ":" + this.token).toString('base64'))
|
||||||
|
} else if (this.userName && this.userPassword) {
|
||||||
req.auth(
|
req.auth(
|
||||||
this.userName,
|
this.userName,
|
||||||
encodeURIComponent(this.userPassword)
|
encodeURIComponent(this.userPassword)
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const tags = require('../tags.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const LoginView = require('../views/login_view.js');
|
const LoginView = require('../views/login_view.js');
|
||||||
|
|
||||||
@ -21,8 +23,10 @@ class LoginController {
|
|||||||
api.forget();
|
api.forget();
|
||||||
api.login(e.detail.name, e.detail.password, e.detail.remember)
|
api.login(e.detail.name, e.detail.password, e.detail.remember)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const ctx = router.show('/');
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showSuccess('Logged in');
|
ctx.controller.showSuccess('Logged in');
|
||||||
|
// reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous
|
||||||
|
tags.refreshCategoryColorMap();
|
||||||
}, error => {
|
}, error => {
|
||||||
this._loginView.showError(error.message);
|
this._loginView.showError(error.message);
|
||||||
this._loginView.enableForm();
|
this._loginView.enableForm();
|
||||||
@ -34,16 +38,16 @@ class LogoutController {
|
|||||||
constructor() {
|
constructor() {
|
||||||
api.forget();
|
api.forget();
|
||||||
api.logout();
|
api.logout();
|
||||||
const ctx = router.show('/');
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showSuccess('Logged out');
|
ctx.controller.showSuccess('Logged out');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/login', (ctx, next) => {
|
router.enter(['login'], (ctx, next) => {
|
||||||
ctx.controller = new LoginController();
|
ctx.controller = new LoginController();
|
||||||
});
|
});
|
||||||
router.enter('/logout', (ctx, next) => {
|
router.enter(['logout'], (ctx, next) => {
|
||||||
ctx.controller = new LogoutController();
|
ctx.controller = new LogoutController();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const uri = require('../util/uri.js');
|
||||||
const PostList = require('../models/post_list.js');
|
const PostList = require('../models/post_list.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const PageController = require('../controllers/page_controller.js');
|
const PageController = require('../controllers/page_controller.js');
|
||||||
@ -25,14 +25,16 @@ class CommentsController {
|
|||||||
this._pageController = new PageController();
|
this._pageController = new PageController();
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: ctx.parameters,
|
parameters: ctx.parameters,
|
||||||
getClientUrlForPage: page => {
|
defaultLimit: 10,
|
||||||
|
getClientUrlForPage: (offset, limit) => {
|
||||||
const parameters = Object.assign(
|
const parameters = Object.assign(
|
||||||
{}, ctx.parameters, {page: page});
|
{}, ctx.parameters, {offset: offset, limit: limit});
|
||||||
return '/comments/' + misc.formatUrlParameters(parameters);
|
return uri.formatClientLink('comments', parameters);
|
||||||
},
|
},
|
||||||
requestPage: page => {
|
requestPage: (offset, limit) => {
|
||||||
return PostList.search(
|
return PostList.search(
|
||||||
'sort:comment-date comment-count-min:1', page, 10, fields);
|
'sort:comment-date comment-count-min:1',
|
||||||
|
offset, limit, fields);
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
Object.assign(pageCtx, {
|
Object.assign(pageCtx, {
|
||||||
@ -69,7 +71,6 @@ class CommentsController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/comments/:parameters?',
|
router.enter(['comments'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => { new CommentsController(ctx); });
|
(ctx, next) => { new CommentsController(ctx); });
|
||||||
};
|
};
|
||||||
|
@ -12,13 +12,13 @@ class HelpController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/help', (ctx, next) => {
|
router.enter(['help'], (ctx, next) => {
|
||||||
new HelpController();
|
new HelpController();
|
||||||
});
|
});
|
||||||
router.enter('/help/:section', (ctx, next) => {
|
router.enter(['help', ':section'], (ctx, next) => {
|
||||||
new HelpController(ctx.parameters.section);
|
new HelpController(ctx.parameters.section);
|
||||||
});
|
});
|
||||||
router.enter('/help/:section/:subsection', (ctx, next) => {
|
router.enter(['help', ':section', ':subsection'], (ctx, next) => {
|
||||||
new HelpController(ctx.parameters.section, ctx.parameters.subsection);
|
new HelpController(ctx.parameters.section, ctx.parameters.subsection);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ class HomeController {
|
|||||||
topNavigation.setTitle('Home');
|
topNavigation.setTitle('Home');
|
||||||
|
|
||||||
this._homeView = new HomeView({
|
this._homeView = new HomeView({
|
||||||
name: config.name,
|
name: api.getName(),
|
||||||
version: config.meta.version,
|
version: config.meta.version,
|
||||||
buildDate: config.meta.buildDate,
|
buildDate: config.meta.buildDate,
|
||||||
canListSnapshots: api.hasPrivilege('snapshots:list'),
|
canListSnapshots: api.hasPrivilege('snapshots:list'),
|
||||||
@ -44,7 +44,7 @@ class HomeController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/', (ctx, next) => {
|
router.enter([], (ctx, next) => {
|
||||||
ctx.controller = new HomeController();
|
ctx.controller = new HomeController();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ class NotFoundController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('*', (ctx, next) => {
|
router.enter(null, (ctx, next) => {
|
||||||
ctx.controller = new NotFoundController(ctx.canonicalPath);
|
ctx.controller = new NotFoundController(ctx.canonicalPath);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -18,12 +18,6 @@ class PageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run(ctx) {
|
run(ctx) {
|
||||||
const extendedContext = {
|
|
||||||
getClientUrlForPage: ctx.getClientUrlForPage,
|
|
||||||
parameters: ctx.parameters,
|
|
||||||
};
|
|
||||||
|
|
||||||
ctx.pageContext = Object.assign({}, extendedContext);
|
|
||||||
this._view.run(ctx);
|
this._view.run(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const PasswordResetView = require('../views/password_reset_view.js');
|
const PasswordResetView = require('../views/password_reset_view.js');
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ class PasswordResetController {
|
|||||||
this._passwordResetView.disableForm();
|
this._passwordResetView.disableForm();
|
||||||
api.forget();
|
api.forget();
|
||||||
api.logout();
|
api.logout();
|
||||||
api.get('/password-reset/' + e.detail.userNameOrEmail)
|
api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._passwordResetView.showSuccess(
|
this._passwordResetView.showSuccess(
|
||||||
'E-mail has been sent. To finish the procedure, ' +
|
'E-mail has been sent. To finish the procedure, ' +
|
||||||
@ -37,26 +38,26 @@ class PasswordResetFinishController {
|
|||||||
api.forget();
|
api.forget();
|
||||||
api.logout();
|
api.logout();
|
||||||
let password = null;
|
let password = null;
|
||||||
api.post('/password-reset/' + name, {token: token})
|
api.post(uri.formatApiLink('password-reset', name), {token: token})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
password = response.password;
|
password = response.password;
|
||||||
return api.login(name, password, false);
|
return api.login(name, password, false);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ctx = router.show('/');
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showSuccess('New password: ' + password);
|
ctx.controller.showSuccess('New password: ' + password);
|
||||||
}, error => {
|
}, error => {
|
||||||
const ctx = router.show('/');
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showError(error.message);
|
ctx.controller.showError(error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/password-reset', (ctx, next) => {
|
router.enter(['password-reset'], (ctx, next) => {
|
||||||
ctx.controller = new PasswordResetController();
|
ctx.controller = new PasswordResetController();
|
||||||
});
|
});
|
||||||
router.enter(/\/password-reset\/([^:]+):([^:]+)$/, (ctx, next) => {
|
router.enter(['password-reset', ':descriptor'], (ctx, next) => {
|
||||||
ctx.controller = new PasswordResetFinishController(
|
const [name, token] = ctx.parameters.descriptor.split(':', 2);
|
||||||
ctx.parameters[0], ctx.parameters[1]);
|
ctx.controller = new PasswordResetFinishController(name, token);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const settings = require('../models/settings.js');
|
const settings = require('../models/settings.js');
|
||||||
const Post = require('../models/post.js');
|
const Post = require('../models/post.js');
|
||||||
const PostList = require('../models/post_list.js');
|
const PostList = require('../models/post_list.js');
|
||||||
@ -55,7 +56,8 @@ class PostDetailController extends BasePostController {
|
|||||||
misc.disableExitConfirmation();
|
misc.disableExitConfirmation();
|
||||||
if (this._id !== e.detail.post.id) {
|
if (this._id !== e.detail.post.id) {
|
||||||
router.replace(
|
router.replace(
|
||||||
'/post/' + e.detail.post.id + '/' + section, null, false);
|
uri.formatClientLink('post', e.detail.post.id, section),
|
||||||
|
null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +69,9 @@ class PostDetailController extends BasePostController {
|
|||||||
this._installView(e.detail.post, 'merge');
|
this._installView(e.detail.post, 'merge');
|
||||||
this._view.showSuccess('Post merged.');
|
this._view.showSuccess('Post merged.');
|
||||||
router.replace(
|
router.replace(
|
||||||
'/post/' + e.detail.targetPost.id + '/merge', null, false);
|
uri.formatClientLink(
|
||||||
|
'post', e.detail.targetPost.id, 'merge'),
|
||||||
|
null, false);
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view.showError(error.message);
|
this._view.showError(error.message);
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
@ -77,7 +81,7 @@ class PostDetailController extends BasePostController {
|
|||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter(
|
router.enter(
|
||||||
'/post/:id/merge',
|
['post', ':id', 'merge'],
|
||||||
(ctx, next) => {
|
(ctx, next) => {
|
||||||
ctx.controller = new PostDetailController(ctx, 'merge');
|
ctx.controller = new PostDetailController(ctx, 'merge');
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const settings = require('../models/settings.js');
|
const settings = require('../models/settings.js');
|
||||||
const misc = require('../util/misc.js');
|
const uri = require('../util/uri.js');
|
||||||
const PostList = require('../models/post_list.js');
|
const PostList = require('../models/post_list.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const PageController = require('../controllers/page_controller.js');
|
const PageController = require('../controllers/page_controller.js');
|
||||||
@ -11,7 +12,7 @@ const PostsPageView = require('../views/posts_page_view.js');
|
|||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'id', 'thumbnailUrl', 'type',
|
'id', 'thumbnailUrl', 'type', 'safety',
|
||||||
'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
|
'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
|
||||||
|
|
||||||
class PostListController {
|
class PostListController {
|
||||||
@ -31,8 +32,12 @@ class PostListController {
|
|||||||
this._headerView = new PostsHeaderView({
|
this._headerView = new PostsHeaderView({
|
||||||
hostNode: this._pageController.view.pageHeaderHolderNode,
|
hostNode: this._pageController.view.pageHeaderHolderNode,
|
||||||
parameters: ctx.parameters,
|
parameters: ctx.parameters,
|
||||||
canMassTag: api.hasPrivilege('tags:masstag'),
|
enableSafety: api.safetyEnabled(),
|
||||||
massTagTags: this._massTagTags,
|
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
|
||||||
|
canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
|
||||||
|
bulkEdit: {
|
||||||
|
tags: this._bulkEditTags
|
||||||
|
},
|
||||||
});
|
});
|
||||||
this._headerView.addEventListener(
|
this._headerView.addEventListener(
|
||||||
'navigate', e => this._evtNavigate(e));
|
'navigate', e => this._evtNavigate(e));
|
||||||
@ -44,68 +49,65 @@ class PostListController {
|
|||||||
this._pageController.showSuccess(message);
|
this._pageController.showSuccess(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
get _massTagTags() {
|
get _bulkEditTags() {
|
||||||
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
|
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtNavigate(e) {
|
_evtNavigate(e) {
|
||||||
history.pushState(
|
router.showNoDispatch(
|
||||||
null,
|
uri.formatClientLink('posts', e.detail.parameters));
|
||||||
window.title,
|
|
||||||
'/posts/' + misc.formatUrlParameters(e.detail.parameters));
|
|
||||||
Object.assign(this._ctx.parameters, e.detail.parameters);
|
Object.assign(this._ctx.parameters, e.detail.parameters);
|
||||||
this._syncPageController();
|
this._syncPageController();
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtTag(e) {
|
_evtTag(e) {
|
||||||
for (let tag of this._massTagTags) {
|
Promise.all(
|
||||||
e.detail.post.addTag(tag);
|
this._bulkEditTags.map(tag =>
|
||||||
}
|
e.detail.post.tags.addByName(tag)))
|
||||||
e.detail.post.save().catch(error => window.alert(error.message));
|
.then(e.detail.post.save())
|
||||||
|
.catch(error => window.alert(error.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtUntag(e) {
|
_evtUntag(e) {
|
||||||
for (let tag of this._massTagTags) {
|
for (let tag of this._bulkEditTags) {
|
||||||
e.detail.post.removeTag(tag);
|
e.detail.post.tags.removeByName(tag);
|
||||||
}
|
}
|
||||||
e.detail.post.save().catch(error => window.alert(error.message));
|
e.detail.post.save().catch(error => window.alert(error.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
_decorateSearchQuery(text) {
|
_evtChangeSafety(e) {
|
||||||
const browsingSettings = settings.get();
|
e.detail.post.safety = e.detail.safety;
|
||||||
let disabledSafety = [];
|
e.detail.post.save().catch(error => window.alert(error.message));
|
||||||
for (let key of Object.keys(browsingSettings.listPosts)) {
|
|
||||||
if (browsingSettings.listPosts[key] === false) {
|
|
||||||
disabledSafety.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (disabledSafety.length) {
|
|
||||||
text = `-rating:${disabledSafety.join(',')} ${text}`;
|
|
||||||
}
|
|
||||||
return text.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncPageController() {
|
_syncPageController() {
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: this._ctx.parameters,
|
parameters: this._ctx.parameters,
|
||||||
getClientUrlForPage: page => {
|
defaultLimit: parseInt(settings.get().postsPerPage),
|
||||||
return '/posts/' + misc.formatUrlParameters(
|
getClientUrlForPage: (offset, limit) => {
|
||||||
Object.assign({}, this._ctx.parameters, {page: page}));
|
const parameters = Object.assign(
|
||||||
|
{}, this._ctx.parameters, {offset: offset, limit: limit});
|
||||||
|
return uri.formatClientLink('posts', parameters);
|
||||||
},
|
},
|
||||||
requestPage: page => {
|
requestPage: (offset, limit) => {
|
||||||
return PostList.search(
|
return PostList.search(
|
||||||
this._decorateSearchQuery(this._ctx.parameters.query),
|
this._ctx.parameters.query, offset, limit, fields);
|
||||||
page, settings.get().postsPerPage, fields);
|
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
Object.assign(pageCtx, {
|
Object.assign(pageCtx, {
|
||||||
canViewPosts: api.hasPrivilege('posts:view'),
|
canViewPosts: api.hasPrivilege('posts:view'),
|
||||||
canMassTag: api.hasPrivilege('tags:masstag'),
|
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
|
||||||
massTagTags: this._massTagTags,
|
canBulkEditSafety:
|
||||||
|
api.hasPrivilege('posts:bulk-edit:safety'),
|
||||||
|
bulkEdit: {
|
||||||
|
tags: this._bulkEditTags,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const view = new PostsPageView(pageCtx);
|
const view = new PostsPageView(pageCtx);
|
||||||
view.addEventListener('tag', e => this._evtTag(e));
|
view.addEventListener('tag', e => this._evtTag(e));
|
||||||
view.addEventListener('untag', e => this._evtUntag(e));
|
view.addEventListener('untag', e => this._evtUntag(e));
|
||||||
|
view.addEventListener(
|
||||||
|
'changeSafety', e => this._evtChangeSafety(e));
|
||||||
return view;
|
return view;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -114,7 +116,6 @@ class PostListController {
|
|||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter(
|
router.enter(
|
||||||
'/posts/:parameters(.*)?',
|
['posts'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => { ctx.controller = new PostListController(ctx); });
|
(ctx, next) => { ctx.controller = new PostListController(ctx); });
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const settings = require('../models/settings.js');
|
const settings = require('../models/settings.js');
|
||||||
const Comment = require('../models/comment.js');
|
const Comment = require('../models/comment.js');
|
||||||
@ -19,8 +20,8 @@ class PostMainController extends BasePostController {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
Post.get(ctx.parameters.id),
|
Post.get(ctx.parameters.id),
|
||||||
PostList.getAround(
|
PostList.getAround(
|
||||||
ctx.parameters.id, this._decorateSearchQuery(
|
ctx.parameters.id,
|
||||||
parameters ? parameters.query : '')),
|
parameters ? parameters.query : null),
|
||||||
]).then(responses => {
|
]).then(responses => {
|
||||||
const [post, aroundResponse] = responses;
|
const [post, aroundResponse] = responses;
|
||||||
|
|
||||||
@ -29,8 +30,8 @@ class PostMainController extends BasePostController {
|
|||||||
if (parameters.query) {
|
if (parameters.query) {
|
||||||
ctx.state.parameters = parameters;
|
ctx.state.parameters = parameters;
|
||||||
const url = editMode ?
|
const url = editMode ?
|
||||||
'/post/' + ctx.parameters.id + '/edit' :
|
uri.formatClientLink('post', ctx.parameters.id, 'edit') :
|
||||||
'/post/' + ctx.parameters.id;
|
uri.formatClientLink('post', ctx.parameters.id);
|
||||||
router.replace(url, ctx.state, false);
|
router.replace(url, ctx.state, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,20 +91,6 @@ class PostMainController extends BasePostController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_decorateSearchQuery(text) {
|
|
||||||
const browsingSettings = settings.get();
|
|
||||||
let disabledSafety = [];
|
|
||||||
for (let key of Object.keys(browsingSettings.listPosts)) {
|
|
||||||
if (browsingSettings.listPosts[key] === false) {
|
|
||||||
disabledSafety.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (disabledSafety.length) {
|
|
||||||
text = `-rating:${disabledSafety.join(',')} ${text}`;
|
|
||||||
}
|
|
||||||
return text.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
_evtFitModeChange(e) {
|
_evtFitModeChange(e) {
|
||||||
const browsingSettings = settings.get();
|
const browsingSettings = settings.get();
|
||||||
browsingSettings.fitMode = e.detail.mode;
|
browsingSettings.fitMode = e.detail.mode;
|
||||||
@ -124,7 +111,7 @@ class PostMainController extends BasePostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_evtMergePost(e) {
|
_evtMergePost(e) {
|
||||||
router.show('/post/' + e.detail.post.id + '/merge');
|
router.show(uri.formatClientLink('post', e.detail.post.id, 'merge'));
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtDeletePost(e) {
|
_evtDeletePost(e) {
|
||||||
@ -133,7 +120,7 @@ class PostMainController extends BasePostController {
|
|||||||
e.detail.post.delete()
|
e.detail.post.delete()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
misc.disableExitConfirmation();
|
misc.disableExitConfirmation();
|
||||||
const ctx = router.show('/posts');
|
const ctx = router.show(uri.formatClientLink('posts'));
|
||||||
ctx.controller.showSuccess('Post deleted.');
|
ctx.controller.showSuccess('Post deleted.');
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view.sidebarControl.showError(error.message);
|
this._view.sidebarControl.showError(error.message);
|
||||||
@ -145,9 +132,6 @@ class PostMainController extends BasePostController {
|
|||||||
this._view.sidebarControl.disableForm();
|
this._view.sidebarControl.disableForm();
|
||||||
this._view.sidebarControl.clearMessages();
|
this._view.sidebarControl.clearMessages();
|
||||||
const post = e.detail.post;
|
const post = e.detail.post;
|
||||||
if (e.detail.tags !== undefined) {
|
|
||||||
post.tags = e.detail.tags;
|
|
||||||
}
|
|
||||||
if (e.detail.safety !== undefined) {
|
if (e.detail.safety !== undefined) {
|
||||||
post.safety = e.detail.safety;
|
post.safety = e.detail.safety;
|
||||||
}
|
}
|
||||||
@ -163,6 +147,9 @@ class PostMainController extends BasePostController {
|
|||||||
if (e.detail.thumbnail !== undefined) {
|
if (e.detail.thumbnail !== undefined) {
|
||||||
post.newThumbnail = e.detail.thumbnail;
|
post.newThumbnail = e.detail.thumbnail;
|
||||||
}
|
}
|
||||||
|
if (e.detail.source !== undefined) {
|
||||||
|
post.source = e.detail.source;
|
||||||
|
}
|
||||||
post.save()
|
post.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._view.sidebarControl.showSuccess('Post saved.');
|
this._view.sidebarControl.showSuccess('Post saved.');
|
||||||
@ -244,8 +231,7 @@ class PostMainController extends BasePostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/post/:id/edit/:parameters(.*)?',
|
router.enter(['post', ':id', 'edit'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => {
|
(ctx, next) => {
|
||||||
// restore parameters from history state
|
// restore parameters from history state
|
||||||
if (ctx.state.parameters) {
|
if (ctx.state.parameters) {
|
||||||
@ -254,8 +240,7 @@ module.exports = router => {
|
|||||||
ctx.controller = new PostMainController(ctx, true);
|
ctx.controller = new PostMainController(ctx, true);
|
||||||
});
|
});
|
||||||
router.enter(
|
router.enter(
|
||||||
'/post/:id/:parameters(.*)?',
|
['post', ':id'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => {
|
(ctx, next) => {
|
||||||
// restore parameters from history state
|
// restore parameters from history state
|
||||||
if (ctx.state.parameters) {
|
if (ctx.state.parameters) {
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const progress = require('../util/progress.js');
|
const progress = require('../util/progress.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const Post = require('../models/post.js');
|
const Post = require('../models/post.js');
|
||||||
|
const Tag = require('../models/tag.js');
|
||||||
const PostUploadView = require('../views/post_upload_view.js');
|
const PostUploadView = require('../views/post_upload_view.js');
|
||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
|
|
||||||
@ -28,6 +30,7 @@ class PostUploadController {
|
|||||||
this._view = new PostUploadView({
|
this._view = new PostUploadView({
|
||||||
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
|
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
|
||||||
canViewPosts: api.hasPrivilege('posts:view'),
|
canViewPosts: api.hasPrivilege('posts:view'),
|
||||||
|
enableSafety: api.safetyEnabled(),
|
||||||
});
|
});
|
||||||
this._view.addEventListener('change', e => this._evtChange(e));
|
this._view.addEventListener('change', e => this._evtChange(e));
|
||||||
this._view.addEventListener('submit', e => this._evtSubmit(e));
|
this._view.addEventListener('submit', e => this._evtSubmit(e));
|
||||||
@ -61,7 +64,7 @@ class PostUploadController {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
misc.disableExitConfirmation();
|
misc.disableExitConfirmation();
|
||||||
const ctx = router.show('/posts');
|
const ctx = router.show(uri.formatClientLink('posts'));
|
||||||
ctx.controller.showSuccess('Posts uploaded.');
|
ctx.controller.showSuccess('Posts uploaded.');
|
||||||
}, error => {
|
}, error => {
|
||||||
if (error.uploadable) {
|
if (error.uploadable) {
|
||||||
@ -95,16 +98,20 @@ class PostUploadController {
|
|||||||
return reverseSearchPromise.then(searchResult => {
|
return reverseSearchPromise.then(searchResult => {
|
||||||
if (searchResult) {
|
if (searchResult) {
|
||||||
// notify about exact duplicate
|
// notify about exact duplicate
|
||||||
if (searchResult.exactPost && !skipDuplicates) {
|
if (searchResult.exactPost) {
|
||||||
|
if (skipDuplicates) {
|
||||||
|
this._view.removeUploadable(uploadable);
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
let error = new Error('Post already uploaded ' +
|
let error = new Error('Post already uploaded ' +
|
||||||
`(@${searchResult.exactPost.id})`);
|
`(@${searchResult.exactPost.id})`);
|
||||||
error.uploadable = uploadable;
|
error.uploadable = uploadable;
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// notify about similar posts
|
// notify about similar posts
|
||||||
if (!searchResult.exactPost &&
|
if (searchResult.similarPosts.length) {
|
||||||
searchResult.similarPosts.length) {
|
|
||||||
let error = new Error(
|
let error = new Error(
|
||||||
`Found ${searchResult.similarPosts.length} similar ` +
|
`Found ${searchResult.similarPosts.length} similar ` +
|
||||||
'posts.\nYou can resume or discard this upload.');
|
'posts.\nYou can resume or discard this upload.');
|
||||||
@ -137,15 +144,22 @@ class PostUploadController {
|
|||||||
let post = new Post();
|
let post = new Post();
|
||||||
post.safety = uploadable.safety;
|
post.safety = uploadable.safety;
|
||||||
post.flags = uploadable.flags;
|
post.flags = uploadable.flags;
|
||||||
post.tags = uploadable.tags;
|
for (let tagName of uploadable.tags) {
|
||||||
|
const tag = new Tag();
|
||||||
|
tag.names = [tagName];
|
||||||
|
post.tags.add(tag);
|
||||||
|
}
|
||||||
post.relations = uploadable.relations;
|
post.relations = uploadable.relations;
|
||||||
post.newContent = uploadable.url || uploadable.file;
|
post.newContent = uploadable.url || uploadable.file;
|
||||||
|
// if uploadable.source is ever going to be a valid field (e.g when setting source directly in the upload window)
|
||||||
|
// you'll need to change the line below to `post.source = uploadable.source || uploadable.url;`
|
||||||
|
if (uploadable.url) post.source = uploadable.url;
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/upload', (ctx, next) => {
|
router.enter(['upload'], (ctx, next) => {
|
||||||
ctx.controller = new PostUploadController();
|
ctx.controller = new PostUploadController();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@ class SettingsController {
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/settings', (ctx, next) => {
|
router.enter(['settings'], (ctx, next) => {
|
||||||
ctx.controller = new SettingsController();
|
ctx.controller = new SettingsController();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const uri = require('../util/uri.js');
|
||||||
const SnapshotList = require('../models/snapshot_list.js');
|
const SnapshotList = require('../models/snapshot_list.js');
|
||||||
const PageController = require('../controllers/page_controller.js');
|
const PageController = require('../controllers/page_controller.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
@ -22,13 +22,14 @@ class SnapshotsController {
|
|||||||
this._pageController = new PageController();
|
this._pageController = new PageController();
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: ctx.parameters,
|
parameters: ctx.parameters,
|
||||||
getClientUrlForPage: page => {
|
defaultLimit: 25,
|
||||||
|
getClientUrlForPage: (offset, limit) => {
|
||||||
const parameters = Object.assign(
|
const parameters = Object.assign(
|
||||||
{}, ctx.parameters, {page: page});
|
{}, ctx.parameters, {offset: offset, limit: limit});
|
||||||
return '/history/' + misc.formatUrlParameters(parameters);
|
return uri.formatClientLink('history', parameters);
|
||||||
},
|
},
|
||||||
requestPage: page => {
|
requestPage: (offset, limit) => {
|
||||||
return SnapshotList.search('', page, 25);
|
return SnapshotList.search('', offset, limit);
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
Object.assign(pageCtx, {
|
Object.assign(pageCtx, {
|
||||||
@ -43,7 +44,6 @@ class SnapshotsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/history/:parameters?',
|
router.enter(['history'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
|
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@ class TagCategoriesController {
|
|||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
this._tagCategories.save()
|
this._tagCategories.save()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
tags.refreshExport();
|
tags.refreshCategoryColorMap();
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
this._view.showSuccess('Changes saved.');
|
this._view.showSuccess('Changes saved.');
|
||||||
}, error => {
|
}, error => {
|
||||||
@ -51,7 +51,7 @@ class TagCategoriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/tag-categories', (ctx, next) => {
|
router.enter(['tag-categories'], (ctx, next) => {
|
||||||
ctx.controller = new TagCategoriesController(ctx, next);
|
ctx.controller = new TagCategoriesController(ctx, next);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const tags = require('../tags.js');
|
const uri = require('../util/uri.js');
|
||||||
const Tag = require('../models/tag.js');
|
const Tag = require('../models/tag.js');
|
||||||
|
const TagCategoryList = require('../models/tag_category_list.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const TagView = require('../views/tag_view.js');
|
const TagView = require('../views/tag_view.js');
|
||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
@ -17,7 +18,12 @@ class TagController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tag.get(ctx.parameters.name).then(tag => {
|
Promise.all([
|
||||||
|
TagCategoryList.get(),
|
||||||
|
Tag.get(ctx.parameters.name),
|
||||||
|
]).then(responses => {
|
||||||
|
const [tagCategoriesResponse, tag] = responses;
|
||||||
|
|
||||||
topNavigation.activate('tags');
|
topNavigation.activate('tags');
|
||||||
topNavigation.setTitle('Tag #' + tag.names[0]);
|
topNavigation.setTitle('Tag #' + tag.names[0]);
|
||||||
|
|
||||||
@ -25,7 +31,7 @@ class TagController {
|
|||||||
tag.addEventListener('change', e => this._evtSaved(e, section));
|
tag.addEventListener('change', e => this._evtSaved(e, section));
|
||||||
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
for (let category of tags.getAllCategories()) {
|
for (let category of tagCategoriesResponse.results) {
|
||||||
categories[category.name] = category.name;
|
categories[category.name] = category.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +47,7 @@ class TagController {
|
|||||||
canMerge: api.hasPrivilege('tags:merge'),
|
canMerge: api.hasPrivilege('tags:merge'),
|
||||||
canDelete: api.hasPrivilege('tags:delete'),
|
canDelete: api.hasPrivilege('tags:delete'),
|
||||||
categories: categories,
|
categories: categories,
|
||||||
|
escapeColons: uri.escapeColons,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._view.addEventListener('change', e => this._evtChange(e));
|
this._view.addEventListener('change', e => this._evtChange(e));
|
||||||
@ -61,7 +68,8 @@ class TagController {
|
|||||||
misc.disableExitConfirmation();
|
misc.disableExitConfirmation();
|
||||||
if (this._name !== e.detail.tag.names[0]) {
|
if (this._name !== e.detail.tag.names[0]) {
|
||||||
router.replace(
|
router.replace(
|
||||||
'/tag/' + e.detail.tag.names[0] + '/' + section, null, false);
|
uri.formatClientLink('tag', e.detail.tag.names[0], section),
|
||||||
|
null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,12 +82,6 @@ class TagController {
|
|||||||
if (e.detail.category !== undefined) {
|
if (e.detail.category !== undefined) {
|
||||||
e.detail.tag.category = e.detail.category;
|
e.detail.tag.category = e.detail.category;
|
||||||
}
|
}
|
||||||
if (e.detail.implications !== undefined) {
|
|
||||||
e.detail.tag.implications = e.detail.implications;
|
|
||||||
}
|
|
||||||
if (e.detail.suggestions !== undefined) {
|
|
||||||
e.detail.tag.suggestions = e.detail.suggestions;
|
|
||||||
}
|
|
||||||
if (e.detail.description !== undefined) {
|
if (e.detail.description !== undefined) {
|
||||||
e.detail.tag.description = e.detail.description;
|
e.detail.tag.description = e.detail.description;
|
||||||
}
|
}
|
||||||
@ -95,11 +97,15 @@ class TagController {
|
|||||||
_evtMerge(e) {
|
_evtMerge(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
e.detail.tag.merge(e.detail.targetTagName).then(() => {
|
e.detail.tag
|
||||||
|
.merge(e.detail.targetTagName, e.detail.addAlias)
|
||||||
|
.then(() => {
|
||||||
this._view.showSuccess('Tag merged.');
|
this._view.showSuccess('Tag merged.');
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
router.replace(
|
router.replace(
|
||||||
'/tag/' + e.detail.targetTagName + '/merge', null, false);
|
uri.formatClientLink(
|
||||||
|
'tag', e.detail.targetTagName, 'merge'),
|
||||||
|
null, false);
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view.showError(error.message);
|
this._view.showError(error.message);
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
@ -111,7 +117,7 @@ class TagController {
|
|||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
e.detail.tag.delete()
|
e.detail.tag.delete()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const ctx = router.show('/tags/');
|
const ctx = router.show(uri.formatClientLink('tags'));
|
||||||
ctx.controller.showSuccess('Tag deleted.');
|
ctx.controller.showSuccess('Tag deleted.');
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view.showError(error.message);
|
this._view.showError(error.message);
|
||||||
@ -121,16 +127,16 @@ class TagController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/tag/:name(.+?)/edit', (ctx, next) => {
|
router.enter(['tag', ':name', 'edit'], (ctx, next) => {
|
||||||
ctx.controller = new TagController(ctx, 'edit');
|
ctx.controller = new TagController(ctx, 'edit');
|
||||||
});
|
});
|
||||||
router.enter('/tag/:name(.+?)/merge', (ctx, next) => {
|
router.enter(['tag', ':name', 'merge'], (ctx, next) => {
|
||||||
ctx.controller = new TagController(ctx, 'merge');
|
ctx.controller = new TagController(ctx, 'merge');
|
||||||
});
|
});
|
||||||
router.enter('/tag/:name(.+?)/delete', (ctx, next) => {
|
router.enter(['tag', ':name', 'delete'], (ctx, next) => {
|
||||||
ctx.controller = new TagController(ctx, 'delete');
|
ctx.controller = new TagController(ctx, 'delete');
|
||||||
});
|
});
|
||||||
router.enter('/tag/:name(.+)', (ctx, next) => {
|
router.enter(['tag', ':name'], (ctx, next) => {
|
||||||
ctx.controller = new TagController(ctx, 'summary');
|
ctx.controller = new TagController(ctx, 'summary');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const uri = require('../util/uri.js');
|
||||||
const TagList = require('../models/tag_list.js');
|
const TagList = require('../models/tag_list.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const PageController = require('../controllers/page_controller.js');
|
const PageController = require('../controllers/page_controller.js');
|
||||||
@ -10,7 +11,12 @@ const TagsPageView = require('../views/tags_page_view.js');
|
|||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'names', 'suggestions', 'implications', 'creationTime', 'usages'];
|
'names',
|
||||||
|
'suggestions',
|
||||||
|
'implications',
|
||||||
|
'creationTime',
|
||||||
|
'usages',
|
||||||
|
'category'];
|
||||||
|
|
||||||
class TagListController {
|
class TagListController {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
@ -46,10 +52,8 @@ class TagListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_evtNavigate(e) {
|
_evtNavigate(e) {
|
||||||
history.pushState(
|
router.showNoDispatch(
|
||||||
null,
|
uri.formatClientLink('tags', e.detail.parameters));
|
||||||
window.title,
|
|
||||||
'/tags/' + misc.formatUrlParameters(e.detail.parameters));
|
|
||||||
Object.assign(this._ctx.parameters, e.detail.parameters);
|
Object.assign(this._ctx.parameters, e.detail.parameters);
|
||||||
this._syncPageController();
|
this._syncPageController();
|
||||||
}
|
}
|
||||||
@ -57,14 +61,15 @@ class TagListController {
|
|||||||
_syncPageController() {
|
_syncPageController() {
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: this._ctx.parameters,
|
parameters: this._ctx.parameters,
|
||||||
getClientUrlForPage: page => {
|
defaultLimit: 50,
|
||||||
|
getClientUrlForPage: (offset, limit) => {
|
||||||
const parameters = Object.assign(
|
const parameters = Object.assign(
|
||||||
{}, this._ctx.parameters, {page: page});
|
{}, this._ctx.parameters, {offset: offset, limit: limit});
|
||||||
return '/tags/' + misc.formatUrlParameters(parameters);
|
return uri.formatClientLink('tags', parameters);
|
||||||
},
|
},
|
||||||
requestPage: page => {
|
requestPage: (offset, limit) => {
|
||||||
return TagList.search(
|
return TagList.search(
|
||||||
this._ctx.parameters.query, page, 50, fields);
|
this._ctx.parameters.query, offset, limit, fields);
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
return new TagsPageView(pageCtx);
|
return new TagsPageView(pageCtx);
|
||||||
@ -75,7 +80,6 @@ class TagListController {
|
|||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter(
|
router.enter(
|
||||||
'/tags/:parameters(.*)?',
|
['tags'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => { ctx.controller = new TagListController(ctx); });
|
(ctx, next) => { ctx.controller = new TagListController(ctx); });
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ const TopNavigationView = require('../views/top_navigation_view.js');
|
|||||||
|
|
||||||
class TopNavigationController {
|
class TopNavigationController {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
api.fetchConfig().then(() => {
|
||||||
this._topNavigationView = new TopNavigationView();
|
this._topNavigationView = new TopNavigationView();
|
||||||
|
|
||||||
topNavigation.addEventListener(
|
topNavigation.addEventListener(
|
||||||
@ -15,6 +16,7 @@ class TopNavigationController {
|
|||||||
api.addEventListener('logout', e => this._evtAuthChange(e));
|
api.addEventListener('logout', e => this._evtAuthChange(e));
|
||||||
|
|
||||||
this._render();
|
this._render();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtAuthChange(e) {
|
_evtAuthChange(e) {
|
||||||
@ -26,7 +28,7 @@ class TopNavigationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateNavigationFromPrivileges() {
|
_updateNavigationFromPrivileges() {
|
||||||
topNavigation.get('account').url = '/user/' + api.userName;
|
topNavigation.get('account').url = 'user/' + api.userName;
|
||||||
topNavigation.get('account').imageUrl =
|
topNavigation.get('account').imageUrl =
|
||||||
api.user ? api.user.avatarUrl : null;
|
api.user ? api.user.avatarUrl : null;
|
||||||
|
|
||||||
@ -47,10 +49,12 @@ class TopNavigationController {
|
|||||||
topNavigation.hide('users');
|
topNavigation.hide('users');
|
||||||
}
|
}
|
||||||
if (api.isLoggedIn()) {
|
if (api.isLoggedIn()) {
|
||||||
|
if (!api.hasPrivilege('users:create:any')) {
|
||||||
topNavigation.hide('register');
|
topNavigation.hide('register');
|
||||||
|
}
|
||||||
topNavigation.hide('login');
|
topNavigation.hide('login');
|
||||||
} else {
|
} else {
|
||||||
if (!api.hasPrivilege('users:create')) {
|
if (!api.hasPrivilege('users:create:self')) {
|
||||||
topNavigation.hide('register');
|
topNavigation.hide('register');
|
||||||
}
|
}
|
||||||
topNavigation.hide('account');
|
topNavigation.hide('account');
|
||||||
@ -62,6 +66,7 @@ class TopNavigationController {
|
|||||||
this._updateNavigationFromPrivileges();
|
this._updateNavigationFromPrivileges();
|
||||||
this._topNavigationView.render({
|
this._topNavigationView.render({
|
||||||
items: topNavigation.getAll(),
|
items: topNavigation.getAll(),
|
||||||
|
name: api.getName()
|
||||||
});
|
});
|
||||||
this._topNavigationView.activate(
|
this._topNavigationView.activate(
|
||||||
topNavigation.activeItem ? topNavigation.activeItem.key : '');
|
topNavigation.activeItem ? topNavigation.activeItem.key : '');
|
||||||
|
@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const misc = require('../util/misc.js');
|
const misc = require('../util/misc.js');
|
||||||
const config = require('../config.js');
|
|
||||||
const views = require('../util/views.js');
|
const views = require('../util/views.js');
|
||||||
const User = require('../models/user.js');
|
const User = require('../models/user.js');
|
||||||
|
const UserToken = require('../models/user_token.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const UserView = require('../views/user_view.js');
|
const UserView = require('../views/user_view.js');
|
||||||
const EmptyView = require('../views/empty_view.js');
|
const EmptyView = require('../views/empty_view.js');
|
||||||
@ -20,8 +21,28 @@ class UserController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._successMessages = [];
|
||||||
|
this._errorMessages = [];
|
||||||
|
|
||||||
|
let userTokenPromise = Promise.resolve([]);
|
||||||
|
if (section === 'list-tokens') {
|
||||||
|
userTokenPromise = UserToken.get(userName)
|
||||||
|
.then(userTokens => {
|
||||||
|
return userTokens.map(token => {
|
||||||
|
token.isCurrentAuthToken = api.isCurrentAuthToken(token);
|
||||||
|
return token;
|
||||||
|
});
|
||||||
|
}, error => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
topNavigation.setTitle('User ' + userName);
|
topNavigation.setTitle('User ' + userName);
|
||||||
User.get(userName).then(user => {
|
Promise.all([
|
||||||
|
userTokenPromise,
|
||||||
|
User.get(userName)
|
||||||
|
]).then(responses => {
|
||||||
|
const [userTokens, user] = responses;
|
||||||
const isLoggedIn = api.isLoggedIn(user);
|
const isLoggedIn = api.isLoggedIn(user);
|
||||||
const infix = isLoggedIn ? 'self' : 'any';
|
const infix = isLoggedIn ? 'self' : 'any';
|
||||||
|
|
||||||
@ -47,6 +68,7 @@ class UserController {
|
|||||||
} else {
|
} else {
|
||||||
topNavigation.activate('users');
|
topNavigation.activate('users');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._view = new UserView({
|
this._view = new UserView({
|
||||||
user: user,
|
user: user,
|
||||||
section: section,
|
section: section,
|
||||||
@ -57,18 +79,51 @@ class UserController {
|
|||||||
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
|
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
|
||||||
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
|
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
|
||||||
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
|
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
|
||||||
|
canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
|
||||||
|
canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
|
||||||
|
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
|
||||||
|
canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`),
|
||||||
canDelete: api.hasPrivilege(`users:delete:${infix}`),
|
canDelete: api.hasPrivilege(`users:delete:${infix}`),
|
||||||
ranks: ranks,
|
ranks: ranks,
|
||||||
|
tokens: userTokens,
|
||||||
});
|
});
|
||||||
this._view.addEventListener('change', e => this._evtChange(e));
|
this._view.addEventListener('change', e => this._evtChange(e));
|
||||||
this._view.addEventListener('submit', e => this._evtUpdate(e));
|
this._view.addEventListener('submit', e => this._evtUpdate(e));
|
||||||
this._view.addEventListener('delete', e => this._evtDelete(e));
|
this._view.addEventListener('delete', e => this._evtDelete(e));
|
||||||
|
this._view.addEventListener('create-token', e => this._evtCreateToken(e));
|
||||||
|
this._view.addEventListener('delete-token', e => this._evtDeleteToken(e));
|
||||||
|
this._view.addEventListener('update-token', e => this._evtUpdateToken(e));
|
||||||
|
|
||||||
|
for (let message of this._successMessages) {
|
||||||
|
this.showSuccess(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let message of this._errorMessages) {
|
||||||
|
this.showError(message);
|
||||||
|
}
|
||||||
|
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view = new EmptyView();
|
this._view = new EmptyView();
|
||||||
this._view.showError(error.message);
|
this._view.showError(error.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSuccess(message) {
|
||||||
|
if (typeof this._view === 'undefined') {
|
||||||
|
this._successMessages.push(message)
|
||||||
|
} else {
|
||||||
|
this._view.showSuccess(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
if (typeof this._view === 'undefined') {
|
||||||
|
this._errorMessages.push(message)
|
||||||
|
} else {
|
||||||
|
this._view.showError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_evtChange(e) {
|
_evtChange(e) {
|
||||||
misc.enableExitConfirmation();
|
misc.enableExitConfirmation();
|
||||||
}
|
}
|
||||||
@ -77,7 +132,8 @@ class UserController {
|
|||||||
misc.disableExitConfirmation();
|
misc.disableExitConfirmation();
|
||||||
if (this._name !== e.detail.user.name) {
|
if (this._name !== e.detail.user.name) {
|
||||||
router.replace(
|
router.replace(
|
||||||
'/user/' + e.detail.user.name + '/' + section, null, false);
|
uri.formatClientLink('user', e.detail.user.name, section),
|
||||||
|
null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +191,10 @@ class UserController {
|
|||||||
api.logout();
|
api.logout();
|
||||||
}
|
}
|
||||||
if (api.hasPrivilege('users:list')) {
|
if (api.hasPrivilege('users:list')) {
|
||||||
const ctx = router.show('/users');
|
const ctx = router.show(uri.formatClientLink('users'));
|
||||||
ctx.controller.showSuccess('Account deleted.');
|
ctx.controller.showSuccess('Account deleted.');
|
||||||
} else {
|
} else {
|
||||||
const ctx = router.show('/');
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showSuccess('Account deleted.');
|
ctx.controller.showSuccess('Account deleted.');
|
||||||
}
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
@ -146,16 +202,66 @@ class UserController {
|
|||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_evtCreateToken(e) {
|
||||||
|
this._view.clearMessages();
|
||||||
|
this._view.disableForm();
|
||||||
|
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
|
||||||
|
.then(response => {
|
||||||
|
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
||||||
|
ctx.controller.showSuccess('Token ' + response.token + ' created.');
|
||||||
|
}, error => {
|
||||||
|
this._view.showError(error.message);
|
||||||
|
this._view.enableForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtDeleteToken(e) {
|
||||||
|
this._view.clearMessages();
|
||||||
|
this._view.disableForm();
|
||||||
|
if (api.isCurrentAuthToken(e.detail.userToken)) {
|
||||||
|
router.show(uri.formatClientLink('logout'));
|
||||||
|
} else {
|
||||||
|
e.detail.userToken.delete(e.detail.user.name)
|
||||||
|
.then(() => {
|
||||||
|
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
||||||
|
ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
|
||||||
|
}, error => {
|
||||||
|
this._view.showError(error.message);
|
||||||
|
this._view.enableForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtUpdateToken(e) {
|
||||||
|
this._view.clearMessages();
|
||||||
|
this._view.disableForm();
|
||||||
|
|
||||||
|
if (e.detail.note !== undefined) {
|
||||||
|
e.detail.userToken.note = e.detail.note;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.detail.userToken.save(e.detail.user.name).then(response => {
|
||||||
|
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
||||||
|
ctx.controller.showSuccess('Token ' + response.token + ' updated.');
|
||||||
|
}, error => {
|
||||||
|
this._view.showError(error.message);
|
||||||
|
this._view.enableForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/user/:name', (ctx, next) => {
|
router.enter(['user', ':name'], (ctx, next) => {
|
||||||
ctx.controller = new UserController(ctx, 'summary');
|
ctx.controller = new UserController(ctx, 'summary');
|
||||||
});
|
});
|
||||||
router.enter('/user/:name/edit', (ctx, next) => {
|
router.enter(['user', ':name', 'edit'], (ctx, next) => {
|
||||||
ctx.controller = new UserController(ctx, 'edit');
|
ctx.controller = new UserController(ctx, 'edit');
|
||||||
});
|
});
|
||||||
router.enter('/user/:name/delete', (ctx, next) => {
|
router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
|
||||||
|
ctx.controller = new UserController(ctx, 'list-tokens');
|
||||||
|
});
|
||||||
|
router.enter(['user', ':name', 'delete'], (ctx, next) => {
|
||||||
ctx.controller = new UserController(ctx, 'delete');
|
ctx.controller = new UserController(ctx, 'delete');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
const misc = require('../util/misc.js');
|
const router = require('../router.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const UserList = require('../models/user_list.js');
|
const UserList = require('../models/user_list.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const PageController = require('../controllers/page_controller.js');
|
const PageController = require('../controllers/page_controller.js');
|
||||||
@ -38,10 +39,8 @@ class UserListController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_evtNavigate(e) {
|
_evtNavigate(e) {
|
||||||
history.pushState(
|
router.showNoDispatch(
|
||||||
null,
|
uri.formatClientLink('users', e.detail.parameters));
|
||||||
window.title,
|
|
||||||
'/users/' + misc.formatUrlParameters(e.detail.parameters));
|
|
||||||
Object.assign(this._ctx.parameters, e.detail.parameters);
|
Object.assign(this._ctx.parameters, e.detail.parameters);
|
||||||
this._syncPageController();
|
this._syncPageController();
|
||||||
}
|
}
|
||||||
@ -49,13 +48,15 @@ class UserListController {
|
|||||||
_syncPageController() {
|
_syncPageController() {
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: this._ctx.parameters,
|
parameters: this._ctx.parameters,
|
||||||
getClientUrlForPage: page => {
|
defaultLimit: 30,
|
||||||
|
getClientUrlForPage: (offset, limit) => {
|
||||||
const parameters = Object.assign(
|
const parameters = Object.assign(
|
||||||
{}, this._ctx.parameters, {page: page});
|
{}, this._ctx.parameters, {offset, offset, limit: limit});
|
||||||
return '/users/' + misc.formatUrlParameters(parameters);
|
return uri.formatClientLink('users', parameters);
|
||||||
},
|
},
|
||||||
requestPage: page => {
|
requestPage: (offset, limit) => {
|
||||||
return UserList.search(this._ctx.parameters.query, page);
|
return UserList.search(
|
||||||
|
this._ctx.parameters.query, offset, limit);
|
||||||
},
|
},
|
||||||
pageRenderer: pageCtx => {
|
pageRenderer: pageCtx => {
|
||||||
Object.assign(pageCtx, {
|
Object.assign(pageCtx, {
|
||||||
@ -69,7 +70,6 @@ class UserListController {
|
|||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter(
|
router.enter(
|
||||||
'/users/:parameters(.*)?',
|
['users'],
|
||||||
(ctx, next) => { misc.parseUrlParametersRoute(ctx, next); },
|
|
||||||
(ctx, next) => { ctx.controller = new UserListController(ctx); });
|
(ctx, next) => { ctx.controller = new UserListController(ctx); });
|
||||||
};
|
};
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const router = require('../router.js');
|
const router = require('../router.js');
|
||||||
const api = require('../api.js');
|
const api = require('../api.js');
|
||||||
|
const uri = require('../util/uri.js');
|
||||||
const User = require('../models/user.js');
|
const User = require('../models/user.js');
|
||||||
const topNavigation = require('../models/top_navigation.js');
|
const topNavigation = require('../models/top_navigation.js');
|
||||||
const RegistrationView = require('../views/registration_view.js');
|
const RegistrationView = require('../views/registration_view.js');
|
||||||
@ -9,7 +10,7 @@ const EmptyView = require('../views/empty_view.js');
|
|||||||
|
|
||||||
class UserRegistrationController {
|
class UserRegistrationController {
|
||||||
constructor() {
|
constructor() {
|
||||||
if (!api.hasPrivilege('users:create')) {
|
if (!api.hasPrivilege('users:create:self')) {
|
||||||
this._view = new EmptyView();
|
this._view = new EmptyView();
|
||||||
this._view.showError('Registration is closed.');
|
this._view.showError('Registration is closed.');
|
||||||
return;
|
return;
|
||||||
@ -28,12 +29,22 @@ class UserRegistrationController {
|
|||||||
user.name = e.detail.name;
|
user.name = e.detail.name;
|
||||||
user.email = e.detail.email;
|
user.email = e.detail.email;
|
||||||
user.password = e.detail.password;
|
user.password = e.detail.password;
|
||||||
|
const isLoggedIn = api.isLoggedIn();
|
||||||
user.save().then(() => {
|
user.save().then(() => {
|
||||||
|
if (isLoggedIn) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
api.forget();
|
api.forget();
|
||||||
return api.login(e.detail.name, e.detail.password, false);
|
return api.login(e.detail.name, e.detail.password, false);
|
||||||
|
}
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ctx = router.show('/');
|
if (isLoggedIn) {
|
||||||
|
const ctx = router.show(uri.formatClientLink('users'));
|
||||||
|
ctx.controller.showSuccess('User added!');
|
||||||
|
} else {
|
||||||
|
const ctx = router.show(uri.formatClientLink());
|
||||||
ctx.controller.showSuccess('Welcome aboard!');
|
ctx.controller.showSuccess('Welcome aboard!');
|
||||||
|
}
|
||||||
}, error => {
|
}, error => {
|
||||||
this._view.showError(error.message);
|
this._view.showError(error.message);
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
@ -42,7 +53,7 @@ class UserRegistrationController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = router => {
|
module.exports = router => {
|
||||||
router.enter('/register', (ctx, next) => {
|
router.enter(['register'], (ctx, next) => {
|
||||||
new UserRegistrationController();
|
new UserRegistrationController();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -28,10 +28,7 @@ class AutoCompleteControl {
|
|||||||
this._sourceInputNode = sourceInputNode;
|
this._sourceInputNode = sourceInputNode;
|
||||||
this._options = {};
|
this._options = {};
|
||||||
Object.assign(this._options, {
|
Object.assign(this._options, {
|
||||||
transform: null,
|
|
||||||
verticalShift: 2,
|
verticalShift: 2,
|
||||||
source: null,
|
|
||||||
addSpace: false,
|
|
||||||
maxResults: 15,
|
maxResults: 15,
|
||||||
getTextToFind: () => {
|
getTextToFind: () => {
|
||||||
const value = sourceInputNode.value;
|
const value = sourceInputNode.value;
|
||||||
@ -56,7 +53,7 @@ class AutoCompleteControl {
|
|||||||
this._isVisible = false;
|
this._isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfirmStrategy(text) {
|
replaceSelectedText(result, addSpace) {
|
||||||
const start = _getSelectionStart(this._sourceInputNode);
|
const start = _getSelectionStart(this._sourceInputNode);
|
||||||
let prefix = '';
|
let prefix = '';
|
||||||
let suffix = this._sourceInputNode.value.substring(start);
|
let suffix = this._sourceInputNode.value.substring(start);
|
||||||
@ -66,30 +63,25 @@ class AutoCompleteControl {
|
|||||||
prefix = this._sourceInputNode.value.substring(0, index + 1);
|
prefix = this._sourceInputNode.value.substring(0, index + 1);
|
||||||
middle = this._sourceInputNode.value.substring(index + 1);
|
middle = this._sourceInputNode.value.substring(index + 1);
|
||||||
}
|
}
|
||||||
this._sourceInputNode.value = prefix + text + ' ' + suffix.trimLeft();
|
this._sourceInputNode.value = (
|
||||||
if (!this._options.addSpace) {
|
prefix + result.toString() + ' ' + suffix.trimLeft());
|
||||||
|
if (!addSpace) {
|
||||||
this._sourceInputNode.value = this._sourceInputNode.value.trim();
|
this._sourceInputNode.value = this._sourceInputNode.value.trim();
|
||||||
}
|
}
|
||||||
this._sourceInputNode.focus();
|
this._sourceInputNode.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
_delete(text) {
|
_delete(result) {
|
||||||
if (this._options.transform) {
|
|
||||||
text = this._options.transform(text);
|
|
||||||
}
|
|
||||||
if (this._options.delete) {
|
if (this._options.delete) {
|
||||||
this._options.delete(text);
|
this._options.delete(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_confirm(text) {
|
_confirm(result) {
|
||||||
if (this._options.transform) {
|
|
||||||
text = this._options.transform(text);
|
|
||||||
}
|
|
||||||
if (this._options.confirm) {
|
if (this._options.confirm) {
|
||||||
this._options.confirm(text);
|
this._options.confirm(result);
|
||||||
} else {
|
} else {
|
||||||
this.defaultConfirmStrategy(text);
|
this.defaultConfirmStrategy(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +96,6 @@ class AutoCompleteControl {
|
|||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this._updateResults(textToFind);
|
this._updateResults(textToFind);
|
||||||
this._refreshList();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,15 +200,16 @@ class AutoCompleteControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateResults(textToFind) {
|
_updateResults(textToFind) {
|
||||||
|
this._options.getMatches(textToFind).then(matches => {
|
||||||
const oldResults = this._results.slice();
|
const oldResults = this._results.slice();
|
||||||
this._results =
|
this._results = matches.slice(0, this._options.maxResults);
|
||||||
this._options.getMatches(textToFind)
|
|
||||||
.slice(0, this._options.maxResults);
|
|
||||||
const oldResultsHash = JSON.stringify(oldResults);
|
const oldResultsHash = JSON.stringify(oldResults);
|
||||||
const newResultsHash = JSON.stringify(this._results);
|
const newResultsHash = JSON.stringify(this._results);
|
||||||
if (oldResultsHash !== newResultsHash) {
|
if (oldResultsHash !== newResultsHash) {
|
||||||
this._activeResult = -1;
|
this._activeResult = -1;
|
||||||
}
|
}
|
||||||
|
this._refreshList();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshList() {
|
_refreshList() {
|
||||||
|
@ -5,15 +5,21 @@ const views = require('../util/views.js');
|
|||||||
|
|
||||||
const template = views.getTemplate('file-dropper');
|
const template = views.getTemplate('file-dropper');
|
||||||
|
|
||||||
|
const KEY_RETURN = 13;
|
||||||
|
|
||||||
class FileDropperControl extends events.EventTarget {
|
class FileDropperControl extends events.EventTarget {
|
||||||
constructor(target, options) {
|
constructor(target, options) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._options = options;
|
this._options = options;
|
||||||
const source = template({
|
const source = template({
|
||||||
allowMultiple: this._options.allowMultiple,
|
extraText: options.extraText,
|
||||||
allowUrls: this._options.allowUrls,
|
allowMultiple: options.allowMultiple,
|
||||||
|
allowUrls: options.allowUrls,
|
||||||
|
lock: options.lock,
|
||||||
id: 'file-' + Math.random().toString(36).substring(7),
|
id: 'file-' + Math.random().toString(36).substring(7),
|
||||||
|
urlPlaceholder:
|
||||||
|
options.urlPlaceholder || 'Alternatively, paste an URL here.',
|
||||||
});
|
});
|
||||||
|
|
||||||
this._dropperNode = source.querySelector('.file-dropper');
|
this._dropperNode = source.querySelector('.file-dropper');
|
||||||
@ -21,7 +27,7 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
this._urlConfirmButtonNode = source.querySelector('button');
|
this._urlConfirmButtonNode = source.querySelector('button');
|
||||||
this._fileInputNode = source.querySelector('input[type=file]');
|
this._fileInputNode = source.querySelector('input[type=file]');
|
||||||
this._fileInputNode.style.display = 'none';
|
this._fileInputNode.style.display = 'none';
|
||||||
this._fileInputNode.multiple = this._options.allowMultiple || false;
|
this._fileInputNode.multiple = options.allowMultiple || false;
|
||||||
|
|
||||||
this._counter = 0;
|
this._counter = 0;
|
||||||
this._dropperNode.addEventListener(
|
this._dropperNode.addEventListener(
|
||||||
@ -36,8 +42,12 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
'change', e => this._evtFileChange(e));
|
'change', e => this._evtFileChange(e));
|
||||||
|
|
||||||
if (this._urlInputNode) {
|
if (this._urlInputNode) {
|
||||||
|
this._urlInputNode.addEventListener(
|
||||||
|
'keydown', e => this._evtUrlInputKeyDown(e));
|
||||||
|
}
|
||||||
|
if (this._urlConfirmButtonNode) {
|
||||||
this._urlConfirmButtonNode.addEventListener(
|
this._urlConfirmButtonNode.addEventListener(
|
||||||
'click', e => this._evtUrlConfirm(e));
|
'click', e => this._evtUrlConfirmButtonClick(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
this._originalHtml = this._dropperNode.innerHTML;
|
this._originalHtml = this._dropperNode.innerHTML;
|
||||||
@ -61,6 +71,10 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
|
|
||||||
_emitUrls(urls) {
|
_emitUrls(urls) {
|
||||||
urls = Array.from(urls).map(url => url.trim());
|
urls = Array.from(urls).map(url => url.trim());
|
||||||
|
if (this._options.lock) {
|
||||||
|
this._dropperNode.innerText =
|
||||||
|
urls.map(url => url.split(/\//).reverse()[0]).join(', ');
|
||||||
|
}
|
||||||
for (let url of urls) {
|
for (let url of urls) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return;
|
return;
|
||||||
@ -75,7 +89,7 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
|
|
||||||
_evtDragEnter(e) {
|
_evtDragEnter(e) {
|
||||||
this._dropperNode.classList.add('active');
|
this._dropperNode.classList.add('active');
|
||||||
counter++;
|
this._counter++;
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtDragLeave(e) {
|
_evtDragLeave(e) {
|
||||||
@ -105,7 +119,17 @@ class FileDropperControl extends events.EventTarget {
|
|||||||
this._emitFiles(e.dataTransfer.files);
|
this._emitFiles(e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
_evtUrlConfirm(e) {
|
_evtUrlInputKeyDown(e) {
|
||||||
|
if (e.which !== KEY_RETURN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
this._dropperNode.classList.remove('active');
|
||||||
|
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
|
||||||
|
this._urlInputNode.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtUrlConfirmButtonClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._dropperNode.classList.remove('active');
|
this._dropperNode.classList.remove('active');
|
||||||
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
|
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
|
||||||
|
@ -5,18 +5,23 @@ const views = require('../util/views.js');
|
|||||||
const optimizedResize = require('../util/optimized_resize.js');
|
const optimizedResize = require('../util/optimized_resize.js');
|
||||||
|
|
||||||
class PostContentControl {
|
class PostContentControl {
|
||||||
constructor(hostNode, post, viewportSizeCalculator) {
|
constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) {
|
||||||
this._post = post;
|
this._post = post;
|
||||||
this._viewportSizeCalculator = viewportSizeCalculator;
|
this._viewportSizeCalculator = viewportSizeCalculator;
|
||||||
this._hostNode = hostNode;
|
this._hostNode = hostNode;
|
||||||
this._template = views.getTemplate('post-content');
|
this._template = views.getTemplate('post-content');
|
||||||
|
|
||||||
|
let fitMode = settings.get().fitMode;
|
||||||
|
if (typeof fitFunctionOverride !== 'undefined') {
|
||||||
|
fitMode = fitFunctionOverride;
|
||||||
|
}
|
||||||
|
|
||||||
this._currentFitFunction = {
|
this._currentFitFunction = {
|
||||||
'fit-both': this.fitBoth,
|
'fit-both': this.fitBoth,
|
||||||
'fit-original': this.fitOriginal,
|
'fit-original': this.fitOriginal,
|
||||||
'fit-width': this.fitWidth,
|
'fit-width': this.fitWidth,
|
||||||
'fit-height': this.fitHeight,
|
'fit-height': this.fitHeight,
|
||||||
}[settings.get().fitMode] || this.fitBoth;
|
}[fitMode] || this.fitBoth;
|
||||||
|
|
||||||
this._install();
|
this._install();
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user