Compare commits

..

No commits in common. "master" and "2.4" have entirely different histories.
master ... 2.4

404 changed files with 12240 additions and 28749 deletions

5
.gitattributes vendored
View file

@ -1,5 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Shell scripts require LF
*.sh text eol=lf

View file

@ -1,108 +0,0 @@
name: Build Docker containers
on:
push:
branches:
- master
jobs:
build-client:
name: Build and push client/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_INFO=${{ env.build_info }}
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/client
-t "szurubooru/client:latest"
-t "szurubooru/client:${{ env.major_tag }}"
-t "szurubooru/client:${{ env.minor_tag }}"
./client
build-server:
name: Build and push server/ Docker container
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Determine metadata
run: |
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
echo "Build Info: ${BUILD_INFO}"
echo "Build Date: ${BUILD_DATE}"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build container
run: >
docker buildx build --push
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
--build-arg BUILD_DATE=${{ env.build_date }}
--build-arg SOURCE_COMMIT=$GITHUB_SHA
--build-arg DOCKER_REPO=szurubooru/server
-t "szurubooru/server:latest"
-t "szurubooru/server:${{ env.major_tag }}"
-t "szurubooru/server:${{ env.minor_tag }}"
./server

View file

@ -1,28 +0,0 @@
name: Run unit tests
on: [push, pull_request]
jobs:
test-server:
name: Run pytest for server/
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Build test container
run: >
docker buildx build --load
--platform linux/amd64 --target testing
-t test_container
./server
- name: Run unit tests
run: >
docker run --rm -t test_container
--color=no
--cov-report=term-missing:skip-covered
--cov=szurubooru
szurubooru/

3
.gitignore vendored
View file

@ -13,6 +13,3 @@ server/**/lib/
server/**/bin/ server/**/bin/
server/**/pyvenv.cfg server/**/pyvenv.cfg
__pycache__/ __pycache__/
data/
sql/

View file

@ -1,62 +0,0 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: mixed-line-ending
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.4.2
hooks:
- id: remove-tabs
- repo: https://github.com/psf/black
rev: '23.1.0'
hooks:
- id: black
files: 'server/'
types: [python]
language_version: python3.9
- repo: https://github.com/PyCQA/isort
rev: '5.12.0'
hooks:
- id: isort
files: 'server/'
types: [python]
exclude: server/szurubooru/migrations/env.py
additional_dependencies:
- toml
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
files: client/js/
exclude: client/js/.gitignore
args: ['--config', 'client/.prettierrc.yml']
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.33.0
hooks:
- id: eslint
files: client/js/
args: ['--fix']
additional_dependencies:
- eslint-config-prettier
- repo: https://github.com/PyCQA/flake8
rev: '6.0.0'
hooks:
- id: flake8
files: server/szurubooru/
additional_dependencies:
- flake8-print
args: ['--config=server/.flake8']
fail_fast: true
exclude: LICENSE.md

View file

@ -3,12 +3,12 @@
Szurubooru is an image board engine inspired by services such as Danbooru, Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
its roots in Polish language and has onomatopeic meaning of scraping or its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. 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 [yt-dlp](https://github.com/yt-dlp/yt-dlp) - 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](doc/API.md)) - Rich JSON REST API ([see documentation](doc/API.md))
@ -20,7 +20,6 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
- Tag suggestions - Tag suggestions
- Tag implications (adding a tag automatically adds another) - Tag implications (adding a tag automatically adds another)
- Tag aliases - Tag aliases
- Pools and pool categories
- Duplicate detection - Duplicate detection
- Post rating and favoriting; comment rating - Post rating and favoriting; comment rating
- Polished UI - Polished UI
@ -32,8 +31,7 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
It is recommended that you use Docker for deployment. It is recommended that you use Docker for deployment.
[See installation instructions.](doc/INSTALL.md) [See installation instructions.](doc/INSTALL.md)
More installation resources, as well as related projects can be found on the Users who wish to avoid using Docker may find the [old installation instructions](doc/LEGACY_INSTALL.md) helpful.
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
## Screenshots ## Screenshots

View file

@ -1,12 +0,0 @@
env:
browser: true
commonjs: true
es6: true
extends: 'prettier'
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- build.js
parserOptions:
ecmaVersion: 11

View file

@ -1,4 +0,0 @@
parser: babel
printWidth: 79
tabWidth: 4
quoteProps: consistent

View file

@ -1,4 +1,4 @@
FROM --platform=$BUILDPLATFORM node:lts as builder FROM node:9 as builder
WORKDIR /opt/app WORKDIR /opt/app
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
@ -11,7 +11,7 @@ ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS} RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
FROM --platform=$BUILDPLATFORM scratch as approot FROM scratch as approot
COPY docker-start.sh / COPY docker-start.sh /
@ -22,7 +22,7 @@ WORKDIR /var/www
COPY --from=builder /opt/app/public/ . COPY --from=builder /opt/app/public/ .
FROM nginx:alpine as release FROM nginx:alpine
RUN apk --no-cache add dumb-init RUN apk --no-cache add dumb-init
COPY --from=approot / / COPY --from=approot / /

View file

@ -4,30 +4,28 @@
// ------------------------------------------------- // -------------------------------------------------
const webapp_icons = [ const webapp_icons = [
{ name: 'android-chrome-192x192.png', size: 192 }, {name: 'android-chrome-192x192.png', size: 192},
{ name: 'android-chrome-512x512.png', size: 512 }, {name: 'android-chrome-512x512.png', size: 512},
{ name: 'apple-touch-icon.png', size: 180 }, {name: 'apple-touch-icon.png', size: 180},
{ name: 'mstile-150x150.png', size: 150 } {name: 'mstile-150x150.png', size: 150}
]; ];
const webapp_splash_screens = [ const webapp_splash_screens = [
{ w: 640, h: 1136, center: 320 }, {w: 640, h: 1136, center: 320},
{ w: 750, h: 1294, center: 375 }, {w: 750, h: 1294, center: 375},
{ w: 1125, h: 2436, center: 565 }, {w: 1125, h: 2436, center: 565},
{ w: 1242, h: 2148, center: 625 }, {w: 1242, h: 2148, center: 625},
{ w: 1536, h: 2048, center: 770 }, {w: 1536, h: 2048, center: 770},
{ w: 1668, h: 2224, center: 820 }, {w: 1668, h: 2224, center: 820},
{ w: 2048, h: 2732, center: 1024 } {w: 2048, h: 2732, center: 1024}
]; ];
const external_js = [ const external_js = [
'dompurify',
'js-cookie',
'marked',
'mousetrap',
'nprogress',
'superagent',
'underscore', 'underscore',
'superagent',
'mousetrap',
'js-cookie',
'nprogress',
]; ];
const app_manifest = { const app_manifest = {
@ -37,7 +35,7 @@ const app_manifest = {
src: baseUrl() + 'img/android-chrome-192x192.png', src: baseUrl() + 'img/android-chrome-192x192.png',
type: 'image/png', type: 'image/png',
sizes: '192x192' sizes: '192x192'
}, },
{ {
src: baseUrl() + 'img/android-chrome-512x512.png', src: baseUrl() + 'img/android-chrome-512x512.png',
type: 'image/png', type: 'image/png',
@ -57,11 +55,6 @@ 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 browserify = require('browserify');
const chokidar = require('chokidar');
const WebSocket = require('ws');
var PrettyError = require('pretty-error');
var pe = new PrettyError();
function readTextFile(path) { function readTextFile(path) {
return fs.readFileSync(path, 'utf-8'); return fs.readFileSync(path, 'utf-8');
@ -117,7 +110,7 @@ function bundleHtml() {
(match, number) => { return placeholders[number]; }); (match, number) => { return placeholders[number]; });
const functionText = underscore.template( const functionText = underscore.template(
templateText, { variable: 'ctx' }).source; templateText, {variable: 'ctx'}).source;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`); compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
} }
@ -136,7 +129,7 @@ function bundleCss() {
let css = ''; let css = '';
for (const file of glob.sync('./css/**/*.styl')) { for (const file of glob.sync('./css/**/*.styl')) {
css += stylus.render(readTextFile(file), { filename: file }); css += stylus.render(readTextFile(file), {filename: file});
} }
fs.writeFileSync('./public/css/app.min.css', minifyCss(css)); fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
if (process.argv.includes('--gzip')) { if (process.argv.includes('--gzip')) {
@ -153,69 +146,59 @@ function bundleCss() {
console.info('Bundled CSS'); console.info('Bundled CSS');
} }
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);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
function bundleVendorJs(compress) {
let b = browserify();
for (let lib of external_js) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled vendor JS');
});
}
function bundleAppJs(b, compress, callback) {
const file = './public/js/app.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled app JS');
callback();
});
}
function bundleJs() { function bundleJs() {
const browserify = require('browserify');
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);
b.bundle().pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
if (!process.argv.includes('--no-vendor-js')) { if (!process.argv.includes('--no-vendor-js')) {
bundleVendorJs(true); let b = browserify();
for (let lib of external_js) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
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 watchify = require('watchify'); let b = browserify({debug: process.argv.includes('--debug')});
let b = browserify({ debug: process.argv.includes('--debug') });
if (!process.argv.includes('--no-transpile')) { if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify'); b = b.transform('babelify');
} }
b = b.external(external_js).add(glob.sync('./js/**/*.js')); b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = !process.argv.includes('--debug'); const compress = !process.argv.includes('--debug');
bundleAppJs(b, compress, () => { }); const file = './public/js/app.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled app JS');
});
} }
} }
const environment = process.argv.includes('--watch') ? "development" : "production";
function bundleConfig() { function bundleConfig() {
function getVersion() { function getVersion() {
let build_info = process.env.BUILD_INFO; let build_info = process.env.BUILD_INFO;
@ -231,10 +214,9 @@ function bundleConfig() {
} }
const config = { const config = {
meta: { meta: {
version: getVersion(), version: getVersion(),
buildDate: new Date().toUTCString() buildDate: new Date().toUTCString()
}, }
environment: environment
}; };
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config)); fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
@ -243,6 +225,7 @@ function bundleConfig() {
function bundleBinaryAssets() { function bundleBinaryAssets() {
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png'); fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
fs.copyFileSync('./img/transparency_grid.png', './public/img/transparency_grid.png');
console.info('Copied images'); console.info('Copied images');
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2') fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
@ -271,31 +254,31 @@ function bundleWebAppFiles() {
Promise.all(webapp_icons.map(icon => { Promise.all(webapp_icons.map(icon => {
return Jimp.read('./img/app.png') return Jimp.read('./img/app.png')
.then(file => { .then(file => {
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER) file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name)); .write(path.join('./public/img/', icon.name));
});
}))
.then(() => {
console.info('Generated webapp icons');
}); });
}))
.then(() => {
console.info('Generated webapp icons');
});
Promise.all(webapp_splash_screens.map(dim => { Promise.all(webapp_splash_screens.map(dim => {
return Jimp.read('./img/splash.png') return Jimp.read('./img/splash.png')
.then(file => { .then(file => {
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER) file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF) .background(0xFFFFFFFF)
.contain(dim.w, dim.center, .contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h, .contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE) Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/', .write(path.join('./public/img/',
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png')); 'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
});
}))
.then(() => {
console.info('Generated splash screens');
}); });
}))
.then(() => {
console.info('Generated splash screens');
});
} }
function makeOutputDirs() { function makeOutputDirs() {
@ -314,111 +297,18 @@ function makeOutputDirs() {
} }
} }
function watch() {
let wss = new WebSocket.Server({ port: 8080 });
const liveReload = !process.argv.includes('--no-live-reload');
function emitReload() {
if (liveReload) {
console.log("Requesting live reload.")
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send("reload");
}
});
}
}
chokidar.watch('./fonts/**/*').on('change', () => {
try {
bundleBinaryAssets();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./img/**/*').on('change', () => {
try {
bundleWebAppFiles();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./html/**/*.tpl').on('change', () => {
try {
bundleHtml();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./css/**/*.styl').on('change', () => {
try {
bundleCss()
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
bundleBinaryAssets();
bundleWebAppFiles();
bundleCss();
bundleHtml();
bundleVendorJs(true);
let watchify = require('watchify');
let b = browserify({
debug: process.argv.includes('--debug'),
entries: ['js/main.js'],
cache: {},
packageCache: {},
});
b.plugin(watchify);
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = false;
function bundle(id) {
console.info("Rebundling app JS...");
let start = new Date();
bundleAppJs(b, compress, () => {
let end = new Date() - start;
console.info('Rebundled in %ds.', end / 1000)
emitReload();
});
}
b.on('update', bundle);
bundle();
}
// ------------------------------------------------- // -------------------------------------------------
console.log("Building for '" + environment + "' environment.");
makeOutputDirs(); makeOutputDirs();
bundleConfig(); bundleConfig();
if (process.argv.includes('--watch')) { bundleBinaryAssets();
watch(); bundleWebAppFiles();
} else { if (!process.argv.includes('--no-html')) {
if (!process.argv.includes('--no-binary-assets')) { bundleHtml();
bundleBinaryAssets(); }
} if (!process.argv.includes('--no-css')) {
if (!process.argv.includes('--no-web-app-files')) { bundleCss();
bundleWebAppFiles(); }
} if (!process.argv.includes('--no-js')) {
if (!process.argv.includes('--no-html')) { bundleJs();
bundleHtml();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
}
} }

View file

@ -1,17 +1,13 @@
$main-color = #24AADD $main-color = #24AADD
$window-color = white $window-color = white
$window-color-darktheme = #1a1a1a
$top-navigation-color = #F5F5F5 $top-navigation-color = #F5F5F5
$top-navigation-color-darktheme = #333333
$text-color = #111 $text-color = #111
$text-color-darktheme = #e6e6e6
$inactive-link-color = #888 $inactive-link-color = #888
$inactive-link-color-darktheme = #cccccc
$line-color = #DDD $line-color = #DDD
$inactive-tab-text-color = $inactive-link-color
$active-tab-text-color = $text-color
$active-tab-background-color = rgba(0, 0, 0, 0.06) $active-tab-background-color = rgba(0, 0, 0, 0.06)
$focused-tab-background-color = rgba(0, 0, 0, 0.03) $focused-tab-background-color = rgba(0, 0, 0, 0.03)
$active-tab-background-color-darktheme = rgba(255, 255, 255, 0.06)
$focused-tab-background-color-darktheme = rgba(255, 255, 255, 0.03)
$message-info-border-color = #BDF $message-info-border-color = #BDF
$message-info-background-color = #E3EFF9 $message-info-background-color = #E3EFF9
$message-error-border-color = #FCC $message-error-border-color = #FCC
@ -25,7 +21,6 @@ $input-good-background-color = #F5FFF5
$input-enabled-background-color = #FAFAFA $input-enabled-background-color = #FAFAFA
$input-enabled-border-color = #EEE $input-enabled-border-color = #EEE
$input-enabled-text-color = $text-color $input-enabled-text-color = $text-color
$input-enabled-text-color-darktheme = $text-color-darktheme
$input-disabled-background-color = #FAFAFA $input-disabled-background-color = #FAFAFA
$input-disabled-border-color = #EEE $input-disabled-border-color = #EEE
$input-disabled-text-color = #888 $input-disabled-text-color = #888
@ -40,6 +35,7 @@ $new-tag-background-color = #DFC
$new-tag-text-color = black $new-tag-text-color = black
$implied-tag-background-color = #FFC $implied-tag-background-color = #FFC
$implied-tag-text-color = black $implied-tag-text-color = black
$tag-suggestions-background-color = $window-color
$tag-suggestions-header-color = #EEE $tag-suggestions-header-color = #EEE
$tag-suggestions-border-color = #AAA $tag-suggestions-border-color = #AAA
$duplicate-tag-background-color = #FDC $duplicate-tag-background-color = #FDC
@ -61,4 +57,3 @@ $safety-sketchy = #F3D75F
$safety-unsafe = #F3985F $safety-unsafe = #F3985F
$scrollbar-thumb-color = $main-color $scrollbar-thumb-color = $main-color
$scrollbar-bg-color = $input-enabled-background-color $scrollbar-bg-color = $input-enabled-background-color
$transparency-grid-square-color = #F0F0F0

View file

@ -1,7 +1,5 @@
@import colors @import colors
$comment-header-background-color = $top-navigation-color $comment-header-background-color = $top-navigation-color
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD $comment-border-color = #DDD
.comment-container .comment-container
@ -83,7 +81,7 @@ $comment-border-color = #DDD
.edit, .delete, .score-container a, .nickname a .edit, .delete, .score-container a, .nickname a
&:not(.inactive) &:not(.inactive)
color: mix($main-color, $inactive-link-color) color: mix($main-color, $inactive-tab-text-color)
i i
margin-right: 0.3em margin-right: 0.3em
@ -114,23 +112,8 @@ $comment-border-color = #DDD
.messages .messages
margin: 1em 0 margin: 1em 0
.darktheme .comment-container .comment header
background: $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
.comment-content .comment-content
p
word-wrap: normal
word-break: break-word
ul, ol ul, ol
list-style-position: inside list-style-position: inside
margin: 1em 0 margin: 1em 0

View file

@ -1,6 +1,5 @@
@import colors @import colors
$comment-border-color = $top-navigation-color $comment-border-color = $top-navigation-color
$comment-border-color-darktheme = $top-navigation-color-darktheme
.global-comment-list .global-comment-list
text-align: left text-align: left
@ -47,8 +46,3 @@ $comment-border-color-darktheme = $top-navigation-color-darktheme
.comments-container .comments-container
width: 100% width: 100%
.darktheme .global-comment-list
&>ul
&>li
border-top: 3px solid $comment-border-color-darktheme

View file

@ -26,10 +26,6 @@ form:not(.horizontal)
font-size: 80% font-size: 80%
line-height: 120% line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal form.horizontal
display: inline-block display: inline-block
margin-bottom: 1em margin-bottom: 1em
@ -171,16 +167,6 @@ input[type=time]
background: $input-disabled-background-color background: $input-disabled-background-color
color: $input-disabled-text-color color: $input-disabled-text-color
.darktheme
input[type=date],
input[type=time]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
/* /*
@ -218,21 +204,6 @@ input[type=number]
background: $input-disabled-background-color background: $input-disabled-background-color
color: $input-disabled-text-color color: $input-disabled-text-color
.darktheme
select,
textarea,
input[type=text],
input[type=email],
input[type=password],
input[type=number]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
input[readonly], input[readonly],
input[readonly]+.radio, input[readonly]+.radio,
input[readonly]+.checkbox, input[readonly]+.checkbox,
@ -271,9 +242,8 @@ form.show-validation .input
outline: 0 outline: 0
border: 2px solid $input-good-border-color border: 2px solid $input-good-border-color
background: $input-good-background-color background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/* /*
* Buttons * Buttons
@ -340,10 +310,6 @@ input::-moz-focus-inner
button button
margin-left: 0.5em margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper input[type=file]:disabled+.file-dropper
cursor: default cursor: default
opacity: .5 opacity: .5
@ -353,6 +319,8 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active .file-dropper.active
border-color: $main-color border-color: $main-color
.autocomplete .autocomplete
position: absolute position: absolute
z-index: 10 z-index: 10
@ -377,10 +345,6 @@ input[type=file]:focus+.file-dropper,
.disabled .disabled
color: $inactive-link-color color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete .anticomplete
display: none display: none

View file

@ -1,11 +1,6 @@
@import colors @import colors
@import mixins @import mixins
$active-tab-text-color = $text-color
$active-tab-text-color-darktheme = $text-color-darktheme
$inactive-tab-text-color = $inactive-link-color
$inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */ /* latin */
@font-face @font-face
font-family: 'Open Sans'; font-family: 'Open Sans';
@ -33,10 +28,6 @@ body
@media (max-width: 1200px) @media (max-width: 1200px)
font-size: 0.95em font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3 h1, h2, h3
font-weight: normal font-weight: normal
margin-bottom: 1em margin-bottom: 1em
@ -71,11 +62,6 @@ a
.vim-nav-hint .vim-nav-hint
position: absolute position: absolute
visibility: hidden visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append a.append, span.append
margin-left: 1em margin-left: 1em
@ -116,19 +102,12 @@ form .fa-question-circle-o
>*:last-child >*:last-child
margin-bottom: 0 margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
hr hr
border: 0 border: 0
border-top: 1px solid $line-color border-top: 1px solid $line-color
margin: 1em 0 margin: 1em 0
padding: 0 padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav nav
ul ul
list-style-type: none list-style-type: none
@ -226,24 +205,6 @@ nav
@media (max-width: 1000px) @media (max-width: 1000px)
display: none display: none
.darktheme nav
&.buttons
ul
li:not(.active) a
color: $inactive-tab-text-color-darktheme
li:hover:not(.active) a
color: $active-tab-text-color-darktheme
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key a .access-key
text-decoration: underline text-decoration: underline
@ -268,18 +229,6 @@ a .access-key
border: 1px solid $message-success-border-color border: 1px solid $message-success-border-color
background: $message-success-background-color background: $message-success-background-color
.darktheme .messages
.message
&.info
border: 1px solid darken($message-info-border-color, 30%)
background: darken($message-info-background-color, 60%)
&.error
border: 1px solid darken($message-error-border-color, 30%)
background: darken($message-error-background-color, 60%)
&.success
border: 1px solid darken($message-success-border-color, 30%)
background: darken($message-success-background-color, 80%)
.thumbnail .thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */ /*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle vertical-align: middle
@ -290,14 +239,9 @@ a .access-key
width: 20px width: 20px
height: 20px height: 20px
&.empty &.empty
background-image: background-image: url('../img/transparency_grid.png')
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat background-repeat: repeat
background-size: 20px 20px background-size: initial
img img
opacity: 0 opacity: 0
width: 100% width: 100%

View file

@ -22,11 +22,3 @@
line-height: 2em line-height: 2em
.expander-content .expander-content
padding: 0.5em 0.5em 2em 0.5em padding: 0.5em 0.5em 2em 0.5em
.darktheme .expander
header
background: $active-tab-background-color-darktheme
a
color: mix($text-color-darktheme, $inactive-link-color-darktheme)
i
color: $inactive-link-color-darktheme

View file

@ -22,14 +22,6 @@
z-index: 1 z-index: 1
span span
position: relative position: relative
background: $window-color background: white
padding: 0 1em padding: 0 1em
z-index: 2 z-index: 2
.darktheme .pager
.page
.page-header
&:before
background: $top-navigation-color-darktheme
span
background: $window-color-darktheme

View file

@ -1,29 +0,0 @@
@import colors
.content-wrapper.pool-categories
width: 100%
max-width: 45em
table
border-spacing: 0
width: 100%
tr.default td
background: $default-pool-category-background-color
td, th
padding: .4em
&.color
input[type=text]
width: 8em
&.usages
text-align: center
&.remove, &.set-default
white-space: pre
th
white-space: nowrap
&:first-child
padding-left: 0
&:last-child
padding-right: 0
tfoot
display: none
form
width: auto

View file

@ -1,58 +0,0 @@
@import colors
div.pool-input
position: relative
.main-control
display: flex
input
flex: 5
button
flex: 1
margin: 0 0 0 0.5em
ul.compact-pools
width: 100%
margin: 0.5em 0 0 0
padding: 0
li
margin: 0
width: 100%
line-height: 140%
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
transition: background-color 0.5s linear
a
display: inline
a:focus
outline: 0
box-shadow: inset 0 0 0 2px $main-color
&.implication
background: $implied-pool-background-color
color: $implied-pool-text-color
&.new
background: $new-pool-background-color
color: $new-pool-text-color
&.duplicate
background: $duplicate-pool-background-color
color: $duplicate-pool-text-color
i
padding-right: 0.4em
div.pool-input, ul.compact-pools
.pool-usages, .pool-weight, .remove-pool
color: $inactive-link-color
unselectable()
.pool-usages, .pool-weight
font-size: 90%
.pool-usages, .pool-weight
margin-left: 0.7em
.remove-pool
margin-right: 0.5em
.darktheme
div.pool-input, ul.compact-pools
.pool-usages, .pool-weight, .remove-pool
color: $inactive-link-color-darktheme

View file

@ -1,63 +0,0 @@
@import colors
.pool-list
table
width: 100%
border-spacing: 0
text-align: left
line-height: 1.3em
tr:hover td
background: $top-navigation-color
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 84%
.post-count
text-align: center
width: 8%
.creation-time
text-align: center
width: 8%
white-space: pre
ul
list-style-type: none
margin: 0
padding: 0
display: inline
li
padding: 0
display: inline
&:not(:last-child):after
content: ', '
@media (max-width: 800px)
.posts
display: none
.darktheme .pool-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.pool-list-header
label
display: none !important
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .pool-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -1,33 +0,0 @@
#pool
width: 100%
max-width: 40em
h1
word-break: break-all
line-height: 130%
margin-top: 0
form
width: 100%
.pool-edit
textarea
height: 10em
.pool-summary
section
&.description
margin: 1.5em 0 0 0
&.details
vertical-align: top
padding-right: 0.5em
ul
margin: 0
padding: 0
list-style-type: none
li
display: inline
margin: 0
padding: 0
li:not(:last-of-type):after
content: ', '
ul:empty:after
content: '(none)'
section
margin-bottom: 1em

View file

@ -1,14 +1,6 @@
@import colors
.post-container .post-container
.post-content.transparency-grid img .post-content.transparency-grid img
background-image: background: url('../img/transparency_grid.png')
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-size: 20px 20px
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
text-align: center text-align: center
.post-content .post-content

View file

@ -70,7 +70,7 @@
height: 1em height: 1em
text-align: center text-align: center
line-height: 1em line-height: 1em
font-size: 2.2em font-size: 1.6em
&.tagged &.tagged
background: rgba(0, 230, 0, 0.7) background: rgba(0, 230, 0, 0.7)
&:after &:after
@ -114,36 +114,12 @@
&[data-disabled] &[data-disabled]
background: rgba(200, 200, 200, 0.7) background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail .thumbnail
background-position: 50% 30%
width: 100% width: 100%
height: 100% height: 100%
outline-offset: -3px outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags .thumbnail-wrapper.no-tags
.thumbnail .thumbnail
@ -158,22 +134,6 @@
.thumbnail .thumbnail
outline: 4px solid $main-color !important outline: 4px solid $main-color !important
.post-flow
ul
li
min-width: inherit
width: inherit
&:not(.flexbox-dummy)
height: 14vw
.thumbnail
outline-offset: -1px
.thumbnail-wrapper.no-tags
.thumbnail
outline: 2px solid $post-thumbnail-no-tags-border-color
&:hover a, a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important
.post-list-header .post-list-header
white-space: nowrap white-space: nowrap
text-align: left text-align: left
@ -187,9 +147,6 @@
vertical-align: top vertical-align: top
@media (max-width: 1000px) @media (max-width: 1000px)
display: block display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input input
margin-bottom: 0.25em margin-bottom: 0.25em
margin-right: 0.25em margin-right: 0.25em
@ -225,7 +182,7 @@
.hint .hint
display: none display: none
input[name=tag] input[name=tag]
width: 24em width: 12em
@media (max-width: 1000px) @media (max-width: 1000px)
display: block display: block
width: 100% width: 100%
@ -241,19 +198,7 @@
.append .append
@media (max-width: 1000px) @media (max-width: 1000px)
margin-left: 0 margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety .safety
margin-right: 0.25em margin-right: 0.25em
&.safety-safe &.safety-safe

View file

@ -7,61 +7,47 @@
>.sidebar >.sidebar
margin-right: 1em margin-right: 1em
min-width: 21em min-width: 20em
max-width: 21em max-width: 20em
line-height: 160% line-height: 160%
a:active a:active
border: 0 border: 0
outline: 0 outline: 0
>.sidebar>nav.buttons, >.content nav.buttons nav.buttons
margin-top: 0 margin-top: 0
display: flex display: flex
flex-wrap: wrap flex-wrap: wrap
article article
flex: 1 0 33% flex: 1 0 33%
a a
display: inline-block display: inline-block
width: 100% width: 100%
padding: 0.3em 0 padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center text-align: center
vertical-align: middle @media (max-width: 800px)
transition: background 0.2s linear, box-shadow 0.2s linear margin-top: 2em
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
>.content >.content
width: 100% width: 100%
.post-container .post-container
margin-bottom: 0.6em margin-bottom: 2em
.post-content .post-content
margin: 0 margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
background: unset
box-shadow: inset 0 0 0 0.3em $main-color
@media (max-width: 800px) @media (max-width: 800px)
.post-view .post-view
flex-wrap: wrap flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar >.sidebar
order: 2 order: 2
min-width: 100% min-width: 100%
@ -119,6 +105,7 @@
h1 h1
margin-bottom: 0.5em margin-bottom: 0.5em
.thumbnail .thumbnail
background-position: 50% 30%
width: 4em width: 4em
height: 3em height: 3em
li li

View file

@ -1,6 +1,5 @@
@import colors @import colors
$upload-header-background-color = $top-navigation-color $upload-header-background-color = $top-navigation-color
$upload-header-background-color-darktheme = $top-navigation-color-darktheme
$upload-border-color = #DDD $upload-border-color = #DDD
$cancel-button-color = tomato $cancel-button-color = tomato
@ -13,12 +12,8 @@ $cancel-button-color = tomato
&.inactive input[type=submit], &.inactive input[type=submit],
&.inactive .skip-duplicates &.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit], &.uploading input[type=submit],
&.uploading .skip-duplicates, &.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel &:not(.uploading) .cancel
display: none display: none
@ -43,12 +38,6 @@ $cancel-button-color = tomato
.skip-duplicates .skip-duplicates
margin-left: 1em margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages form>.messages
margin-top: 1em margin-top: 1em
@ -62,14 +51,6 @@ $cancel-button-color = tomato
margin: 0 0 1.2em 0 margin: 0 0 1.2em 0
padding-left: 13em padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper &>.thumbnail-wrapper
float: left float: left
width: 12em width: 12em
@ -168,15 +149,3 @@ $cancel-button-color = tomato
color: $inactive-link-color color: $inactive-link-color
&:last-child .move-down &:last-child .move-down
color: $inactive-link-color color: $inactive-link-color
.darktheme &:first-child .move-up
color: $inactive-link-color-darktheme
.darktheme &:last-child .move-down
color: $inactive-link-color-darktheme
.darktheme #post-upload .uploadables-container .uploadable-container
.uploadable header
background: $upload-header-background-color-darktheme
&:first-child .move-up
color: $inactive-link-color-darktheme
&:last-child .move-down
color: $inactive-link-color-darktheme

View file

@ -31,34 +31,16 @@ $snapshot-merged-background-color = #FEC
div.operation-created div.operation-created
background: $snapshot-created-background-color background: $snapshot-created-background-color
&+.details &+.details
background: alpha(@background, 50%) background: lighten($snapshot-created-background-color, 50%)
div.operation-modified div.operation-modified
background: $snapshot-modified-background-color background: $snapshot-modified-background-color
&+.details &+.details
background: alpha(@background, 50%) background: lighten($snapshot-modified-background-color, 50%)
div.operation-deleted div.operation-deleted
background: $snapshot-deleted-background-color background: $snapshot-deleted-background-color
&+.details &+.details
background: alpha(@background, 50%) background: lighten($snapshot-deleted-background-color, 50%)
div.operation-merged div.operation-merged
background: $snapshot-merged-background-color background: $snapshot-merged-background-color
&+.details &+.details
background: alpha(@background, 50%) background: lighten($snapshot-merged-background-color, 50%)
.darktheme .snapshot-list ul li
div.operation-created
background: darken($snapshot-created-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-modified
background: darken($snapshot-modified-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-deleted
background: darken($snapshot-deleted-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-merged
background: darken($snapshot-merged-background-color, 80%)
&+.details
background: alpha(@background, 50%)

View file

@ -27,3 +27,4 @@
display: none display: none
form form
width: auto width: auto

View file

@ -46,7 +46,7 @@ div.tag-input
.wrapper .wrapper
margin-left: 0.5em margin-left: 0.5em
background: $window-color background: $tag-suggestions-background-color
border: 1px solid $tag-suggestions-border-color border: 1px solid $tag-suggestions-border-color
width: 15em width: 15em
word-break: break-all word-break: break-all
@ -62,7 +62,7 @@ div.tag-input
max-height: 20em max-height: 20em
padding: 0.5em 1em 0 1em padding: 0.5em 1em 0 1em
li:last-child li:last-child
border-bottom: 0.5em solid alpha($window-color, 0) border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0)
li li
margin: 0 margin: 0
font-size: 90% font-size: 90%
@ -86,12 +86,6 @@ div.tag-input
font-size: 90% font-size: 90%
unselectable() unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags ul.compact-tags
width: 100% width: 100%
margin: 0.5em 0 0 0 margin: 0.5em 0 0 0
@ -109,30 +103,18 @@ ul.compact-tags
a:focus a:focus
outline: 0 outline: 0
box-shadow: inset 0 0 0 2px $main-color box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication &.implication
background: $implied-tag-background-color
color: $implied-tag-text-color color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new &.new
background: $new-tag-background-color
color: $new-tag-text-color color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate &.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i i
padding-right: 0.4em padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag .tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color color: $inactive-link-color
@ -143,19 +125,3 @@ div.tag-input, ul.compact-tags
margin-left: 0.7em margin-left: 0.7em
.remove-tag .remove-tag
margin-right: 0.5em margin-right: 0.5em
.darktheme
div.tag-input .tag-suggestions
.buttons a
color: $inactive-link-color-darktheme
.wrapper
background: $window-color-darktheme
ul li:last-child
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
p
background: darken($tag-suggestions-header-color, 80%)
.append
color: $inactive-link-color-darktheme
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color-darktheme

View file

@ -40,13 +40,6 @@
.implications, .suggestions .implications, .suggestions
display: none display: none
.darktheme .tag-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.tag-list-header .tag-list-header
label label
display: none !important display: none !important
@ -61,7 +54,3 @@
vertical-align: middle vertical-align: middle
font-size: 0.95em font-size: 0.95em
color: $inactive-link-color color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -21,15 +21,10 @@
.details .details
font-size: 90% font-size: 90%
line-height: 130% line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail .thumbnail
width: 3em width: 3em
height: 3em height: 3em
margin: 0.25em 0.6em 0 0
.darktheme .user-list
ul li
background: $top-navigation-color-darktheme
.user-list-header .user-list-header
label label
@ -45,7 +40,3 @@
vertical-align: middle vertical-align: middle
font-size: 0.95em font-size: 0.95em
color: $inactive-link-color color: $inactive-link-color
.darktheme .user-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -2,10 +2,10 @@
# Integrate environment variables # Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \ sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf /etc/nginx/nginx.conf
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \ sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/index.htm \ /var/www/index.htm \
/var/www/manifest.json /var/www/manifest.json
# Start server # Start server
exec nginx exec nginx

16
client/hooks/build Executable file
View 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
View 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

View file

@ -4,7 +4,6 @@
--><li data-name='posts'><a href='<%- ctx.formatClientLink('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='<%- ctx.formatClientLink('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='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!-- --><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
--><li data-name='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</li><!--
--></ul><!-- --></ul><!--
--></nav> --></nav>

View file

@ -1,97 +0,0 @@
<p><strong>Anonymous tokens</strong></p>
<p>Same as <code>name</code> token.</p>
<p><strong>Named tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>name</code></td>
<td>having given name (accepts wildcards)</td>
</tr>
<tr>
<td><code>category</code></td>
<td>having given category (accepts wildcards)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>created at given date</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>edited at given date</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>last-edit-date</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>alias of <code>usages</code></td>
</tr>
</tbody>
</table>
<p><strong>Sort style tokens</strong></p>
<table>
<tbody>
<tr>
<td><code>random</code></td>
<td>as random as it can get</td>
</tr>
<tr>
<td><code>name</code></td>
<td>A to Z</td>
</tr>
<tr>
<td><code>category</code></td>
<td>category (A to Z)</td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>recently created first</td>
</tr>
<tr>
<td><code>creation-time</code></td>
<td>alias of <code>creation-date</code></td>
</tr>
<tr>
<td><code>last-edit-date</code></td>
<td>recently edited first</td>
</tr>
<tr>
<td><code>last-edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-date</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>edit-time</code></td>
<td>alias of <code>creation-time</code></td>
</tr>
<tr>
<td><code>post-count</code></td>
<td>number of posts</td>
</tr>
</tbody>
</table>
<p><strong>Special tokens</strong></p>
<p>None.</p>

View file

@ -20,15 +20,15 @@
</tr> </tr>
<tr> <tr>
<td><code>uploader</code></td> <td><code>uploader</code></td>
<td>uploaded by given user (accepts wildcards)</td> <td>uploaded by given use (accepts wildcards)r</td>
</tr> </tr>
<tr> <tr>
<td><code>upload</code></td> <td><code>upload</code></td>
<td>alias of <code>uploader</code></td> <td>alias of <code>upload</code></td>
</tr> </tr>
<tr> <tr>
<td><code>submit</code></td> <td><code>submit</code></td>
<td>alias of <code>uploader</code></td> <td>alias of <code>upload</code></td>
</tr> </tr>
<tr> <tr>
<td><code>comment</code></td> <td><code>comment</code></td>
@ -42,10 +42,6 @@
<td><code>source</code></td> <td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td> <td>having given source URL (accepts wildcards)</td>
</tr> </tr>
<tr>
<td><code>pool</code></td>
<td>belonging to the pool with the given ID</td>
</tr>
<tr> <tr>
<td><code>tag-count</code></td> <td><code>tag-count</code></td>
<td>having given number of tags</td> <td>having given number of tags</td>
@ -82,17 +78,9 @@
<td><code>flag</code></td> <td><code>flag</code></td>
<td>having given flag. <code>&lt;value&gt;</code> can be either <code>loop</code> or <code>sound</code>.</td> <td>having given flag. <code>&lt;value&gt;</code> can be either <code>loop</code> or <code>sound</code>.</td>
</tr> </tr>
<tr>
<td><code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>md5</code></td>
<td>having given MD5 checksum</td>
</tr>
<tr> <tr>
<td><code>content-checksum</code></td> <td><code>content-checksum</code></td>
<td>alias of <code>sha1</code></td> <td>having given SHA1 checksum</td>
</tr> </tr>
<tr> <tr>
<td><code>file-size</code></td> <td><code>file-size</code></td>

View file

@ -1,7 +1,7 @@
<ul> <ul>
<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><%- ctx.isDevelopmentMode ? " (DEV MODE)" : "" %> 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='<%- ctx.formatClientLink('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>

View file

@ -2,7 +2,7 @@
<html> <html>
<head> <head>
<meta charset='utf-8'/> <meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'> <meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
<meta name='theme-color' content='#24aadd'/> <meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/> <meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/> <meta name='apple-mobile-web-app-status-bar-style' content='black'/>

View file

@ -1,18 +0,0 @@
<div class='content-wrapper' id='pool'>
<h1><%- ctx.getPrettyName(ctx.pool.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'edit') %>'>Edit</a></li><!--
--><% } %><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'merge') %>'>Merge with&hellip;</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('pool', ctx.pool.id, 'delete') %>'>Delete</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>
<div class='pool-content-holder'></div>
</div>

View file

@ -1,30 +0,0 @@
<div class='content-wrapper pool-categories'>
<form>
<h1>Pool categories</h1>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class='name'>Category name</th>
<th class='color'>CSS color</th>
<th class='usages'>Usages</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<% if (ctx.canCreate) { %>
<p><a href class='add'>Add new category</a></p>
<% } %>
<div class='messages'></div>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>
<% } %>
</form>
</div>

View file

@ -1,43 +0,0 @@
<% if (ctx.poolCategory.isDefault) { %><%
%><tr data-category='<%- ctx.poolCategory.name %>' class='default'><%
%><% } else { %><%
%><tr data-category='<%- ctx.poolCategory.name %>'><%
%><% } %>
<td class='name'>
<% if (ctx.canEditName) { %>
<%= ctx.makeTextInput({value: ctx.poolCategory.name, required: true}) %>
<% } else { %>
<%- ctx.poolCategory.name %>
<% } %>
</td>
<td class='color'>
<% if (ctx.canEditColor) { %>
<%= ctx.makeColorInput({value: ctx.poolCategory.color}) %>
<% } else { %>
<%- ctx.poolCategory.color %>
<% } %>
</td>
<td class='usages'>
<% if (ctx.poolCategory.name) { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'category:' + ctx.poolCategory.name}) %>'>
<%- ctx.poolCategory.poolCount %>
</a>
<% } else { %>
<%- ctx.poolCategory.poolCount %>
<% } %>
</td>
<% if (ctx.canDelete) { %>
<td class='remove'>
<% if (ctx.poolCategory.poolCount) { %>
<a class='inactive' title="Can't delete category in use">Remove</a>
<% } else { %>
<a href>Remove</a>
<% } %>
</td>
<% } %>
<% if (ctx.canSetDefault) { %>
<td class='set-default'>
<a href>Make default</a>
</td>
<% } %>
</tr>

View file

@ -1,42 +0,0 @@
<div class='content-wrapper pool-create'>
<form>
<ul class='input'>
<li class='names'>
<%= ctx.makeTextInput({
text: 'Names',
value: '',
required: true,
}) %>
</li>
<li class='category'>
<%= ctx.makeSelect({
text: 'Category',
keyValues: ctx.categories,
selectedKey: 'default',
required: true,
}) %>
</li>
<li class='description'>
<%= ctx.makeTextarea({
text: 'Description',
value: '',
}) %>
</li>
<li class='posts'>
<%= ctx.makeTextInput({
text: 'Posts',
value: '',
placeholder: 'space-separated post IDs',
}) %>
</li>
</ul>
<% if (ctx.canCreate) { %>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' class='save' value='Create pool'>
</div>
<% } %>
</form>
</div>

View file

@ -1,21 +0,0 @@
<div class='pool-delete'>
<form>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
name: 'confirm-deletion',
text: 'I confirm that I want to delete this pool.',
required: true,
}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Delete pool'/>
</div>
</form>
</div>

View file

@ -1,50 +0,0 @@
<div class='content-wrapper pool-edit'>
<form>
<ul class='input'>
<li class='names'>
<% if (ctx.canEditNames) { %>
<%= ctx.makeTextInput({
text: 'Names',
value: ctx.pool.names.join(' '),
required: true,
}) %>
<% } %>
</li>
<li class='category'>
<% if (ctx.canEditCategory) { %>
<%= ctx.makeSelect({
text: 'Category',
keyValues: ctx.categories,
selectedKey: ctx.pool.category,
required: true,
}) %>
<% } %>
</li>
<li class='description'>
<% if (ctx.canEditDescription) { %>
<%= ctx.makeTextarea({
text: 'Description',
value: ctx.pool.description,
}) %>
<% } %>
</li>
<li class='posts'>
<% if (ctx.canEditPosts) { %>
<%= ctx.makeTextInput({
text: 'Posts',
placeholder: 'space-separated post IDs',
value: ctx.pool.posts.map(post => post.id).join(' ')
}) %>
<% } %>
</li>
</ul>
<% if (ctx.canEditAnything) { %>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>
<% } %>
</form>
</div>

View file

@ -1,7 +0,0 @@
<div class='pool-input'>
<div class='main-control'>
<input type='text' placeholder='type to add…'/>
</div>
<ul class='compact-pools'></ul>
</div>

View file

@ -1,22 +0,0 @@
<div class='pool-merge'>
<form>
<ul class='input'>
<li class='target'>
<%= ctx.makeTextInput({name: 'target-pool', required: true, text: 'Target pool', pattern: ctx.poolNamePattern}) %>
</li>
<li>
<p>Posts in the two pools will be combined.
Category needs to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this pool.'}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Merge pool'/>
</div>
</form>
</div>

View file

@ -1,23 +0,0 @@
<div class='content-wrapper pool-summary'>
<section class='details'>
<section>
Category:
<span class='<%= ctx.makeCssName(ctx.pool.category, 'pool') %>'><%- ctx.pool.category %></span>
</section>
<section>
Aliases:<br/>
<ul><!--
--><% for (let name of ctx.pool.names.slice(1)) { %><!--
--><li><%= ctx.makePoolLink(ctx.pool.id, false, false, ctx.pool, name) %></li><!--
--><% } %><!--
--></ul>
</section>
</section>
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.pool.description || 'This pool has no description yet.') %>
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
</section>
</div>

View file

@ -1,22 +0,0 @@
<div class='pool-list-header'>
<form class='horizontal'>
<ul class='input'>
<li>
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
</li>
</ul>
<div class='buttons'>
<input type='submit' value='Search'/>
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Syntax help</a>
<% if (ctx.canCreate) { %>
<a class='append' href='<%- ctx.formatClientLink('pool', 'create') %>'>Add new pool</a>
<% } %>
<% if (ctx.canEditPoolCategories) { %>
<a class='append' href='<%- ctx.formatClientLink('pool-categories') %>'>Pool categories</a>
<% } %>
</div>
</form>
</div>

View file

@ -1,48 +0,0 @@
<div class='pool-list table-wrap'>
<% if (ctx.response.results.length) { %>
<table>
<thead>
<th class='names'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:name'}) %>'>Pool name(s)</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:name'}) %>'>Pool name(s)</a>
<% } %>
</th>
<th class='post-count'>
<% if (ctx.parameters.query == 'sort:post-count') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:post-count'}) %>'>Post count</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:post-count'}) %>'>Post count</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('pools', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('pools', {query: 'sort:creation-time'}) %>'>Created on</a>
<% } %>
</th>
</thead>
<tbody>
<% for (let pool of ctx.response.results) { %>
<tr>
<td class='names'>
<ul>
<% for (let name of pool.names) { %>
<li><%= ctx.makePoolLink(pool.id, false, false, pool, name) %></li>
<% } %>
</ul>
</td>
<td class='post-count'>
<a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + pool.id}) %>'><%- pool.postCount %></a>
</td>
<td class='creation-time'>
<%= ctx.makeRelativeTime(pool.creationTime) %>
</td>
</tr>
<% } %>
</tbody>
</table>
<% } %>
</div>

View file

@ -73,12 +73,6 @@
</section> </section>
<% } %> <% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPostNotes) { %> <% if (ctx.canEditPostNotes) { %>
<section class='notes'> <section class='notes'>
<a href class='add'>Add a note</a> <a href class='add'>Add a note</a>

View file

@ -29,7 +29,6 @@
<span class='vim-nav-hint'>Next post &gt;</span> <span class='vim-nav-hint'>Next post &gt;</span>
</a> </a>
</article> </article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'> <article class='edit-post'>
<% if (ctx.editMode) { %> <% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'> <a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
@ -37,13 +36,16 @@
<span class='vim-nav-hint'>Back to view mode</span> <span class='vim-nav-hint'>Back to view mode</span>
</a> </a>
<% } else { %> <% } else { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'> <% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<i class='fa fa-pencil'></i> <a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<span class='vim-nav-hint'>Edit post</span> <% } else { %>
<a class='inactive'>
<% } %>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a> </a>
<% } %> <% } %>
</article> </article>
<% } %>
</nav> </nav>
<div class='sidebar-container'></div> <div class='sidebar-container'></div>
@ -52,16 +54,13 @@
<div class='content'> <div class='content'>
<div class='post-container'></div> <div class='post-container'></div>
<div class='after-mobile-controls'> <% if (ctx.canListComments) { %>
<div class='description'></div> <div class='comments-container'></div>
<% if (ctx.canCreateComments) { %> <% } %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %> <% if (ctx.canCreateComments) { %>
<div class='comments-container'></div> <h2>Add comment</h2>
<% } %> <div class='comment-form-container'></div>
</div> <% } %>
</div> </div>
</div> </div>

View file

@ -36,13 +36,8 @@
'image/jpeg': 'JPEG', 'image/jpeg': 'JPEG',
'image/png': 'PNG', 'image/png': 'PNG',
'image/webp': 'WEBP', 'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4', 'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF', 'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] + }[ctx.post.mimeType] +
' (' + ' (' +

View file

@ -9,16 +9,11 @@
'image/jpeg': 'JPEG', 'image/jpeg': 'JPEG',
'image/png': 'PNG', 'image/png': 'PNG',
'image/webp': 'WEBP', 'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4', 'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'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.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!-- --><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
@ -57,8 +52,7 @@
<section class='search'> <section class='search'>
Search on Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot; <a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot; <a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section> </section>
<section class='social'> <section class='social'>
@ -97,12 +91,12 @@
--></a><!-- --></a><!--
--><% } %><!-- --><% } %><!--
--><% if (ctx.canListPosts) { %><!-- --><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!-- --><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!-- --><% } %><!--
--><%- ctx.getPrettyName(tag.names[0]) %><!-- --><%- ctx.getPrettyTagName(tag.names[0]) %>&#32;<!--
--><% if (ctx.canListPosts) { %><!-- --><% if (ctx.canListPosts) { %><!--
--></a><!-- --></a><!--
--><% } %>&#32;<!-- --><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!-- --><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!-- --></li><!--
--><% } %><!-- --><% } %><!--

View file

@ -7,28 +7,12 @@
<span class='skip-duplicates'> <span class='skip-duplicates'>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Skip duplicate', text: 'Skip duplicates',
name: 'skip-duplicates', name: 'skip-duplicates',
checked: false, checked: false,
}) %> }) %>
</span> </span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<input type='button' value='Cancel' class='cancel'/> <input type='button' value='Cancel' class='cancel'/>
</div> </div>

View file

@ -61,7 +61,6 @@
text: 'Upload anonymously', text: 'Upload anonymously',
name: 'anonymous', name: 'anonymous',
checked: ctx.uploadable.anonymous, checked: ctx.uploadable.anonymous,
readonly: ctx.uploadable.forceAnonymous,
}) %> }) %>
</div> </div>
<% } %> <% } %>

View file

@ -16,6 +16,7 @@
%><form class='horizontal bulk-edit bulk-edit-tags'><% %><form class='horizontal bulk-edit bulk-edit-tags'><%
%><span class='append hint'>Tagging with:</span><% %><span class='append hint'>Tagging with:</span><%
%><a href class='mousetrap button append open'>Mass tag</a><% %><a href class='mousetrap button append open'>Mass tag</a><%
%><wbr/><%
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><% %><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start' type='submit' value='Start tagging'/><% %><input class='mousetrap start' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append close'>Stop tagging</a><% %><a href class='mousetrap button append close'>Stop tagging</a><%
@ -27,11 +28,4 @@
%><a href class='mousetrap button append close'>Stop editing safety</a><% %><a href class='mousetrap button append close'>Stop editing safety</a><%
%></form><% %></form><%
%><% } %><% %><% } %><%
%><% if (ctx.canBulkDelete) { %><%
%><form class='horizontal bulk-edit bulk-edit-delete'><%
%><a href class='mousetrap button append open'>Mass delete</a><%
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
%><a href class='mousetrap button append close'>Stop deleting</a><%
%></form><%
%><% } %><%
%></div> %></div>

View file

@ -1,4 +1,4 @@
<% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %> <div class='post-list'>
<% if (ctx.response.results.length) { %> <% if (ctx.response.results.length) { %>
<ul> <ul>
<% for (let post of ctx.response.results) { %> <% for (let post of ctx.response.results) { %>
@ -50,10 +50,6 @@
<% } %> <% } %>
</span> </span>
<% } %> <% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span> </span>
</li> </li>
<% } %> <% } %>

View file

@ -22,15 +22,6 @@
}) %> }) %>
</li> </li>
<li>
<%= ctx.makeCheckbox({
text: 'Use dark theme',
name: 'dark-theme',
checked: ctx.browsingSettings.darkTheme,
}) %>
<p class='hint'>Changing this setting will require you to refresh the page for it to apply.</p>
</li>
<li> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Upscale small posts', text: 'Upscale small posts',
@ -47,15 +38,6 @@
<p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p> <p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p>
</li> </li>
<li>
<%= ctx.makeCheckbox({
text: 'Use post flow',
name: 'post-flow',
checked: ctx.browsingSettings.postFlow,
}) %>
<p class='hint'>Use a content-aware flow for thumbnails on the post search page.</p>
</li>
<li> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Enable transparency grid', text: 'Enable transparency grid',
@ -84,8 +66,8 @@
<li> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Display underscores as spaces', text: 'Display underscores as spaces in tags',
name: 'underscores-as-spaces', name: 'tag-underscores-as-spaces',
checked: ctx.browsingSettings.tagUnderscoresAsSpaces, 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> <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>

View file

@ -1,5 +1,5 @@
<div class='content-wrapper' id='tag'> <div class='content-wrapper' id='tag'>
<h1><%- ctx.getPrettyName(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='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!-- --><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--

View file

@ -7,7 +7,6 @@
<tr> <tr>
<th class='name'>Category name</th> <th class='name'>Category name</th>
<th class='color'>CSS color</th> <th class='color'>CSS color</th>
<th class='order'>Order</th>
<th class='usages'>Usages</th> <th class='usages'>Usages</th>
</tr> </tr>
</thead> </thead>
@ -22,7 +21,7 @@
<div class='messages'></div> <div class='messages'></div>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %> <% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
<div class='buttons'> <div class='buttons'>
<input type='submit' class='save' value='Save changes'> <input type='submit' class='save' value='Save changes'>
</div> </div>

View file

@ -17,13 +17,6 @@
<%- ctx.tagCategory.color %> <%- ctx.tagCategory.color %>
<% } %> <% } %>
</td> </td>
<td class='order'>
<% if (ctx.canEditOrder) { %>
<%= ctx.makeNumericInput({value: ctx.tagCategory.order}) %>
<% } else { %>
<%- ctx.tagCategory.order %>
<% } %>
</td>
<td class='usages'> <td class='usages'>
<% if (ctx.tagCategory.name) { %> <% if (ctx.tagCategory.name) { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'> <a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>

View file

@ -1,6 +1,6 @@
<div class='tag-delete'> <div class='tag-delete'>
<form> <form>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(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>

View file

@ -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='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(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>

View file

@ -3,35 +3,35 @@
<table> <table>
<thead> <thead>
<th class='names'> <th class='names'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %> <% if (ctx.query == 'sort:name' || !ctx.query) { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a> <a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
<% } else { %> <% } else { %>
<a href='<%- ctx.formatClientLink('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.parameters.query == 'sort:implication-count') { %> <% if (ctx.query == 'sort:implication-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a> <a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
<% } else { %> <% } else { %>
<a href='<%- ctx.formatClientLink('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.parameters.query == 'sort:suggestion-count') { %> <% if (ctx.query == 'sort:suggestion-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a> <a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
<% } else { %> <% } else { %>
<a href='<%- ctx.formatClientLink('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.parameters.query == 'sort:usages') { %> <% if (ctx.query == 'sort:usages') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a> <a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
<% } else { %> <% } else { %>
<a href='<%- ctx.formatClientLink('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.parameters.query == 'sort:creation-time') { %> <% if (ctx.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a> <a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %> <% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a> <a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 B

View file

@ -1,10 +1,10 @@
"use strict"; 'use strict';
const cookies = require("js-cookie"); const cookies = require('js-cookie');
const request = require("superagent"); const request = require('superagent');
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"); const uri = require('./util/uri.js');
let fileTokens = {}; let fileTokens = {};
let remoteConfig = null; let remoteConfig = null;
@ -18,22 +18,22 @@ class Api extends events.EventTarget {
this.token = null; this.token = null;
this.cache = {}; this.cache = {};
this.allRanks = [ this.allRanks = [
"anonymous", 'anonymous',
"restricted", 'restricted',
"regular", 'regular',
"power", 'power',
"moderator", 'moderator',
"administrator", 'administrator',
"nobody", 'nobody',
]; ];
this.rankNames = new Map([ this.rankNames = new Map([
["anonymous", "Anonymous"], ['anonymous', 'Anonymous'],
["restricted", "Restricted user"], ['restricted', 'Restricted user'],
["regular", "Regular user"], ['regular', 'Regular user'],
["power", "Power user"], ['power', 'Power user'],
["moderator", "Moderator"], ['moderator', 'Moderator'],
["administrator", "Administrator"], ['administrator', 'Administrator'],
["nobody", "Nobody"], ['nobody', 'Nobody'],
]); ]);
} }
@ -43,12 +43,11 @@ class Api extends events.EventTarget {
resolve(this.cache[url]); resolve(this.cache[url]);
}); });
} }
return this._wrappedRequest(url, request.get, {}, {}, options).then( return this._wrappedRequest(url, request.get, {}, {}, options)
(response) => { .then(response => {
this.cache[url] = response; this.cache[url] = response;
return Promise.resolve(response); return Promise.resolve(response);
} });
);
} }
post(url, data, files, options) { post(url, data, files, options) {
@ -68,9 +67,10 @@ class Api extends events.EventTarget {
fetchConfig() { fetchConfig() {
if (remoteConfig === null) { if (remoteConfig === null) {
return this.get(uri.formatApiLink("info")).then((response) => { return this.get(uri.formatApiLink('info'))
remoteConfig = response.config; .then(response => {
}); remoteConfig = response.config;
});
} else { } else {
return Promise.resolve(); return Promise.resolve();
} }
@ -84,10 +84,6 @@ class Api extends events.EventTarget {
return remoteConfig.tagNameRegex; return remoteConfig.tagNameRegex;
} }
getPoolNameRegex() {
return remoteConfig.poolNameRegex;
}
getPasswordRegex() { getPasswordRegex() {
return remoteConfig.passwordRegex; return remoteConfig.passwordRegex;
} }
@ -115,8 +111,7 @@ class Api extends events.EventTarget {
continue; continue;
} }
const rankIndex = this.allRanks.indexOf( const rankIndex = this.allRanks.indexOf(
remoteConfig.privileges[p] remoteConfig.privileges[p]);
);
if (minViableRank === null || rankIndex < minViableRank) { if (minViableRank === null || rankIndex < minViableRank) {
minViableRank = rankIndex; minViableRank = rankIndex;
} }
@ -124,16 +119,17 @@ class Api extends events.EventTarget {
if (minViableRank === null) { if (minViableRank === null) {
throw `Bad privilege name: ${lookup}`; throw `Bad privilege name: ${lookup}`;
} }
let myRank = let myRank = this.user !== null ?
this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0; this.allRanks.indexOf(this.user.rank) :
0;
return myRank >= minViableRank; return myRank >= minViableRank;
} }
loginFromCookies() { loginFromCookies() {
const auth = cookies.getJSON("auth"); const auth = cookies.getJSON('auth');
return auth && auth.user && auth.token return auth && auth.user && auth.token ?
? this.loginWithToken(auth.user, auth.token, true) this.loginWithToken(auth.user, auth.token, true) :
: Promise.resolve(); Promise.resolve();
} }
loginWithToken(userName, token, doRemember) { loginWithToken(userName, token, doRemember) {
@ -141,74 +137,63 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.userName = userName; this.userName = userName;
this.token = token; this.token = token;
this.get("/user/" + userName + "?bump-login=true").then( this.get('/user/' + userName + '?bump-login=true')
(response) => { .then(response => {
const options = {}; const options = {};
if (doRemember) { if (doRemember) {
options.expires = 365; options.expires = 365;
} }
cookies.set( cookies.set(
"auth", 'auth',
{ user: userName, token: token }, {'user': userName, 'token': token},
options options);
);
this.user = response; this.user = response;
resolve(); resolve();
this.dispatchEvent(new CustomEvent("login")); this.dispatchEvent(new CustomEvent('login'));
}, }, error => {
(error) => {
reject(error); reject(error);
this.logout(); this.logout();
} });
);
}); });
} }
createToken(userName, options) { createToken(userName, options) {
let userTokenRequest = { let userTokenRequest = {
enabled: true, enabled: true,
note: "Web Login Token", note: 'Web Login Token'
}; };
if (typeof options.expires !== "undefined") { if (typeof options.expires !== 'undefined') {
userTokenRequest.expirationTime = new Date() userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
.addDays(options.expires)
.toISOString();
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.post("/user-token/" + userName, userTokenRequest).then( this.post('/user-token/' + userName, userTokenRequest)
(response) => { .then(response => {
cookies.set( cookies.set(
"auth", 'auth',
{ user: userName, token: response.token }, {'user': userName, 'token': response.token},
options options);
);
this.userName = userName; this.userName = userName;
this.token = response.token; this.token = response.token;
this.userPassword = null; this.userPassword = null;
}, }, error => {
(error) => {
reject(error); reject(error);
} });
);
}); });
} }
deleteToken(userName, userToken) { deleteToken(userName, userToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.delete("/user-token/" + userName + "/" + userToken, {}).then( this.delete('/user-token/' + userName + '/' + userToken, {})
(response) => { .then(response => {
const options = {}; const options = {};
cookies.set( cookies.set(
"auth", 'auth',
{ user: userName, token: null }, {'user': userName, 'token': null},
options options);
);
resolve(); resolve();
}, }, error => {
(error) => {
reject(error); reject(error);
} });
);
}); });
} }
@ -217,8 +202,8 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.userName = userName; this.userName = userName;
this.userPassword = userPassword; this.userPassword = userPassword;
this.get("/user/" + userName + "?bump-login=true").then( this.get('/user/' + userName + '?bump-login=true')
(response) => { .then(response => {
const options = {}; const options = {};
if (doRemember) { if (doRemember) {
options.expires = 365; options.expires = 365;
@ -226,26 +211,22 @@ class Api extends events.EventTarget {
this.createToken(this.userName, options); this.createToken(this.userName, options);
this.user = response; this.user = response;
resolve(); resolve();
this.dispatchEvent(new CustomEvent("login")); this.dispatchEvent(new CustomEvent('login'));
}, }, error => {
(error) => {
reject(error); reject(error);
this.logout(); this.logout();
} });
);
}); });
} }
logout() { logout() {
let self = this; let self = this;
this.deleteToken(this.userName, this.token).then( this.deleteToken(this.userName, this.token)
(response) => { .then(response => {
self._logout(); self._logout();
}, }, error => {
(error) => {
self._logout(); self._logout();
} });
);
} }
_logout() { _logout() {
@ -253,19 +234,17 @@ class Api extends events.EventTarget {
this.userName = null; this.userName = null;
this.userPassword = null; this.userPassword = null;
this.token = null; this.token = null;
this.dispatchEvent(new CustomEvent("logout")); this.dispatchEvent(new CustomEvent('logout'));
} }
forget() { forget() {
cookies.remove("auth"); cookies.remove('auth');
} }
isLoggedIn(user) { isLoggedIn(user) {
if (user) { if (user) {
return ( return this.userName !== null &&
this.userName !== null && this.userName.toLowerCase() === user.name.toLowerCase();
this.userName.toLowerCase() === user.name.toLowerCase()
);
} else { } else {
return this.userName !== null; return this.userName !== null;
} }
@ -276,7 +255,8 @@ class Api extends events.EventTarget {
} }
_getFullUrl(url) { _getFullUrl(url) {
const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/"); const fullUrl =
('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];
@ -301,7 +281,7 @@ class Api extends events.EventTarget {
const file = files[key]; const file = files[key];
const fileId = this._getFileId(file); const fileId = this._getFileId(file);
if (fileTokens[fileId]) { if (fileTokens[fileId]) {
data[key + "Token"] = fileTokens[fileId]; data[key + 'Token'] = fileTokens[fileId];
} else { } else {
promise = promise promise = promise
.then(() => { .then(() => {
@ -309,40 +289,33 @@ class Api extends events.EventTarget {
abortFunction = () => uploadPromise.abort(); abortFunction = () => uploadPromise.abort();
return uploadPromise; return uploadPromise;
}) })
.then((token) => { .then(token => {
abortFunction = () => {}; abortFunction = () => {};
fileTokens[fileId] = token; fileTokens[fileId] = token;
data[key + "Token"] = token; data[key + 'Token'] = token;
return Promise.resolve(); return Promise.resolve();
}); });
} }
} }
} }
promise = promise promise = promise.then(
.then(() => { () => {
let requestPromise = this._rawRequest( let requestPromise = this._rawRequest(
url, url, requestFactory, data, {}, options);
requestFactory,
data,
{},
options
);
abortFunction = () => requestPromise.abort(); abortFunction = () => requestPromise.abort();
return requestPromise; return requestPromise;
}) })
.catch((error) => { .catch(error => {
if ( if (error.response && error.response.name ===
error.response && 'MissingOrExpiredRequiredFileError') {
error.response.name === "MissingOrExpiredRequiredFileError"
) {
for (let key of Object.keys(files)) { for (let key of Object.keys(files)) {
const file = files[key]; const file = files[key];
const fileId = this._getFileId(file); const fileId = this._getFileId(file);
fileTokens[fileId] = null; fileTokens[fileId] = null;
} }
error.message = error.message =
"The uploaded file has expired; " + 'The uploaded file has expired; ' +
"please resend the form to reupload."; 'please resend the form to reupload.';
} }
return Promise.reject(error); return Promise.reject(error);
}); });
@ -354,17 +327,13 @@ 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", 'uploads', request.post, {}, {content: file}, options);
request.post,
{},
{ content: file },
options
);
abortFunction = () => uploadPromise.abort(); abortFunction = () => uploadPromise.abort();
return uploadPromise.then((response) => { return uploadPromise.then(
abortFunction = () => {}; response => {
return resolve(response.token); abortFunction = () => {};
}, reject); return resolve(response.token);
}, reject);
}); });
returnedPromise.abort = () => abortFunction(); returnedPromise.abort = () => abortFunction();
return returnedPromise; return returnedPromise;
@ -379,7 +348,7 @@ class Api extends events.EventTarget {
let returnedPromise = new Promise((resolve, reject) => { let returnedPromise = new Promise((resolve, reject) => {
let req = requestFactory(fullUrl); let req = requestFactory(fullUrl);
req.set("Accept", "application/json"); req.set('Accept', 'application/json');
if (query) { if (query) {
req.query(query); req.query(query);
@ -389,7 +358,7 @@ class Api extends events.EventTarget {
for (let key of Object.keys(files)) { for (let key of Object.keys(files)) {
const value = files[key]; const value = files[key];
if (value.constructor === String) { if (value.constructor === String) {
data[key + "Url"] = value; data[key + 'Url'] = value;
} else { } else {
req.attach(key, value || new Blob()); req.attach(key, value || new Blob());
} }
@ -398,9 +367,9 @@ class Api extends events.EventTarget {
if (data) { if (data) {
if (files && Object.keys(files).length) { if (files && Object.keys(files).length) {
req.attach("metadata", new Blob([JSON.stringify(data)])); req.attach('metadata', new Blob([JSON.stringify(data)]));
} else { } else {
req.set("Content-Type", "application/json"); req.set('Content-Type', 'application/json');
req.send(data); req.send(data);
} }
} }
@ -408,29 +377,19 @@ class Api extends events.EventTarget {
try { try {
if (this.userName && this.token) { if (this.userName && this.token) {
req.auth = null; req.auth = null;
// eslint-disable-next-line no-undef req.set('Authorization', 'Token '
req.set( + new Buffer(this.userName + ":" + this.token).toString('base64'))
"Authorization",
"Token " +
new Buffer(
this.userName + ":" + this.token
).toString("base64")
);
} else if (this.userName && this.userPassword) { } else if (this.userName && this.userPassword) {
req.auth( req.auth(
this.userName, this.userName,
encodeURIComponent(this.userPassword).replace( encodeURIComponent(this.userPassword)
/%([0-9A-F]{2})/g, .replace(/%([0-9A-F]{2})/g, (match, p1) => {
(match, p1) => { return String.fromCharCode('0x' + p1);
return String.fromCharCode("0x" + p1); }));
}
)
);
} }
} catch (e) { } catch (e) {
reject( reject(
new Error("Authentication error (malformed credentials)") new Error('Authentication error (malformed credentials)'));
);
} }
if (!options.noProgress) { if (!options.noProgress) {
@ -438,11 +397,10 @@ class Api extends events.EventTarget {
} }
abortFunction = () => { abortFunction = () => {
req.abort(); // does *NOT* call the callback passed in .end() req.abort(); // does *NOT* call the callback passed in .end()
progress.done(); progress.done();
reject( reject(
new Error("The request was aborted due to user cancel.") new Error('The request was aborted due to user cancel.'));
);
}; };
req.end((error, response) => { req.end((error, response) => {
@ -451,8 +409,7 @@ class Api extends events.EventTarget {
if (error) { if (error) {
if (response && response.body) { if (response && response.body) {
error = new Error( error = new Error(
response.body.description || "Unknown error" response.body.description || 'Unknown error');
);
error.response = response.body; error.response = response.body;
} }
reject(error); reject(error);

View file

@ -1,4 +1,4 @@
"use strict"; 'use strict';
const config = require("./.config.autogen.json"); const config = require('./.config.autogen.json');
module.exports = config; module.exports = config;

View file

@ -1,40 +1,36 @@
"use strict"; 'use strict';
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 tags = require('../tags.js');
const pools = require("../pools.js"); const uri = require('../util/uri.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");
class LoginController { class LoginController {
constructor() { constructor() {
api.forget(); api.forget();
topNavigation.activate("login"); topNavigation.activate('login');
topNavigation.setTitle("Login"); topNavigation.setTitle('Login');
this._loginView = new LoginView(); this._loginView = new LoginView();
this._loginView.addEventListener("submit", (e) => this._evtLogin(e)); this._loginView.addEventListener('submit', e => this._evtLogin(e));
} }
_evtLogin(e) { _evtLogin(e) {
this._loginView.clearMessages(); this._loginView.clearMessages();
this._loginView.disableForm(); this._loginView.disableForm();
api.forget(); api.forget();
api.login(e.detail.name, e.detail.password, e.detail.remember).then( api.login(e.detail.name, e.detail.password, e.detail.remember)
() => { .then(() => {
const ctx = router.show(uri.formatClientLink()); 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 // reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous
tags.refreshCategoryColorMap(); tags.refreshCategoryColorMap();
pools.refreshCategoryColorMap(); }, error => {
},
(error) => {
this._loginView.showError(error.message); this._loginView.showError(error.message);
this._loginView.enableForm(); this._loginView.enableForm();
} });
);
} }
} }
@ -43,15 +39,15 @@ class LogoutController {
api.forget(); api.forget();
api.logout(); api.logout();
const ctx = router.show(uri.formatClientLink()); 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();
}); });
}; };

View file

@ -1,19 +1,19 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class BasePostController { class BasePostController {
constructor(ctx) { constructor(ctx) {
if (!api.hasPrivilege("posts:view")) { if (!api.hasPrivilege('posts:view')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view posts."); this._view.showError('You don\'t have privileges to view posts.');
return; return;
} }
topNavigation.activate("posts"); topNavigation.activate('posts');
topNavigation.setTitle("Post #" + ctx.parameters.id.toString()); topNavigation.setTitle('Post #' + ctx.parameters.id.toString());
} }
} }

View file

@ -1,55 +1,49 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const uri = require("../util/uri.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');
const CommentsPageView = require("../views/comments_page_view.js"); const CommentsPageView = require('../views/comments_page_view.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
const fields = ["id", "comments", "commentCount", "thumbnailUrl"]; const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl'];
class CommentsController { class CommentsController {
constructor(ctx) { constructor(ctx) {
if (!api.hasPrivilege("comments:list")) { if (!api.hasPrivilege('comments:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError( this._view.showError(
"You don't have privileges to view comments." 'You don\'t have privileges to view comments.');
);
return; return;
} }
topNavigation.activate("comments"); topNavigation.activate('comments');
topNavigation.setTitle("Listing comments"); topNavigation.setTitle('Listing comments');
this._pageController = new PageController(); this._pageController = new PageController();
this._pageController.run({ this._pageController.run({
parameters: ctx.parameters, parameters: ctx.parameters,
defaultLimit: 10, defaultLimit: 10,
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, ctx.parameters, { const parameters = Object.assign(
offset: offset, {}, ctx.parameters, {offset: offset, limit: limit});
limit: limit, return uri.formatClientLink('comments', parameters);
});
return uri.formatClientLink("comments", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return PostList.search( return PostList.search(
"sort:comment-date comment-count-min:1", 'sort:comment-date comment-count-min:1',
offset, offset, limit, fields);
limit,
fields
);
}, },
pageRenderer: (pageCtx) => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"), canViewPosts: api.hasPrivilege('posts:view'),
}); });
const view = new CommentsPageView(pageCtx); const view = new CommentsPageView(pageCtx);
view.addEventListener("submit", (e) => this._evtUpdate(e)); view.addEventListener('submit', e => this._evtUpdate(e));
view.addEventListener("score", (e) => this._evtScore(e)); view.addEventListener('score', e => this._evtScore(e));
view.addEventListener("delete", (e) => this._evtDelete(e)); view.addEventListener('delete', e => this._evtDelete(e));
return view; return view;
}, },
}); });
@ -58,27 +52,25 @@ class CommentsController {
_evtUpdate(e) { _evtUpdate(e) {
// TODO: disable form // TODO: disable form
e.detail.comment.text = e.detail.text; e.detail.comment.text = e.detail.text;
e.detail.comment.save().catch((error) => { e.detail.comment.save()
e.detail.target.showError(error.message); .catch(error => {
// TODO: enable form e.detail.target.showError(error.message);
}); // TODO: enable form
});
} }
_evtScore(e) { _evtScore(e) {
e.detail.comment e.detail.comment.setScore(e.detail.score)
.setScore(e.detail.score) .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
_evtDelete(e) { _evtDelete(e) {
e.detail.comment e.detail.comment.delete()
.delete() .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
} };
module.exports = (router) => { module.exports = router => {
router.enter(["comments"], (ctx, next) => { router.enter(['comments'],
new CommentsController(ctx); (ctx, next) => { new CommentsController(ctx); });
});
}; };

View file

@ -1,24 +1,24 @@
"use strict"; 'use strict';
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const HelpView = require("../views/help_view.js"); const HelpView = require('../views/help_view.js');
class HelpController { class HelpController {
constructor(section, subsection) { constructor(section, subsection) {
topNavigation.activate("help"); topNavigation.activate('help');
topNavigation.setTitle("Help"); topNavigation.setTitle('Help');
this._helpView = new HelpView(section, subsection); this._helpView = new HelpView(section, subsection);
} }
} }
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);
}); });
}; };

View file

@ -1,27 +1,26 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const config = require("../config.js"); const config = require('../config.js');
const Info = require("../models/info.js"); const Info = require('../models/info.js');
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const HomeView = require("../views/home_view.js"); const HomeView = require('../views/home_view.js');
class HomeController { class HomeController {
constructor() { constructor() {
topNavigation.activate("home"); topNavigation.activate('home');
topNavigation.setTitle("Home"); topNavigation.setTitle('Home');
this._homeView = new HomeView({ this._homeView = new HomeView({
name: api.getName(), 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'),
canListPosts: api.hasPrivilege("posts:list"), canListPosts: api.hasPrivilege('posts:list'),
isDevelopmentMode: config.environment == "development",
}); });
Info.get().then( Info.get()
(info) => { .then(info => {
this._homeView.setStats({ this._homeView.setStats({
diskUsage: info.diskUsage, diskUsage: info.diskUsage,
postCount: info.postCount, postCount: info.postCount,
@ -32,8 +31,7 @@ class HomeController {
featuringTime: info.featuringTime, featuringTime: info.featuringTime,
}); });
}, },
(error) => this._homeView.showError(error.message) error => this._homeView.showError(error.message));
);
} }
showSuccess(message) { showSuccess(message) {
@ -43,9 +41,9 @@ class HomeController {
showError(message) { showError(message) {
this._homeView.showError(message); this._homeView.showError(message);
} }
} };
module.exports = (router) => { module.exports = router => {
router.enter([], (ctx, next) => { router.enter([], (ctx, next) => {
ctx.controller = new HomeController(); ctx.controller = new HomeController();
}); });

View file

@ -1,17 +1,17 @@
"use strict"; 'use strict';
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const NotFoundView = require("../views/not_found_view.js"); const NotFoundView = require('../views/not_found_view.js');
class NotFoundController { class NotFoundController {
constructor(path) { constructor(path) {
topNavigation.activate(""); topNavigation.activate('');
topNavigation.setTitle("Not found"); topNavigation.setTitle('Not found');
this._notFoundView = new NotFoundView(path); this._notFoundView = new NotFoundView(path);
} }
} };
module.exports = (router) => { module.exports = router => {
router.enter(null, (ctx, next) => { router.enter(null, (ctx, next) => {
ctx.controller = new NotFoundController(ctx.canonicalPath); ctx.controller = new NotFoundController(ctx.canonicalPath);
}); });

View file

@ -1,8 +1,8 @@
"use strict"; 'use strict';
const settings = require("../models/settings.js"); const settings = require('../models/settings.js');
const EndlessPageView = require("../views/endless_page_view.js"); const EndlessPageView = require('../views/endless_page_view.js');
const ManualPageView = require("../views/manual_page_view.js"); const ManualPageView = require('../views/manual_page_view.js');
class PageController { class PageController {
constructor(ctx) { constructor(ctx) {

View file

@ -1,20 +1,19 @@
"use strict"; 'use strict';
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 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');
class PasswordResetController { class PasswordResetController {
constructor() { constructor() {
topNavigation.activate("login"); topNavigation.activate('login');
topNavigation.setTitle("Password reminder"); topNavigation.setTitle('Password reminder');
this._passwordResetView = new PasswordResetView(); this._passwordResetView = new PasswordResetView();
this._passwordResetView.addEventListener("submit", (e) => this._passwordResetView.addEventListener(
this._evtReset(e) 'submit', e => this._evtReset(e));
);
} }
_evtReset(e) { _evtReset(e) {
@ -22,20 +21,15 @@ class PasswordResetController {
this._passwordResetView.disableForm(); this._passwordResetView.disableForm();
api.forget(); api.forget();
api.logout(); api.logout();
api.get( api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
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, ' +
"please click the link it contains." 'please click the link it contains.');
); }, error => {
},
(error) => {
this._passwordResetView.showError(error.message); this._passwordResetView.showError(error.message);
this._passwordResetView.enableForm(); this._passwordResetView.enableForm();
} });
);
} }
} }
@ -44,30 +38,26 @@ class PasswordResetFinishController {
api.forget(); api.forget();
api.logout(); api.logout();
let password = null; let password = null;
api.post(uri.formatApiLink("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(uri.formatClientLink());
() => { ctx.controller.showSuccess('New password: ' + password);
const ctx = router.show(uri.formatClientLink()); }, error => {
ctx.controller.showSuccess("New password: " + password); const ctx = router.show(uri.formatClientLink());
}, ctx.controller.showError(error.message);
(error) => { });
const ctx = router.show(uri.formatClientLink());
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", ":descriptor"], (ctx, next) => { router.enter(['password-reset', ':descriptor'], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(":", 2); const [name, token] = ctx.parameters.descriptor.split(':', 2);
ctx.controller = new PasswordResetFinishController(name, token); ctx.controller = new PasswordResetFinishController(name, token);
}); });
}; };

View file

@ -1,69 +0,0 @@
"use strict";
const api = require("../api.js");
const pools = require("../pools.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolCategoriesView = require("../views/pool_categories_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCategoriesController {
constructor() {
if (!api.hasPrivilege("poolCategories:list")) {
this._view = new EmptyView();
this._view.showError(
"You don't have privileges to view pool categories."
);
return;
}
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
PoolCategoryList.get().then(
(response) => {
this._poolCategories = response.results;
this._view = new PoolCategoriesView({
poolCategories: this._poolCategories,
canEditName: api.hasPrivilege("poolCategories:edit:name"),
canEditColor: api.hasPrivilege(
"poolCategories:edit:color"
),
canDelete: api.hasPrivilege("poolCategories:delete"),
canCreate: api.hasPrivilege("poolCategories:create"),
canSetDefault: api.hasPrivilege(
"poolCategories:setDefault"
),
});
this._view.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._poolCategories.save().then(
() => {
pools.refreshCategoryColorMap();
this._view.enableForm();
this._view.showSuccess("Changes saved.");
},
(error) => {
this._view.enableForm();
this._view.showError(error.message);
}
);
}
}
module.exports = (router) => {
router.enter(["pool-categories"], (ctx, next) => {
ctx.controller = new PoolCategoriesController(ctx, next);
});
};

View file

@ -1,176 +0,0 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const Post = require("../models/post.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolView = require("../views/pool_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolController {
constructor(ctx, section) {
if (!api.hasPrivilege("pools:view")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to view pools.");
return;
}
Promise.all([
PoolCategoryList.get(),
Pool.get(ctx.parameters.id),
]).then(
(responses) => {
const [poolCategoriesResponse, pool] = responses;
topNavigation.activate("pools");
topNavigation.setTitle("Pool #" + pool.names[0]);
this._name = ctx.parameters.name;
pool.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolView({
pool: pool,
section: section,
canEditAnything: api.hasPrivilege("pools:edit"),
canEditNames: api.hasPrivilege("pools:edit:names"),
canEditCategory: api.hasPrivilege("pools:edit:category"),
canEditDescription: api.hasPrivilege(
"pools:edit:description"
),
canEditPosts: api.hasPrivilege("pools:edit:posts"),
canMerge: api.hasPrivilege("pools:merge"),
canDelete: api.hasPrivilege("pools:delete"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtChange(e) {
misc.enableExitConfirmation();
}
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._name !== e.detail.pool.names[0]) {
router.replace(
uri.formatClientLink("pool", e.detail.pool.id, section),
null,
false
);
}
}
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) {
e.detail.pool.names = e.detail.names;
}
if (e.detail.category !== undefined && e.detail.category !== null) {
e.detail.pool.category = e.detail.category;
}
if (e.detail.description !== undefined && e.detail.description !== null) {
e.detail.pool.description = e.detail.description;
}
if (e.detail.posts !== undefined && e.detail.posts !== null) {
e.detail.pool.posts.clear();
for (let postId of e.detail.posts) {
e.detail.pool.posts.add(
Post.fromResponse({ id: parseInt(postId) })
);
}
}
e.detail.pool.save().then(
() => {
this._view.showSuccess("Pool saved.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.merge(e.detail.targetPoolId, e.detail.addAlias).then(
() => {
this._view.showSuccess("Pool merged.");
this._view.enableForm();
router.replace(
uri.formatClientLink(
"pool",
e.detail.targetPoolId,
"merge"
),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool deleted.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = (router) => {
router.enter(["pool", ":id", "edit"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "edit");
});
router.enter(["pool", ":id", "merge"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "merge");
});
router.enter(["pool", ":id", "delete"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "delete");
});
router.enter(["pool", ":id"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "summary");
});
};

View file

@ -1,65 +0,0 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const PoolCreateView = require("../views/pool_create_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCreateController {
constructor(ctx) {
if (!api.hasPrivilege("pools:create")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to create pools.");
return;
}
PoolCategoryList.get().then(
(poolCategoriesResponse) => {
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolCreateView({
canCreate: api.hasPrivilege("pools:create"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("submit", (e) =>
this._evtCreate(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtCreate(e) {
this._view.clearMessages();
this._view.disableForm();
api.post(uri.formatApiLink("pool"), e.detail).then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool created.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = (router) => {
router.enter(["pool", "create"], (ctx, next) => {
ctx.controller = new PoolCreateController(ctx, "create");
});
};

View file

@ -1,121 +0,0 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const PoolList = require("../models/pool_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PoolsHeaderView = require("../views/pools_header_view.js");
const PoolsPageView = require("../views/pools_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
"id",
"names",
"posts",
"creationTime",
"postCount",
"category",
];
class PoolListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege("pools:list")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to view pools.");
return;
}
this._ctx = ctx;
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
this._headerView = new PoolsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canCreate: api.hasPrivilege("pools:create"),
canEditPoolCategories: api.hasPrivilege("poolCategories:edit"),
});
this._headerView.addEventListener(
"submit",
(e) => this._evtSubmit(e),
);
this._headerView.addEventListener(
"navigate",
(e) => this._evtNavigate(e)
);
this._syncPageController();
}
showSuccess(message) {
this._pageController.showSuccess(message);
}
showError(message) {
this._pageController.showError(message);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.save().then(
() => {
this._installView(e.detail.pool, "edit");
this._view.showSuccess("Pool created.");
router.replace(
uri.formatClientLink("pool", e.detail.pool.id, "edit"),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink("pools", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
_syncPageController() {
this._pageController.run({
parameters: this._ctx.parameters,
defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("pools", parameters);
},
requestPage: (offset, limit) => {
return PoolList.search(
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: (pageCtx) => {
return new PoolsPageView(pageCtx);
},
});
}
}
module.exports = (router) => {
router.enter(["pools"], (ctx, next) => {
ctx.controller = new PoolListController(ctx);
});
};

View file

@ -1,33 +1,28 @@
"use strict"; 'use strict';
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 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');
const PostDetailView = require("../views/post_detail_view.js"); const PostDetailView = require('../views/post_detail_view.js');
const BasePostController = require("./base_post_controller.js"); const BasePostController = require('./base_post_controller.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class PostDetailController extends BasePostController { class PostDetailController extends BasePostController {
constructor(ctx, section) { constructor(ctx, section) {
super(ctx); super(ctx);
Post.get(ctx.parameters.id).then( Post.get(ctx.parameters.id).then(post => {
(post) => { this._id = ctx.parameters.id;
this._id = ctx.parameters.id; post.addEventListener('change', e => this._evtSaved(e, section));
post.addEventListener("change", (e) => this._installView(post, section);
this._evtSaved(e, section) }, error => {
); this._view = new EmptyView();
this._installView(post, section); this._view.showError(error.message);
}, });
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
} }
showSuccess(message) { showSuccess(message) {
@ -38,68 +33,56 @@ class PostDetailController extends BasePostController {
this._view = new PostDetailView({ this._view = new PostDetailView({
post: post, post: post,
section: section, section: section,
canMerge: api.hasPrivilege("posts:merge"), canMerge: api.hasPrivilege('posts:merge'),
}); });
this._view.addEventListener("select", (e) => this._evtSelect(e)); this._view.addEventListener('select', e => this._evtSelect(e));
this._view.addEventListener("merge", (e) => this._evtMerge(e)); this._view.addEventListener('merge', e => this._evtMerge(e));
} }
_evtSelect(e) { _evtSelect(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
Post.get(e.detail.postId).then( Post.get(e.detail.postId).then(post => {
(post) => { this._view.selectPost(post);
this._view.selectPost(post); this._view.enableForm();
this._view.enableForm(); }, error => {
}, this._view.showError(error.message);
(error) => { this._view.enableForm();
this._view.showError(error.message); });
this._view.enableForm();
}
);
} }
_evtSaved(e, section) { _evtSaved(e, section) {
misc.disableExitConfirmation(); misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) { if (this._id !== e.detail.post.id) {
router.replace( router.replace(
uri.formatClientLink("post", e.detail.post.id, section), uri.formatClientLink('post', e.detail.post.id, section),
null, null, false);
false
);
} }
} }
_evtMerge(e) { _evtMerge(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.post e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent)
.merge(e.detail.targetPost.id, e.detail.useOldContent) .then(() => {
.then( this._installView(e.detail.post, 'merge');
() => { this._view.showSuccess('Post merged.');
this._installView(e.detail.post, "merge"); router.replace(
this._view.showSuccess("Post merged."); uri.formatClientLink(
router.replace( 'post', e.detail.targetPost.id, 'merge'),
uri.formatClientLink( null, false);
"post", }, error => {
e.detail.targetPost.id, this._view.showError(error.message);
"merge" this._view.enableForm();
), });
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["post", ":id", "merge"], (ctx, next) => { router.enter(
ctx.controller = new PostDetailController(ctx, "merge"); ['post', ':id', 'merge'],
}); (ctx, next) => {
ctx.controller = new PostDetailController(ctx, 'merge');
});
}; };

View file

@ -1,68 +1,47 @@
"use strict"; 'use strict';
const router = require("../router.js"); 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 uri = require("../util/uri.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');
const PostsHeaderView = require("../views/posts_header_view.js"); const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require("../views/posts_page_view.js"); 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", 'id', 'thumbnailUrl', 'type', 'safety',
"thumbnailUrl", 'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
"type",
"safety",
"score",
"favoriteCount",
"commentCount",
"tags",
"version",
];
class PostListController { class PostListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController(); if (!api.hasPrivilege('posts:list')) {
if (!api.hasPrivilege("posts:list")) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view posts."); this._view.showError('You don\'t have privileges to view posts.');
return; return;
} }
this._ctx = ctx; topNavigation.activate('posts');
topNavigation.setTitle('Listing posts');
topNavigation.activate("posts"); this._ctx = ctx;
topNavigation.setTitle("Listing posts"); this._pageController = new PageController();
this._headerView = new PostsHeaderView({ this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,
enableSafety: api.safetyEnabled(), enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"), canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags
}, },
}); });
this._headerView.addEventListener("navigate", (e) => this._headerView.addEventListener(
this._evtNavigate(e) 'navigate', e => this._evtNavigate(e));
);
if (this._headerView._bulkDeleteEditor) {
this._headerView._bulkDeleteEditor.addEventListener(
"deleteSelectedPosts",
(e) => {
this._evtDeleteSelectedPosts(e);
}
);
}
this._postsMarkedForDeletion = [];
this._syncPageController(); this._syncPageController();
} }
@ -71,67 +50,34 @@ class PostListController {
} }
get _bulkEditTags() { 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) {
router.showNoDispatch( router.showNoDispatch(
uri.formatClientLink("posts", e.detail.parameters) uri.formatClientLink('posts', 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) {
Promise.all( Promise.all(
this._bulkEditTags.map((tag) => e.detail.post.tags.addByName(tag)) this._bulkEditTags.map(tag =>
) e.detail.post.tags.addByName(tag)))
.then(e.detail.post.save()) .then(e.detail.post.save())
.catch((error) => window.alert(error.message)); .catch(error => window.alert(error.message));
} }
_evtUntag(e) { _evtUntag(e) {
for (let tag of this._bulkEditTags) { for (let tag of this._bulkEditTags) {
e.detail.post.tags.removeByName(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));
} }
_evtChangeSafety(e) { _evtChangeSafety(e) {
e.detail.post.safety = e.detail.safety; e.detail.post.safety = e.detail.safety;
e.detail.post.save().catch((error) => window.alert(error.message)); e.detail.post.save().catch(error => window.alert(error.message));
}
_evtMarkForDeletion(e) {
const postId = e.detail;
// Add or remove post from delete list
if (e.detail.delete) {
this._postsMarkedForDeletion.push(e.detail.post);
} else {
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
(x) => x.id != e.detail.post.id
);
}
}
_evtDeleteSelectedPosts(e) {
if (this._postsMarkedForDeletion.length == 0) return;
if (
confirm(
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
)
) {
Promise.all(
this._postsMarkedForDeletion.map((post) => post.delete())
)
.catch((error) => window.alert(error.message))
.then(() => {
this._postsMarkedForDeletion = [];
this._headerView._navigate();
});
}
} }
_syncPageController() { _syncPageController() {
@ -139,51 +85,37 @@ class PostListController {
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
defaultLimit: parseInt(settings.get().postsPerPage), defaultLimit: parseInt(settings.get().postsPerPage),
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, { const parameters = Object.assign(
offset: offset, {}, this._ctx.parameters, {offset: offset, limit: limit});
limit: limit, return uri.formatClientLink('posts', parameters);
});
return uri.formatClientLink("posts", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return PostList.search( return PostList.search(
this._ctx.parameters.query, this._ctx.parameters.query, offset, limit, fields);
offset,
limit,
fields
);
}, },
pageRenderer: (pageCtx) => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"), canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"), canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege( canBulkEditSafety:
"posts:bulk-edit:safety" api.hasPrivilege('posts:bulk-edit:safety'),
),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: { bulkEdit: {
tags: this._bulkEditTags, tags: this._bulkEditTags,
markedForDeletion: this._postsMarkedForDeletion,
}, },
postFlow: settings.get().postFlow,
}); });
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) => view.addEventListener(
this._evtChangeSafety(e) 'changeSafety', e => this._evtChangeSafety(e));
);
view.addEventListener("markForDeletion", (e) =>
this._evtMarkForDeletion(e)
);
return view; return view;
}, },
}); });
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["posts"], (ctx, next) => { router.enter(
ctx.controller = new PostListController(ctx); ['posts'],
}); (ctx, next) => { ctx.controller = new PostListController(ctx); });
}; };

View file

@ -1,16 +1,16 @@
"use strict"; 'use strict';
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 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');
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');
const PostMainView = require("../views/post_main_view.js"); const PostMainView = require('../views/post_main_view.js');
const BasePostController = require("./base_post_controller.js"); const BasePostController = require('./base_post_controller.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class PostMainController extends BasePostController { class PostMainController extends BasePostController {
constructor(ctx, editMode) { constructor(ctx, editMode) {
@ -18,110 +18,77 @@ class PostMainController extends BasePostController {
let parameters = ctx.parameters; let parameters = ctx.parameters;
Promise.all([ Promise.all([
Post.get(ctx.parameters.id), Post.get(ctx.parameters.id),
PostList.getAround( PostList.getAround(
ctx.parameters.id, ctx.parameters.id,
parameters ? parameters.query : null parameters ? parameters.query : null),
), ]).then(responses => {
]).then( const [post, aroundResponse] = responses;
(responses) => {
const [post, aroundResponse] = responses;
// remove junk from query, but save it into history so that it can // remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh // be still accessed after history navigation / page refresh
if (parameters.query) { if (parameters.query) {
ctx.state.parameters = parameters; ctx.state.parameters = parameters;
const url = editMode const url = editMode ?
? uri.formatClientLink( uri.formatClientLink('post', ctx.parameters.id, 'edit') :
"post", uri.formatClientLink('post', ctx.parameters.id);
ctx.parameters.id, router.replace(url, ctx.state, false);
"edit"
)
: uri.formatClientLink("post", ctx.parameters.id);
router.replace(url, ctx.state, false);
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev
? aroundResponse.prev.id
: null,
nextPostId: aroundResponse.next
? aroundResponse.next.id
: null,
canEditPosts: api.hasPrivilege("posts:edit"),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canListComments: api.hasPrivilege("comments:list"),
canCreateComments: api.hasPrivilege("comments:create"),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
"favorite",
(e) => this._evtFavoritePost(e)
);
this._view.sidebarControl.addEventListener(
"unfavorite",
(e) => this._evtUnfavoritePost(e)
);
this._view.sidebarControl.addEventListener("score", (e) =>
this._evtScorePost(e)
);
this._view.sidebarControl.addEventListener(
"fitModeChange",
(e) => this._evtFitModeChange(e)
);
this._view.sidebarControl.addEventListener("change", (e) =>
this._evtPostChange(e)
);
this._view.sidebarControl.addEventListener("submit", (e) =>
this._evtUpdatePost(e)
);
this._view.sidebarControl.addEventListener(
"feature",
(e) => this._evtFeaturePost(e)
);
this._view.sidebarControl.addEventListener("delete", (e) =>
this._evtDeletePost(e)
);
this._view.sidebarControl.addEventListener("merge", (e) =>
this._evtMergePost(e)
);
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener("change", (e) =>
this._evtCommentChange(e)
);
this._view.commentControl.addEventListener("submit", (e) =>
this._evtCreateComment(e)
);
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
"submit",
(e) => this._evtUpdateComment(e)
);
this._view.commentListControl.addEventListener(
"score",
(e) => this._evtScoreComment(e)
);
this._view.commentListControl.addEventListener(
"delete",
(e) => this._evtDeleteComment(e)
);
}
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
} }
);
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
canEditPosts: api.hasPrivilege('posts:edit'),
canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'),
canListComments: api.hasPrivilege('comments:list'),
canCreateComments: api.hasPrivilege('comments:create'),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
'favorite', e => this._evtFavoritePost(e));
this._view.sidebarControl.addEventListener(
'unfavorite', e => this._evtUnfavoritePost(e));
this._view.sidebarControl.addEventListener(
'score', e => this._evtScorePost(e));
this._view.sidebarControl.addEventListener(
'fitModeChange', e => this._evtFitModeChange(e));
this._view.sidebarControl.addEventListener(
'change', e => this._evtPostChange(e));
this._view.sidebarControl.addEventListener(
'submit', e => this._evtUpdatePost(e));
this._view.sidebarControl.addEventListener(
'feature', e => this._evtFeaturePost(e));
this._view.sidebarControl.addEventListener(
'delete', e => this._evtDeletePost(e));
this._view.sidebarControl.addEventListener(
'merge', e => this._evtMergePost(e));
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener(
'change', e => this._evtCommentChange(e));
this._view.commentControl.addEventListener(
'submit', e => this._evtCreateComment(e));
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
'submit', e => this._evtUpdateComment(e));
this._view.commentListControl.addEventListener(
'score', e => this._evtScoreComment(e));
this._view.commentListControl.addEventListener(
'delete', e => this._evtDeleteComment(e));
}
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
} }
_evtFitModeChange(e) { _evtFitModeChange(e) {
@ -133,74 +100,65 @@ class PostMainController extends BasePostController {
_evtFeaturePost(e) { _evtFeaturePost(e) {
this._view.sidebarControl.disableForm(); this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages(); this._view.sidebarControl.clearMessages();
e.detail.post.feature().then( e.detail.post.feature()
() => { .then(() => {
this._view.sidebarControl.showSuccess("Post featured."); this._view.sidebarControl.showSuccess('Post featured.');
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
}, }, error => {
(error) => {
this._view.sidebarControl.showError(error.message); this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
} });
);
} }
_evtMergePost(e) { _evtMergePost(e) {
router.show(uri.formatClientLink("post", e.detail.post.id, "merge")); router.show(uri.formatClientLink('post', e.detail.post.id, 'merge'));
} }
_evtDeletePost(e) { _evtDeletePost(e) {
this._view.sidebarControl.disableForm(); this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages(); this._view.sidebarControl.clearMessages();
e.detail.post.delete().then( e.detail.post.delete()
() => { .then(() => {
misc.disableExitConfirmation(); misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("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);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
} });
);
} }
_evtUpdatePost(e) { _evtUpdatePost(e) {
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.safety !== undefined && e.detail.safety !== null) { if (e.detail.safety !== undefined) {
post.safety = e.detail.safety; post.safety = e.detail.safety;
} }
if (e.detail.flags !== undefined && e.detail.flags !== null) { if (e.detail.flags !== undefined) {
post.flags = e.detail.flags; post.flags = e.detail.flags;
} }
if (e.detail.relations !== undefined && e.detail.relations !== null) { if (e.detail.relations !== undefined) {
post.relations = e.detail.relations; post.relations = e.detail.relations;
} }
if (e.detail.content !== undefined && e.detail.content !== null) { if (e.detail.content !== undefined) {
post.newContent = e.detail.content; post.newContent = e.detail.content;
} }
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) { if (e.detail.thumbnail !== undefined) {
post.newThumbnail = e.detail.thumbnail; post.newThumbnail = e.detail.thumbnail;
} }
if (e.detail.source !== undefined && e.detail.source !== null) { if (e.detail.source !== undefined) {
post.source = e.detail.source; post.source = e.detail.source;
} }
if (e.detail.desc !== undefined && e.detail.desc !== null) { post.save()
post.desc = e.detail.desc; .then(() => {
} this._view.sidebarControl.showSuccess('Post saved.');
post.save().then(
() => {
this._view.sidebarControl.showSuccess("Post saved.");
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
}, }, error => {
(error) => {
this._view.sidebarControl.showError(error.message); this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm(); this._view.sidebarControl.enableForm();
} });
);
} }
_evtPostChange(e) { _evtPostChange(e) {
@ -215,82 +173,79 @@ class PostMainController extends BasePostController {
this._view.commentControl.disableForm(); this._view.commentControl.disableForm();
const comment = Comment.create(this._post.id); const comment = Comment.create(this._post.id);
comment.text = e.detail.text; comment.text = e.detail.text;
comment.save().then( comment.save()
() => { .then(() => {
this._post.comments.add(comment); this._post.comments.add(comment);
this._view.commentControl.exitEditMode(); this._view.commentControl.exitEditMode();
this._view.commentControl.enableForm(); this._view.commentControl.enableForm();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
}, }, error => {
(error) => {
this._view.commentControl.showError(error.message); this._view.commentControl.showError(error.message);
this._view.commentControl.enableForm(); this._view.commentControl.enableForm();
} });
);
} }
_evtUpdateComment(e) { _evtUpdateComment(e) {
// TODO: disable form // TODO: disable form
e.detail.comment.text = e.detail.text; e.detail.comment.text = e.detail.text;
e.detail.comment.save().catch((error) => { e.detail.comment.save()
e.detail.target.showError(error.message); .catch(error => {
// TODO: enable form e.detail.target.showError(error.message);
}); // TODO: enable form
});
} }
_evtScoreComment(e) { _evtScoreComment(e) {
e.detail.comment e.detail.comment.setScore(e.detail.score)
.setScore(e.detail.score) .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
_evtDeleteComment(e) { _evtDeleteComment(e) {
e.detail.comment e.detail.comment.delete()
.delete() .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
_evtScorePost(e) { _evtScorePost(e) {
if (!api.hasPrivilege("posts:score")) { if (!api.hasPrivilege('posts:score')) {
return; return;
} }
e.detail.post e.detail.post.setScore(e.detail.score)
.setScore(e.detail.score) .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
_evtFavoritePost(e) { _evtFavoritePost(e) {
if (!api.hasPrivilege("posts:favorite")) { if (!api.hasPrivilege('posts:favorite')) {
return; return;
} }
e.detail.post e.detail.post.addToFavorites()
.addToFavorites() .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
_evtUnfavoritePost(e) { _evtUnfavoritePost(e) {
if (!api.hasPrivilege("posts:favorite")) { if (!api.hasPrivilege('posts:favorite')) {
return; return;
} }
e.detail.post e.detail.post.removeFromFavorites()
.removeFromFavorites() .catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["post", ":id", "edit"], (ctx, next) => { router.enter(['post', ':id', 'edit'],
// restore parameters from history state (ctx, next) => {
if (ctx.state.parameters) { // restore parameters from history state
Object.assign(ctx.parameters, ctx.state.parameters); if (ctx.state.parameters) {
} Object.assign(ctx.parameters, ctx.state.parameters);
ctx.controller = new PostMainController(ctx, true); }
}); ctx.controller = new PostMainController(ctx, true);
router.enter(["post", ":id"], (ctx, next) => { });
// restore parameters from history state router.enter(
if (ctx.state.parameters) { ['post', ':id'],
Object.assign(ctx.parameters, ctx.state.parameters); (ctx, next) => {
} // restore parameters from history state
ctx.controller = new PostMainController(ctx, false); if (ctx.state.parameters) {
}); Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, false);
});
}; };

View file

@ -1,40 +1,40 @@
"use strict"; 'use strict';
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 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 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');
const genericErrorMessage = const genericErrorMessage =
"One or more posts needs your attention; " + 'One of the posts needs your attention; ' +
'click "resume upload" when you\'re ready.'; 'click "resume upload" when you\'re ready.';
class PostUploadController { class PostUploadController {
constructor() { constructor() {
this._lastCancellablePromise = null; this._lastCancellablePromise = null;
if (!api.hasPrivilege("posts:create")) { if (!api.hasPrivilege('posts:create')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to upload posts."); this._view.showError('You don\'t have privileges to upload posts.');
return; return;
} }
topNavigation.activate("upload"); topNavigation.activate('upload');
topNavigation.setTitle("Upload"); topNavigation.setTitle('Upload');
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(), 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));
this._view.addEventListener("cancel", (e) => this._evtCancel(e)); this._view.addEventListener('cancel', e => this._evtCancel(e));
} }
_evtChange(e) { _evtChange(e) {
@ -55,130 +55,89 @@ class PostUploadController {
_evtSubmit(e) { _evtSubmit(e) {
this._view.disableForm(); this._view.disableForm();
this._view.clearMessages(); this._view.clearMessages();
let anyFailures = false;
e.detail.uploadables e.detail.uploadables.reduce(
.reduce( (promise, uploadable) =>
(promise, uploadable) => promise.then(() => this._uploadSinglePost(
promise.then(() => uploadable, e.detail.skipDuplicates)),
this._uploadSinglePost( Promise.resolve())
uploadable, .then(() => {
e.detail.skipDuplicates,
e.detail.alwaysUploadSimilar
).catch((error) => {
anyFailures = true;
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes =
error.similarPosts;
this._view.updateUploadable(
error.uploadable
);
this._view.showInfo(
error.message,
error.uploadable
);
} else {
this._view.showError(
error.message,
error.uploadable
);
}
} else {
this._view.showError(
error.message,
uploadable
);
}
if (e.detail.pauseRemainOnError) {
return Promise.reject();
}
})
),
Promise.resolve()
)
.then(() => {
if (anyFailures) {
return Promise.reject();
}
})
.then(
() => {
this._view.clearMessages(); this._view.clearMessages();
misc.disableExitConfirmation(); misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("posts")); const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess("Posts uploaded."); ctx.controller.showSuccess('Posts uploaded.');
}, }, error => {
(error) => { if (error.uploadable) {
this._view.showError(genericErrorMessage); if (error.similarPosts) {
error.uploadable.lookalikes = error.similarPosts;
this._view.updateUploadable(error.uploadable);
this._view.showInfo(genericErrorMessage);
this._view.showInfo(
error.message, error.uploadable);
} else {
this._view.showError(genericErrorMessage);
this._view.showError(
error.message, error.uploadable);
}
} else {
this._view.showError(error.message);
}
this._view.enableForm(); this._view.enableForm();
} });
);
} }
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) { _uploadSinglePost(uploadable, skipDuplicates) {
progress.start(); progress.start();
let reverseSearchPromise = Promise.resolve(); let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) { if (!uploadable.lookalikesConfirmed) {
reverseSearchPromise = Post.reverseSearch( reverseSearchPromise =
uploadable.url || uploadable.file Post.reverseSearch(uploadable.url || uploadable.file);
);
} }
this._lastCancellablePromise = reverseSearchPromise; this._lastCancellablePromise = reverseSearchPromise;
return reverseSearchPromise return reverseSearchPromise.then(searchResult => {
.then((searchResult) => { if (searchResult) {
if (searchResult) { // notify about exact duplicate
// notify about exact duplicate if (searchResult.exactPost) {
if (searchResult.exactPost) { if (skipDuplicates) {
if (skipDuplicates) { this._view.removeUploadable(uploadable);
this._view.removeUploadable(uploadable); return Promise.resolve();
return Promise.resolve(); } else {
} else { let error = new Error('Post already uploaded ' +
let error = new Error( `(@${searchResult.exactPost.id})`);
"Post already uploaded " +
`(@${searchResult.exactPost.id})`
);
error.uploadable = uploadable;
return Promise.reject(error);
}
}
// notify about similar posts
if (
searchResult.similarPosts.length &&
!alwaysUploadSimilar
) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload."
);
error.uploadable = uploadable; error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error); return Promise.reject(error);
} }
} }
// no duplicates, proceed with saving // notify about similar posts
let post = this._uploadableToPost(uploadable); if (searchResult.similarPosts.length) {
let savePromise = post.save(uploadable.anonymous).then(() => { let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
'posts.\nYou can resume or discard this upload.');
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
}
}
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous)
.then(() => {
this._view.removeUploadable(uploadable); this._view.removeUploadable(uploadable);
return Promise.resolve(); return Promise.resolve();
}); });
this._lastCancellablePromise = savePromise; this._lastCancellablePromise = savePromise;
return savePromise; return savePromise;
}) }).then(result => {
.then( progress.done();
(result) => { return Promise.resolve(result);
progress.done(); }, error => {
return Promise.resolve(result); error.uploadable = uploadable;
}, progress.done();
(error) => { return Promise.reject(error);
error.uploadable = uploadable; });
progress.done();
return Promise.reject(error);
}
);
} }
_uploadableToPost(uploadable) { _uploadableToPost(uploadable) {
@ -194,15 +153,13 @@ class PostUploadController {
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) // 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;` // you'll need to change the line below to `post.source = uploadable.source || uploadable.url;`
if (uploadable.url) { if (uploadable.url) post.source = 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();
}); });
}; };

View file

@ -1,28 +1,28 @@
"use strict"; 'use strict';
const settings = require("../models/settings.js"); const settings = require('../models/settings.js');
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const SettingsView = require("../views/settings_view.js"); const SettingsView = require('../views/settings_view.js');
class SettingsController { class SettingsController {
constructor() { constructor() {
topNavigation.activate("settings"); topNavigation.activate('settings');
topNavigation.setTitle("Browsing settings"); topNavigation.setTitle('Browsing settings');
this._view = new SettingsView({ this._view = new SettingsView({
settings: settings.get(), settings: settings.get(),
}); });
this._view.addEventListener("submit", (e) => this._evtSubmit(e)); this._view.addEventListener('submit', e => this._evtSubmit(e));
} }
_evtSubmit(e) { _evtSubmit(e) {
this._view.clearMessages(); this._view.clearMessages();
settings.save(e.detail); settings.save(e.detail);
this._view.showSuccess("Settings saved."); this._view.showSuccess('Settings saved.');
} }
} };
module.exports = (router) => { module.exports = router => {
router.enter(["settings"], (ctx, next) => { router.enter(['settings'], (ctx, next) => {
ctx.controller = new SettingsController(); ctx.controller = new SettingsController();
}); });
}; };

View file

@ -1,43 +1,41 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const uri = require("../util/uri.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');
const SnapshotsPageView = require("../views/snapshots_page_view.js"); const SnapshotsPageView = require('../views/snapshots_page_view.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class SnapshotsController { class SnapshotsController {
constructor(ctx) { constructor(ctx) {
if (!api.hasPrivilege("snapshots:list")) { if (!api.hasPrivilege('snapshots:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view history."); this._view.showError('You don\'t have privileges to view history.');
return; return;
} }
topNavigation.activate(""); topNavigation.activate('');
topNavigation.setTitle("History"); topNavigation.setTitle('History');
this._pageController = new PageController(); this._pageController = new PageController();
this._pageController.run({ this._pageController.run({
parameters: ctx.parameters, parameters: ctx.parameters,
defaultLimit: 25, defaultLimit: 25,
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, ctx.parameters, { const parameters = Object.assign(
offset: offset, {}, ctx.parameters, {offset: offset, limit: limit});
limit: limit, return uri.formatClientLink('history', parameters);
});
return uri.formatClientLink("history", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return SnapshotList.search("", offset, limit); return SnapshotList.search('', offset, limit);
}, },
pageRenderer: (pageCtx) => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"), canViewPosts: api.hasPrivilege('posts:view'),
canViewUsers: api.hasPrivilege("users:view"), canViewUsers: api.hasPrivilege('users:view'),
canViewTags: api.hasPrivilege("tags:view"), canViewTags: api.hasPrivilege('tags:view'),
}); });
return new SnapshotsPageView(pageCtx); return new SnapshotsPageView(pageCtx);
}, },
@ -45,8 +43,7 @@ class SnapshotsController {
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["history"], (ctx, next) => { router.enter(['history'],
ctx.controller = new SnapshotsController(ctx); (ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
});
}; };

View file

@ -1,68 +1,57 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const tags = require("../tags.js"); const tags = require('../tags.js');
const TagCategoryList = require("../models/tag_category_list.js"); const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const TagCategoriesView = require("../views/tag_categories_view.js"); const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class TagCategoriesController { class TagCategoriesController {
constructor() { constructor() {
if (!api.hasPrivilege("tagCategories:list")) { if (!api.hasPrivilege('tagCategories:list')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError( this._view.showError(
"You don't have privileges to view tag categories." 'You don\'t have privileges to view tag categories.');
);
return; return;
} }
topNavigation.activate("tags"); topNavigation.activate('tags');
topNavigation.setTitle("Listing tags"); topNavigation.setTitle('Listing tags');
TagCategoryList.get().then( TagCategoryList.get().then(response => {
(response) => { this._tagCategories = response.results;
this._tagCategories = response.results; this._view = new TagCategoriesView({
this._view = new TagCategoriesView({ tagCategories: this._tagCategories,
tagCategories: this._tagCategories, canEditName: api.hasPrivilege('tagCategories:edit:name'),
canEditName: api.hasPrivilege("tagCategories:edit:name"), canEditColor: api.hasPrivilege('tagCategories:edit:color'),
canEditColor: api.hasPrivilege("tagCategories:edit:color"), canDelete: api.hasPrivilege('tagCategories:delete'),
canEditOrder: api.hasPrivilege("tagCategories:edit:order"), canCreate: api.hasPrivilege('tagCategories:create'),
canDelete: api.hasPrivilege("tagCategories:delete"), canSetDefault: api.hasPrivilege('tagCategories:setDefault'),
canCreate: api.hasPrivilege("tagCategories:create"), });
canSetDefault: api.hasPrivilege( this._view.addEventListener('submit', e => this._evtSubmit(e));
"tagCategories:setDefault" }, error => {
), this._view = new EmptyView();
}); this._view.showError(error.message);
this._view.addEventListener("submit", (e) => });
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
} }
_evtSubmit(e) { _evtSubmit(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
this._tagCategories.save().then( this._tagCategories.save()
() => { .then(() => {
tags.refreshCategoryColorMap(); tags.refreshCategoryColorMap();
this._view.enableForm(); this._view.enableForm();
this._view.showSuccess("Changes saved."); this._view.showSuccess('Changes saved.');
}, }, error => {
(error) => {
this._view.enableForm(); this._view.enableForm();
this._view.showError(error.message); this._view.showError(error.message);
} });
);
} }
} }
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);
}); });
}; };

View file

@ -1,80 +1,63 @@
"use strict"; 'use strict';
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 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 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');
class TagController { class TagController {
constructor(ctx, section) { constructor(ctx, section) {
if (!api.hasPrivilege("tags:view")) { if (!api.hasPrivilege('tags:view')) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view tags."); this._view.showError('You don\'t have privileges to view tags.');
return; return;
} }
Promise.all([ Promise.all([
TagCategoryList.get(), TagCategoryList.get(),
Tag.get(ctx.parameters.name), Tag.get(ctx.parameters.name),
]).then( ]).then(responses => {
(responses) => { const [tagCategoriesResponse, tag] = responses;
const [tagCategoriesResponse, tag] = responses;
topNavigation.activate("tags"); topNavigation.activate('tags');
topNavigation.setTitle("Tag #" + tag.names[0]); topNavigation.setTitle('Tag #' + tag.names[0]);
this._name = ctx.parameters.name; this._name = ctx.parameters.name;
tag.addEventListener("change", (e) => tag.addEventListener('change', e => this._evtSaved(e, section));
this._evtSaved(e, section)
);
const categories = {}; const categories = {};
for (let category of tagCategoriesResponse.results) { for (let category of tagCategoriesResponse.results) {
categories[category.name] = category.name; categories[category.name] = category.name;
}
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege("tags:edit"),
canEditNames: api.hasPrivilege("tags:edit:names"),
canEditCategory: api.hasPrivilege("tags:edit:category"),
canEditImplications: api.hasPrivilege(
"tags:edit:implications"
),
canEditSuggestions: api.hasPrivilege(
"tags:edit:suggestions"
),
canEditDescription: api.hasPrivilege(
"tags:edit:description"
),
canMerge: api.hasPrivilege("tags:merge"),
canDelete: api.hasPrivilege("tags:delete"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
} }
);
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege('tags:edit'),
canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'),
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
canEditDescription: api.hasPrivilege('tags:edit:description'),
canMerge: api.hasPrivilege('tags:merge'),
canDelete: api.hasPrivilege('tags:delete'),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
} }
_evtChange(e) { _evtChange(e) {
@ -85,88 +68,75 @@ 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(
uri.formatClientLink("tag", e.detail.tag.names[0], section), uri.formatClientLink('tag', e.detail.tag.names[0], section),
null, null, false);
false
);
} }
} }
_evtUpdate(e) { _evtUpdate(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) { if (e.detail.names !== undefined) {
e.detail.tag.names = e.detail.names; e.detail.tag.names = e.detail.names;
} }
if (e.detail.category !== undefined && e.detail.category !== null) { if (e.detail.category !== undefined) {
e.detail.tag.category = e.detail.category; e.detail.tag.category = e.detail.category;
} }
if (e.detail.description !== undefined && e.detail.description !== null) { if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description; e.detail.tag.description = e.detail.description;
} }
e.detail.tag.save().then( e.detail.tag.save().then(() => {
() => { this._view.showSuccess('Tag saved.');
this._view.showSuccess("Tag saved."); this._view.enableForm();
this._view.enableForm(); }, error => {
}, this._view.showError(error.message);
(error) => { this._view.enableForm();
this._view.showError(error.message); });
this._view.enableForm();
}
);
} }
_evtMerge(e) { _evtMerge(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.tag.merge(e.detail.targetTagName, e.detail.addAlias).then( e.detail.tag
() => { .merge(e.detail.targetTagName, e.detail.addAlias)
this._view.showSuccess("Tag merged."); .then(() => {
this._view.showSuccess('Tag merged.');
this._view.enableForm(); this._view.enableForm();
router.replace( router.replace(
uri.formatClientLink( uri.formatClientLink(
"tag", 'tag', e.detail.targetTagName, 'merge'),
e.detail.targetTagName, null, false);
"merge" }, error => {
),
null,
false
);
},
(error) => {
this._view.showError(error.message); this._view.showError(error.message);
this._view.enableForm(); this._view.enableForm();
} });
);
} }
_evtDelete(e) { _evtDelete(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
e.detail.tag.delete().then( e.detail.tag.delete()
() => { .then(() => {
const ctx = router.show(uri.formatClientLink("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);
this._view.enableForm(); this._view.enableForm();
} });
);
} }
} }
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');
}); });
}; };

View file

@ -1,47 +1,44 @@
"use strict"; 'use strict';
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 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');
const TagsHeaderView = require("../views/tags_header_view.js"); const TagsHeaderView = require('../views/tags_header_view.js');
const TagsPageView = require("../views/tags_page_view.js"); 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", 'names',
"suggestions", 'suggestions',
"implications", 'implications',
"creationTime", 'creationTime',
"usages", 'usages',
"category", 'category'];
];
class TagListController { class TagListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController(); if (!api.hasPrivilege('tags:list')) {
if (!api.hasPrivilege("tags:list")) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view tags."); this._view.showError('You don\'t have privileges to view tags.');
return; return;
} }
this._ctx = ctx; topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
topNavigation.activate("tags"); this._ctx = ctx;
topNavigation.setTitle("Listing tags"); this._pageController = new PageController();
this._headerView = new TagsHeaderView({ this._headerView = new TagsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,
canEditTagCategories: api.hasPrivilege("tagCategories:edit"), canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
}); });
this._headerView.addEventListener("navigate", (e) => this._headerView.addEventListener(
this._evtNavigate(e) 'navigate', e => this._evtNavigate(e));
);
this._syncPageController(); this._syncPageController();
} }
@ -56,8 +53,7 @@ class TagListController {
_evtNavigate(e) { _evtNavigate(e) {
router.showNoDispatch( router.showNoDispatch(
uri.formatClientLink("tags", e.detail.parameters) uri.formatClientLink('tags', e.detail.parameters));
);
Object.assign(this._ctx.parameters, e.detail.parameters); Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController(); this._syncPageController();
} }
@ -67,29 +63,23 @@ class TagListController {
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
defaultLimit: 50, defaultLimit: 50,
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, { const parameters = Object.assign(
offset: offset, {}, this._ctx.parameters, {offset: offset, limit: limit});
limit: limit, return uri.formatClientLink('tags', parameters);
});
return uri.formatClientLink("tags", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return TagList.search( return TagList.search(
this._ctx.parameters.query, this._ctx.parameters.query, offset, limit, fields);
offset,
limit,
fields
);
}, },
pageRenderer: (pageCtx) => { pageRenderer: pageCtx => {
return new TagsPageView(pageCtx); return new TagsPageView(pageCtx);
}, },
}); });
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["tags"], (ctx, next) => { router.enter(
ctx.controller = new TagListController(ctx); ['tags'],
}); (ctx, next) => { ctx.controller = new TagListController(ctx); });
}; };

View file

@ -1,20 +1,19 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const topNavigation = require("../models/top_navigation.js"); const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require("../views/top_navigation_view.js"); const TopNavigationView = require('../views/top_navigation_view.js');
class TopNavigationController { class TopNavigationController {
constructor() { constructor() {
api.fetchConfig().then(() => { api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView(); this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener("activate", (e) => topNavigation.addEventListener(
this._evtActivate(e) 'activate', e => this._evtActivate(e));
);
api.addEventListener("login", (e) => this._evtAuthChange(e)); api.addEventListener('login', e => this._evtAuthChange(e));
api.addEventListener("logout", (e) => this._evtAuthChange(e)); api.addEventListener('logout', e => this._evtAuthChange(e));
this._render(); this._render();
}); });
@ -29,41 +28,37 @@ class TopNavigationController {
} }
_updateNavigationFromPrivileges() { _updateNavigationFromPrivileges() {
topNavigation.get("account").url = "user/" + api.userName; topNavigation.get('account').url = 'user/' + api.userName;
topNavigation.get("account").imageUrl = api.user topNavigation.get('account').imageUrl =
? api.user.avatarUrl api.user ? api.user.avatarUrl : null;
: null;
topNavigation.showAll(); topNavigation.showAll();
if (!api.hasPrivilege("posts:list")) { if (!api.hasPrivilege('posts:list')) {
topNavigation.hide("posts"); topNavigation.hide('posts');
} }
if (!api.hasPrivilege("posts:create")) { if (!api.hasPrivilege('posts:create')) {
topNavigation.hide("upload"); topNavigation.hide('upload');
} }
if (!api.hasPrivilege("comments:list")) { if (!api.hasPrivilege('comments:list')) {
topNavigation.hide("comments"); topNavigation.hide('comments');
} }
if (!api.hasPrivilege("tags:list")) { if (!api.hasPrivilege('tags:list')) {
topNavigation.hide("tags"); topNavigation.hide('tags');
} }
if (!api.hasPrivilege("users:list")) { if (!api.hasPrivilege('users:list')) {
topNavigation.hide("users"); topNavigation.hide('users');
}
if (!api.hasPrivilege("pools:list")) {
topNavigation.hide("pools");
} }
if (api.isLoggedIn()) { if (api.isLoggedIn()) {
if (!api.hasPrivilege("users:create:any")) { 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:self")) { if (!api.hasPrivilege('users:create:self')) {
topNavigation.hide("register"); topNavigation.hide('register');
} }
topNavigation.hide("account"); topNavigation.hide('account');
topNavigation.hide("logout"); topNavigation.hide('logout');
} }
} }
@ -71,11 +66,10 @@ class TopNavigationController {
this._updateNavigationFromPrivileges(); this._updateNavigationFromPrivileges();
this._topNavigationView.render({ this._topNavigationView.render({
items: topNavigation.getAll(), items: topNavigation.getAll(),
name: api.getName(), name: api.getName()
}); });
this._topNavigationView.activate( this._topNavigationView.activate(
topNavigation.activeItem ? topNavigation.activeItem.key : "" topNavigation.activeItem ? topNavigation.activeItem.key : '');
);
} }
} }

View file

@ -1,25 +1,23 @@
"use strict"; 'use strict';
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 uri = require('../util/uri.js');
const misc = require("../util/misc.js"); const misc = require('../util/misc.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 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');
class UserController { class UserController {
constructor(ctx, section) { constructor(ctx, section) {
const userName = ctx.parameters.name; const userName = ctx.parameters.name;
if ( if (!api.hasPrivilege('users:view') &&
!api.hasPrivilege("users:view") && !api.isLoggedIn({name: userName})) {
!api.isLoggedIn({ name: userName })
) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view users."); this._view.showError('You don\'t have privileges to view users.');
return; return;
} }
@ -27,128 +25,100 @@ class UserController {
this._errorMessages = []; this._errorMessages = [];
let userTokenPromise = Promise.resolve([]); let userTokenPromise = Promise.resolve([]);
if (section === "list-tokens") { if (section === 'list-tokens') {
userTokenPromise = UserToken.get(userName).then( userTokenPromise = UserToken.get(userName)
(userTokens) => { .then(userTokens => {
return userTokens.map((token) => { return userTokens.map(token => {
token.isCurrentAuthToken = token.isCurrentAuthToken = api.isCurrentAuthToken(token);
api.isCurrentAuthToken(token);
return token; return token;
}); });
}, }, error => {
(error) => {
return []; return [];
} });
);
} }
topNavigation.setTitle("User " + userName); topNavigation.setTitle('User ' + userName);
Promise.all([userTokenPromise, User.get(userName)]).then( Promise.all([
(responses) => { userTokenPromise,
const [userTokens, user] = responses; User.get(userName)
const isLoggedIn = api.isLoggedIn(user); ]).then(responses => {
const infix = isLoggedIn ? "self" : "any"; const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
this._name = userName; this._name = userName;
user.addEventListener("change", (e) => user.addEventListener('change', e => this._evtSaved(e, section));
this._evtSaved(e, section)
);
const myRankIndex = api.user const myRankIndex = api.user ?
? api.allRanks.indexOf(api.user.rank) api.allRanks.indexOf(api.user.rank) :
: 0; 0;
let ranks = {}; let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) { for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === "anonymous") { if (rankIdentifier === 'anonymous') {
continue; continue;
}
if (rankIdx > myRankIndex) {
continue;
}
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
} }
if (rankIdx > myRankIndex) {
if (isLoggedIn) { continue;
topNavigation.activate("account");
} else {
topNavigation.activate("users");
} }
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(
`users:edit:${infix}:pass`
),
canEditEmail: api.hasPrivilege(
`users:edit:${infix}:email`
),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(
`users:edit:${infix}:avatar`
),
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}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(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) => {
this._view = new EmptyView();
this._view.showError(error.message);
} }
);
if (isLoggedIn) {
topNavigation.activate('account');
} else {
topNavigation.activate('users');
}
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`),
canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
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}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(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 => {
this._view = new EmptyView();
this._view.showError(error.message);
});
} }
showSuccess(message) { showSuccess(message) {
if (typeof this._view === "undefined") { if (typeof this._view === 'undefined') {
this._successMessages.push(message); this._successMessages.push(message)
} else { } else {
this._view.showSuccess(message); this._view.showSuccess(message);
} }
} }
showError(message) { showError(message) {
if (typeof this._view === "undefined") { if (typeof this._view === 'undefined') {
this._errorMessages.push(message); this._errorMessages.push(message)
} else { } else {
this._view.showError(message); this._view.showError(message);
} }
@ -162,10 +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(
uri.formatClientLink("user", e.detail.user.name, section), uri.formatClientLink('user', e.detail.user.name, section),
null, null, false);
false
);
} }
} }
@ -173,128 +141,95 @@ class UserController {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user); const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? "self" : "any"; const infix = isLoggedIn ? 'self' : 'any';
if (e.detail.name !== undefined && e.detail.name !== null) { if (e.detail.name !== undefined) {
e.detail.user.name = e.detail.name; e.detail.user.name = e.detail.name;
} }
if (e.detail.email !== undefined && e.detail.email !== null) { if (e.detail.email !== undefined) {
e.detail.user.email = e.detail.email; e.detail.user.email = e.detail.email;
} }
if (e.detail.rank !== undefined && e.detail.rank !== null) { if (e.detail.rank !== undefined) {
e.detail.user.rank = e.detail.rank; e.detail.user.rank = e.detail.rank;
} }
if (e.detail.password !== undefined && e.detail.password !== null) { if (e.detail.password !== undefined) {
e.detail.user.password = e.detail.password; e.detail.user.password = e.detail.password;
} }
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) { if (e.detail.avatarStyle !== undefined) {
e.detail.user.avatarStyle = e.detail.avatarStyle; e.detail.user.avatarStyle = e.detail.avatarStyle;
if (e.detail.avatarContent) { if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent; e.detail.user.avatarContent = e.detail.avatarContent;
} }
} }
e.detail.user e.detail.user.save().then(() => {
.save() return isLoggedIn ?
.then(() => { api.login(
return isLoggedIn e.detail.name || api.userName,
? api.login( e.detail.password || api.userPassword,
e.detail.name || api.userName, false) :
e.detail.password || api.userPassword, Promise.resolve();
false }).then(() => {
) this._view.showSuccess('Settings updated.');
: Promise.resolve(); this._view.enableForm();
}) }, error => {
.then( this._view.showError(error.message);
() => { this._view.enableForm();
this._view.showSuccess("Settings updated."); });
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
_evtDelete(e) { _evtDelete(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user); const isLoggedIn = api.isLoggedIn(e.detail.user);
e.detail.user.delete().then( e.detail.user.delete()
() => { .then(() => {
if (isLoggedIn) { if (isLoggedIn) {
api.forget(); api.forget();
api.logout(); api.logout();
} }
if (api.hasPrivilege("users:list")) { if (api.hasPrivilege('users:list')) {
const ctx = router.show(uri.formatClientLink("users")); const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess("Account deleted."); ctx.controller.showSuccess('Account deleted.');
} else { } else {
const ctx = router.show(uri.formatClientLink()); const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("Account deleted."); ctx.controller.showSuccess('Account deleted.');
} }
}, }, error => {
(error) => {
this._view.showError(error.message); this._view.showError(error.message);
this._view.enableForm(); this._view.enableForm();
} });
);
} }
_evtCreateToken(e) { _evtCreateToken(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
UserToken.create( UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
e.detail.user.name, .then(response => {
e.detail.note, const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
e.detail.expirationTime ctx.controller.showSuccess('Token ' + response.token + ' created.');
).then( }, error => {
(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.showError(error.message);
this._view.enableForm(); this._view.enableForm();
} });
);
} }
_evtDeleteToken(e) { _evtDeleteToken(e) {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (api.isCurrentAuthToken(e.detail.userToken)) { if (api.isCurrentAuthToken(e.detail.userToken)) {
router.show(uri.formatClientLink("logout")); router.show(uri.formatClientLink('logout'));
} else { } else {
e.detail.userToken.delete(e.detail.user.name).then( e.detail.userToken.delete(e.detail.user.name)
() => { .then(() => {
const ctx = router.show( const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
uri.formatClientLink( ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
"user", }, error => {
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + e.detail.userToken.token + " deleted."
);
},
(error) => {
this._view.showError(error.message); this._view.showError(error.message);
this._view.enableForm(); this._view.enableForm();
} });
);
} }
} }
@ -302,42 +237,31 @@ class UserController {
this._view.clearMessages(); this._view.clearMessages();
this._view.disableForm(); this._view.disableForm();
if (e.detail.note !== undefined && e.detail.note !== null) { if (e.detail.note !== undefined) {
e.detail.userToken.note = e.detail.note; e.detail.userToken.note = e.detail.note;
} }
e.detail.userToken.save(e.detail.user.name).then( e.detail.userToken.save(e.detail.user.name).then(response => {
(response) => { const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
const ctx = router.show( ctx.controller.showSuccess('Token ' + response.token + ' updated.');
uri.formatClientLink( }, error => {
"user", this._view.showError(error.message);
e.detail.user.name, this._view.enableForm();
"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", "list-tokens"], (ctx, next) => { router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
ctx.controller = new UserController(ctx, "list-tokens"); ctx.controller = new UserController(ctx, 'list-tokens');
}); });
router.enter(["user", ":name", "delete"], (ctx, next) => { router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, "delete"); ctx.controller = new UserController(ctx, 'delete');
}); });
}; };

View file

@ -1,37 +1,35 @@
"use strict"; 'use strict';
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 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');
const UsersHeaderView = require("../views/users_header_view.js"); const UsersHeaderView = require('../views/users_header_view.js');
const UsersPageView = require("../views/users_page_view.js"); const UsersPageView = require('../views/users_page_view.js');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class UserListController { class UserListController {
constructor(ctx) { constructor(ctx) {
this._pageController = new PageController(); if (!api.hasPrivilege('users:list')) {
if (!api.hasPrivilege("users:list")) {
this._view = new EmptyView(); this._view = new EmptyView();
this._view.showError("You don't have privileges to view users."); this._view.showError('You don\'t have privileges to view users.');
return; return;
} }
topNavigation.activate("users"); topNavigation.activate('users');
topNavigation.setTitle("Listing users"); topNavigation.setTitle('Listing users');
this._ctx = ctx; this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new UsersHeaderView({ this._headerView = new UsersHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode, hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters, parameters: ctx.parameters,
}); });
this._headerView.addEventListener("navigate", (e) => this._headerView.addEventListener(
this._evtNavigate(e) 'navigate', e => this._evtNavigate(e));
);
this._syncPageController(); this._syncPageController();
} }
@ -42,8 +40,7 @@ class UserListController {
_evtNavigate(e) { _evtNavigate(e) {
router.showNoDispatch( router.showNoDispatch(
uri.formatClientLink("users", e.detail.parameters) uri.formatClientLink('users', e.detail.parameters));
);
Object.assign(this._ctx.parameters, e.detail.parameters); Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController(); this._syncPageController();
} }
@ -53,22 +50,17 @@ class UserListController {
parameters: this._ctx.parameters, parameters: this._ctx.parameters,
defaultLimit: 30, defaultLimit: 30,
getClientUrlForPage: (offset, limit) => { getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, { const parameters = Object.assign(
offset: offset, {}, this._ctx.parameters, {offset, offset, limit: limit});
limit: limit, return uri.formatClientLink('users', parameters);
});
return uri.formatClientLink("users", parameters);
}, },
requestPage: (offset, limit) => { requestPage: (offset, limit) => {
return UserList.search( return UserList.search(
this._ctx.parameters.query, this._ctx.parameters.query, offset, limit);
offset,
limit
);
}, },
pageRenderer: (pageCtx) => { pageRenderer: pageCtx => {
Object.assign(pageCtx, { Object.assign(pageCtx, {
canViewUsers: api.hasPrivilege("users:view"), canViewUsers: api.hasPrivilege('users:view'),
}); });
return new UsersPageView(pageCtx); return new UsersPageView(pageCtx);
}, },
@ -76,8 +68,8 @@ class UserListController {
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["users"], (ctx, next) => { router.enter(
ctx.controller = new UserListController(ctx); ['users'],
}); (ctx, next) => { ctx.controller = new UserListController(ctx); });
}; };

View file

@ -1,25 +1,25 @@
"use strict"; 'use strict';
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 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');
const EmptyView = require("../views/empty_view.js"); const EmptyView = require('../views/empty_view.js');
class UserRegistrationController { class UserRegistrationController {
constructor() { constructor() {
if (!api.hasPrivilege("users:create:self")) { 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;
} }
topNavigation.activate("register"); topNavigation.activate('register');
topNavigation.setTitle("Registration"); topNavigation.setTitle('Registration');
this._view = new RegistrationView(); this._view = new RegistrationView();
this._view.addEventListener("submit", (e) => this._evtRegister(e)); this._view.addEventListener('submit', e => this._evtRegister(e));
} }
_evtRegister(e) { _evtRegister(e) {
@ -30,35 +30,30 @@ class UserRegistrationController {
user.email = e.detail.email; user.email = e.detail.email;
user.password = e.detail.password; user.password = e.detail.password;
const isLoggedIn = api.isLoggedIn(); const isLoggedIn = api.isLoggedIn();
user.save() user.save().then(() => {
.then(() => { if (isLoggedIn) {
if (isLoggedIn) { return Promise.resolve();
return Promise.resolve(); } else {
} 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(() => {
}) if (isLoggedIn) {
.then( const ctx = router.show(uri.formatClientLink('users'));
() => { ctx.controller.showSuccess('User added!');
if (isLoggedIn) { } else {
const ctx = router.show(uri.formatClientLink("users")); const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("User added!"); ctx.controller.showSuccess('Welcome aboard!');
} else { }
const ctx = router.show(uri.formatClientLink()); }, error => {
ctx.controller.showSuccess("Welcome aboard!"); this._view.showError(error.message);
} this._view.enableForm();
}, });
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
} }
} }
module.exports = (router) => { module.exports = router => {
router.enter(["register"], (ctx, next) => { router.enter(['register'], (ctx, next) => {
new UserRegistrationController(); new UserRegistrationController();
}); });
}; };

View file

@ -1,6 +1,6 @@
"use strict"; 'use strict';
const views = require("../util/views.js"); const views = require('../util/views.js');
const KEY_TAB = 9; const KEY_TAB = 9;
const KEY_RETURN = 13; const KEY_RETURN = 13;
@ -10,14 +10,14 @@ const KEY_UP = 38;
const KEY_DOWN = 40; const KEY_DOWN = 40;
function _getSelectionStart(input) { function _getSelectionStart(input) {
if ("selectionStart" in input) { if ('selectionStart' in input) {
return input.selectionStart; return input.selectionStart;
} }
if (document.selection) { if (document.selection) {
input.focus(); input.focus();
const sel = document.selection.createRange(); const sel = document.selection.createRange();
const selLen = document.selection.createRange().text.length; const selLen = document.selection.createRange().text.length;
sel.moveStart("character", -input.value.length); sel.moveStart('character', -input.value.length);
return sel.text.length - selLen; return sel.text.length - selLen;
} }
return 0; return 0;
@ -27,22 +27,18 @@ class AutoCompleteControl {
constructor(sourceInputNode, options) { constructor(sourceInputNode, options) {
this._sourceInputNode = sourceInputNode; this._sourceInputNode = sourceInputNode;
this._options = {}; this._options = {};
Object.assign( Object.assign(this._options, {
this._options, verticalShift: 2,
{ maxResults: 15,
verticalShift: 2, getTextToFind: () => {
maxResults: 15, const value = sourceInputNode.value;
getTextToFind: () => { const start = _getSelectionStart(sourceInputNode);
const value = sourceInputNode.value; return value.substring(0, start).replace(/.*\s+/, '');
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, "");
},
confirm: null,
delete: null,
getMatches: null,
}, },
options confirm: null,
); delete: null,
getMatches: null,
}, options);
this._showTimeout = null; this._showTimeout = null;
this._results = []; this._results = [];
@ -53,25 +49,22 @@ class AutoCompleteControl {
hide() { hide() {
window.clearTimeout(this._showTimeout); window.clearTimeout(this._showTimeout);
this._suggestionDiv.style.display = "none"; this._suggestionDiv.style.display = 'none';
this._isVisible = false; this._isVisible = false;
} }
replaceSelectedText(result, addSpace) { 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);
let middle = this._sourceInputNode.value.substring(0, start); let middle = this._sourceInputNode.value.substring(0, start);
const spaceIndex = middle.lastIndexOf(" "); const index = middle.lastIndexOf(' ');
const commaIndex = middle.lastIndexOf(",");
const index = spaceIndex < commaIndex ? commaIndex : spaceIndex;
const delimiter = spaceIndex < commaIndex ? "" : " ";
if (index !== -1) { if (index !== -1) {
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 = this._sourceInputNode.value = (
prefix + result.toString() + delimiter + suffix.trimLeft(); prefix + result.toString() + ' ' + suffix.trimLeft());
if (!addSpace) { if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim(); this._sourceInputNode.value = this._sourceInputNode.value.trim();
} }
@ -93,7 +86,7 @@ class AutoCompleteControl {
} }
_show() { _show() {
this._suggestionDiv.style.display = "block"; this._suggestionDiv.style.display = 'block';
this._isVisible = true; this._isVisible = true;
} }
@ -108,32 +101,27 @@ class AutoCompleteControl {
_install() { _install() {
if (!this._sourceInputNode) { if (!this._sourceInputNode) {
throw new Error("Input element was not found"); throw new Error('Input element was not found');
} }
if (this._sourceInputNode.getAttribute("data-autocomplete")) { if (this._sourceInputNode.getAttribute('data-autocomplete')) {
throw new Error( throw new Error(
"Autocompletion was already added for this element" 'Autocompletion was already added for this element');
);
} }
this._sourceInputNode.setAttribute("data-autocomplete", true); this._sourceInputNode.setAttribute('data-autocomplete', true);
this._sourceInputNode.setAttribute("autocomplete", "off"); this._sourceInputNode.setAttribute('autocomplete', 'off');
this._sourceInputNode.addEventListener("keydown", (e) => this._sourceInputNode.addEventListener(
this._evtKeyDown(e) 'keydown', e => this._evtKeyDown(e));
); this._sourceInputNode.addEventListener(
this._sourceInputNode.addEventListener("blur", (e) => 'blur', e => this._evtBlur(e));
this._evtBlur(e)
);
this._suggestionDiv = views.htmlToDom( this._suggestionDiv = views.htmlToDom(
'<div class="autocomplete"><ul></ul></div>' '<div class="autocomplete"><ul></ul></div>');
); this._suggestionList = this._suggestionDiv.querySelector('ul');
this._suggestionList = this._suggestionDiv.querySelector("ul");
document.body.appendChild(this._suggestionDiv); document.body.appendChild(this._suggestionDiv);
views.monitorNodeRemoval(this._sourceInputNode, () => { views.monitorNodeRemoval(
this._uninstall(); this._sourceInputNode, () => { this._uninstall(); });
});
} }
_uninstall() { _uninstall() {
@ -149,21 +137,13 @@ class AutoCompleteControl {
if (key === KEY_ESCAPE) { if (key === KEY_ESCAPE) {
func = this.hide; func = this.hide;
} else if (key === KEY_TAB && shift) { } else if (key === KEY_TAB && shift) {
func = () => { func = () => { this._selectPrevious(); };
this._selectPrevious();
};
} else if (key === KEY_TAB && !shift) { } else if (key === KEY_TAB && !shift) {
func = () => { func = () => { this._selectNext(); };
this._selectNext();
};
} else if (key === KEY_UP) { } else if (key === KEY_UP) {
func = () => { func = () => { this._selectPrevious(); };
this._selectPrevious();
};
} else if (key === KEY_DOWN) { } else if (key === KEY_DOWN) {
func = () => { func = () => { this._selectNext(); };
this._selectNext();
};
} else if (key === KEY_RETURN && this._activeResult >= 0) { } else if (key === KEY_RETURN && this._activeResult >= 0) {
func = () => { func = () => {
this._confirm(this._getActiveSuggestion()); this._confirm(this._getActiveSuggestion());
@ -184,17 +164,14 @@ class AutoCompleteControl {
func(); func();
} else { } else {
window.clearTimeout(this._showTimeout); window.clearTimeout(this._showTimeout);
this._showTimeout = window.setTimeout(() => { this._showTimeout = window.setTimeout(
this._showOrHide(); () => { this._showOrHide(); }, 250);
}, 250);
} }
} }
_evtBlur(e) { _evtBlur(e) {
window.clearTimeout(this._showTimeout); window.clearTimeout(this._showTimeout);
window.setTimeout(() => { window.setTimeout(() => { this.hide(); }, 50);
this.hide();
}, 50);
} }
_getActiveSuggestion() { _getActiveSuggestion() {
@ -205,11 +182,9 @@ class AutoCompleteControl {
} }
_selectPrevious() { _selectPrevious() {
this._select( this._select(this._activeResult === -1 ?
this._activeResult === -1 this._results.length - 1 :
? this._results.length - 1 this._activeResult - 1);
: this._activeResult - 1
);
} }
_selectNext() { _selectNext() {
@ -217,18 +192,15 @@ class AutoCompleteControl {
} }
_select(newActiveResult) { _select(newActiveResult) {
this._activeResult = newActiveResult.between( this._activeResult =
0, newActiveResult.between(0, this._results.length - 1, true) ?
this._results.length - 1, newActiveResult :
true -1;
)
? newActiveResult
: -1;
this._refreshActiveResult(); this._refreshActiveResult();
} }
_updateResults(textToFind) { _updateResults(textToFind) {
this._options.getMatches(textToFind).then((matches) => { this._options.getMatches(textToFind).then(matches => {
const oldResults = this._results.slice(); const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults); this._results = matches.slice(0, this._options.maxResults);
const oldResultsHash = JSON.stringify(oldResults); const oldResultsHash = JSON.stringify(oldResults);
@ -251,30 +223,34 @@ class AutoCompleteControl {
} }
for (let [resultIndex, resultItem] of this._results.entries()) { for (let [resultIndex, resultItem] of this._results.entries()) {
let resultIndexWorkaround = resultIndex; let resultIndexWorkaround = resultIndex;
const listItem = document.createElement("li"); const listItem = document.createElement('li');
const link = document.createElement("a"); const link = document.createElement('a');
link.innerHTML = resultItem.caption; link.innerHTML = resultItem.caption;
link.setAttribute("href", ""); link.setAttribute('href', '');
link.setAttribute("data-key", resultItem.value); link.setAttribute('data-key', resultItem.value);
link.addEventListener("mouseenter", (e) => { link.addEventListener(
e.preventDefault(); 'mouseenter',
this._activeResult = resultIndexWorkaround; e => {
this._refreshActiveResult(); e.preventDefault();
}); this._activeResult = resultIndexWorkaround;
link.addEventListener("mousedown", (e) => { this._refreshActiveResult();
e.preventDefault(); });
this._activeResult = resultIndexWorkaround; link.addEventListener(
this._confirm(this._getActiveSuggestion()); 'mousedown',
this.hide(); e => {
}); e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
this.hide();
});
listItem.appendChild(link); listItem.appendChild(link);
this._suggestionList.appendChild(listItem); this._suggestionList.appendChild(listItem);
} }
this._refreshActiveResult(); this._refreshActiveResult();
// display the suggestions offscreen to get the height // display the suggestions offscreen to get the height
this._suggestionDiv.style.left = "-9999px"; this._suggestionDiv.style.left = '-9999px';
this._suggestionDiv.style.top = "-9999px"; this._suggestionDiv.style.top = '-9999px';
this._show(); this._show();
const verticalShift = this._options.verticalShift; const verticalShift = this._options.verticalShift;
const inputRect = this._sourceInputNode.getBoundingClientRect(); const inputRect = this._sourceInputNode.getBoundingClientRect();
@ -288,44 +264,38 @@ class AutoCompleteControl {
inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1; inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
let x = inputRect.left - bodyRect.left; let x = inputRect.left - bodyRect.left;
let y = let y = direction == 1 ?
direction === 1 inputRect.bottom - bodyRect.top - verticalShift :
? inputRect.bottom - bodyRect.top - verticalShift inputRect.top - bodyRect.top - listRect.height + verticalShift;
: inputRect.top -
bodyRect.top -
listRect.height +
verticalShift;
// remove offscreen items until whole suggestion list can fit on the // remove offscreen items until whole suggestion list can fit on the
// screen // screen
while ( while ((y < 0 || y + listRect.height > viewPortHeight) &&
(y < 0 || y + listRect.height > viewPortHeight) && this._suggestionList.childNodes.length) {
this._suggestionList.childNodes.length
) {
this._suggestionList.removeChild(this._suggestionList.lastChild); this._suggestionList.removeChild(this._suggestionList.lastChild);
const prevHeight = listRect.height; const prevHeight = listRect.height;
listRect = this._suggestionDiv.getBoundingClientRect(); listRect = this._suggestionDiv.getBoundingClientRect();
const heightDelta = prevHeight - listRect.height; const heightDelta = prevHeight - listRect.height;
if (direction === -1) { if (direction == -1) {
y += heightDelta; y += heightDelta;
} }
} }
this._suggestionDiv.style.left = x + "px"; this._suggestionDiv.style.left = x + 'px';
this._suggestionDiv.style.top = y + "px"; this._suggestionDiv.style.top = y + 'px';
} }
_refreshActiveResult() { _refreshActiveResult() {
let activeItem = this._suggestionList.querySelector("li.active"); let activeItem = this._suggestionList.querySelector('li.active');
if (activeItem) { if (activeItem) {
activeItem.classList.remove("active"); activeItem.classList.remove('active');
} }
if (this._activeResult >= 0) { if (this._activeResult >= 0) {
const allItems = this._suggestionList.querySelectorAll("li"); const allItems = this._suggestionList.querySelectorAll('li');
activeItem = allItems[this._activeResult]; activeItem = allItems[this._activeResult];
activeItem.classList.add("active"); activeItem.classList.add('active');
} }
} }
} };
module.exports = AutoCompleteControl; module.exports = AutoCompleteControl;

View file

@ -1,12 +1,12 @@
"use strict"; 'use strict';
const api = require("../api.js"); const api = require('../api.js');
const misc = require("../util/misc.js"); const misc = require('../util/misc.js');
const events = require("../events.js"); const events = require('../events.js');
const views = require("../util/views.js"); const views = require('../util/views.js');
const template = views.getTemplate("comment"); const template = views.getTemplate('comment');
const scoreTemplate = views.getTemplate("score"); const scoreTemplate = views.getTemplate('score');
class CommentControl extends events.EventTarget { class CommentControl extends events.EventTarget {
constructor(hostNode, comment, onlyEditing) { constructor(hostNode, comment, onlyEditing) {
@ -16,111 +16,104 @@ class CommentControl extends events.EventTarget {
this._onlyEditing = onlyEditing; this._onlyEditing = onlyEditing;
if (comment) { if (comment) {
comment.addEventListener("change", (e) => this._evtChange(e)); comment.addEventListener(
comment.addEventListener("changeScore", (e) => 'change', e => this._evtChange(e));
this._evtChangeScore(e) comment.addEventListener(
); 'changeScore', e => this._evtChangeScore(e));
} }
const isLoggedIn = comment && api.isLoggedIn(comment.user); const isLoggedIn = comment && api.isLoggedIn(comment.user);
const infix = isLoggedIn ? "own" : "any"; const infix = isLoggedIn ? 'own' : 'any';
views.replaceContent( views.replaceContent(this._hostNode, template({
this._hostNode, comment: comment,
template({ user: comment ? comment.user : api.user,
comment: comment, canViewUsers: api.hasPrivilege('users:view'),
user: comment ? comment.user : api.user, canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canViewUsers: api.hasPrivilege("users:view"), canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`), onlyEditing: onlyEditing,
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`), }));
onlyEditing: onlyEditing,
})
);
if (this._editButtonNodes) { if (this._editButtonNodes) {
for (let node of this._editButtonNodes) { for (let node of this._editButtonNodes) {
node.addEventListener("click", (e) => this._evtEditClick(e)); node.addEventListener('click', e => this._evtEditClick(e));
} }
} }
if (this._deleteButtonNode) { if (this._deleteButtonNode) {
this._deleteButtonNode.addEventListener("click", (e) => this._deleteButtonNode.addEventListener(
this._evtDeleteClick(e) 'click', e => this._evtDeleteClick(e));
);
} }
if (this._previewEditingButtonNode) { if (this._previewEditingButtonNode) {
this._previewEditingButtonNode.addEventListener("click", (e) => this._previewEditingButtonNode.addEventListener(
this._evtPreviewEditingClick(e) 'click', e => this._evtPreviewEditingClick(e));
);
} }
if (this._saveChangesButtonNode) { if (this._saveChangesButtonNode) {
this._saveChangesButtonNode.addEventListener("click", (e) => this._saveChangesButtonNode.addEventListener(
this._evtSaveChangesClick(e) 'click', e => this._evtSaveChangesClick(e));
);
} }
if (this._cancelEditingButtonNode) { if (this._cancelEditingButtonNode) {
this._cancelEditingButtonNode.addEventListener("click", (e) => this._cancelEditingButtonNode.addEventListener(
this._evtCancelEditingClick(e) 'click', e => this._evtCancelEditingClick(e));
);
} }
this._installScore(); this._installScore();
if (onlyEditing) { if (onlyEditing) {
this._selectNav("edit"); this._selectNav('edit');
this._selectTab("edit"); this._selectTab('edit');
} else { } else {
this._selectNav("readonly"); this._selectNav('readonly');
this._selectTab("preview"); this._selectTab('preview');
} }
} }
get _formNode() { get _formNode() {
return this._hostNode.querySelector("form"); return this._hostNode.querySelector('form');
} }
get _scoreContainerNode() { get _scoreContainerNode() {
return this._hostNode.querySelector(".score-container"); return this._hostNode.querySelector('.score-container');
} }
get _editButtonNodes() { get _editButtonNodes() {
return this._hostNode.querySelectorAll("li.edit>a, a.edit"); return this._hostNode.querySelectorAll('li.edit>a, a.edit');
} }
get _previewEditingButtonNode() { get _previewEditingButtonNode() {
return this._hostNode.querySelector("li.preview>a"); return this._hostNode.querySelector('li.preview>a');
} }
get _deleteButtonNode() { get _deleteButtonNode() {
return this._hostNode.querySelector(".delete"); return this._hostNode.querySelector('.delete');
} }
get _upvoteButtonNode() { get _upvoteButtonNode() {
return this._hostNode.querySelector(".upvote"); return this._hostNode.querySelector('.upvote');
} }
get _downvoteButtonNode() { get _downvoteButtonNode() {
return this._hostNode.querySelector(".downvote"); return this._hostNode.querySelector('.downvote');
} }
get _saveChangesButtonNode() { get _saveChangesButtonNode() {
return this._hostNode.querySelector(".save-changes"); return this._hostNode.querySelector('.save-changes');
} }
get _cancelEditingButtonNode() { get _cancelEditingButtonNode() {
return this._hostNode.querySelector(".cancel-editing"); return this._hostNode.querySelector('.cancel-editing');
} }
get _textareaNode() { get _textareaNode() {
return this._hostNode.querySelector(".tab.edit textarea"); return this._hostNode.querySelector('.tab.edit textarea');
} }
get _contentNode() { get _contentNode() {
return this._hostNode.querySelector(".tab.preview .comment-content"); return this._hostNode.querySelector('.tab.preview .comment-content');
} }
get _heightKeeperNode() { get _heightKeeperNode() {
return this._hostNode.querySelector(".keep-height"); return this._hostNode.querySelector('.keep-height');
} }
_installScore() { _installScore() {
@ -129,35 +122,32 @@ class CommentControl extends events.EventTarget {
scoreTemplate({ scoreTemplate({
score: this._comment ? this._comment.score : 0, score: this._comment ? this._comment.score : 0,
ownScore: this._comment ? this._comment.ownScore : 0, ownScore: this._comment ? this._comment.ownScore : 0,
canScore: api.hasPrivilege("comments:score"), canScore: api.hasPrivilege('comments:score'),
}) }));
);
if (this._upvoteButtonNode) { if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener("click", (e) => this._upvoteButtonNode.addEventListener(
this._evtScoreClick(e, 1) 'click', e => this._evtScoreClick(e, 1));
);
} }
if (this._downvoteButtonNode) { if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener("click", (e) => this._downvoteButtonNode.addEventListener(
this._evtScoreClick(e, -1) 'click', e => this._evtScoreClick(e, -1));
);
} }
} }
enterEditMode() { enterEditMode() {
this._selectNav("edit"); this._selectNav('edit');
this._selectTab("edit"); this._selectTab('edit');
} }
exitEditMode() { exitEditMode() {
if (this._onlyEditing) { if (this._onlyEditing) {
this._selectNav("edit"); this._selectNav('edit');
this._selectTab("edit"); this._selectTab('edit');
this._setText(""); this._setText('');
} else { } else {
this._selectNav("readonly"); this._selectNav('readonly');
this._selectTab("preview"); this._selectTab('preview');
this._setText(this._comment.text); this._setText(this._comment.text);
} }
this._forgetHeight(); this._forgetHeight();
@ -183,31 +173,27 @@ class CommentControl extends events.EventTarget {
_evtScoreClick(e, score) { _evtScoreClick(e, score) {
e.preventDefault(); e.preventDefault();
if (!api.hasPrivilege("comments:score")) { if (!api.hasPrivilege('comments:score')) {
return; return;
} }
this.dispatchEvent( this.dispatchEvent(new CustomEvent('score', {
new CustomEvent("score", { detail: {
detail: { comment: this._comment,
comment: this._comment, score: this._comment.ownScore === score ? 0 : score,
score: this._comment.ownScore === score ? 0 : score, },
}, }));
})
);
} }
_evtDeleteClick(e) { _evtDeleteClick(e) {
e.preventDefault(); e.preventDefault();
if (!window.confirm("Are you sure you want to delete this comment?")) { if (!window.confirm('Are you sure you want to delete this comment?')) {
return; return;
} }
this.dispatchEvent( this.dispatchEvent(new CustomEvent('delete', {
new CustomEvent("delete", { detail: {
detail: { comment: this._comment,
comment: this._comment, },
}, }));
})
);
} }
_evtChange(e) { _evtChange(e) {
@ -220,24 +206,26 @@ class CommentControl extends events.EventTarget {
_evtPreviewEditingClick(e) { _evtPreviewEditingClick(e) {
e.preventDefault(); e.preventDefault();
this._contentNode.innerHTML = misc.formatMarkdown( this._contentNode.innerHTML =
this._textareaNode.value misc.formatMarkdown(this._textareaNode.value);
); this._selectTab('edit');
this._selectTab("edit"); this._selectTab('preview');
this._selectTab("preview"); }
_evtEditClick(e) {
e.preventDefault();
this.enterEditMode();
} }
_evtSaveChangesClick(e) { _evtSaveChangesClick(e) {
e.preventDefault(); e.preventDefault();
this.dispatchEvent( this.dispatchEvent(new CustomEvent('submit', {
new CustomEvent("submit", { detail: {
detail: { target: this,
target: this, comment: this._comment,
comment: this._comment, text: this._textareaNode.value,
text: this._textareaNode.value, },
}, }));
})
);
} }
_evtCancelEditingClick(e) { _evtCancelEditingClick(e) {
@ -251,27 +239,27 @@ class CommentControl extends events.EventTarget {
} }
_selectNav(modeName) { _selectNav(modeName) {
for (let node of this._hostNode.querySelectorAll("nav")) { for (let node of this._hostNode.querySelectorAll('nav')) {
node.classList.toggle("active", node.classList.contains(modeName)); node.classList.toggle('active', node.classList.contains(modeName));
} }
} }
_selectTab(tabName) { _selectTab(tabName) {
this._ensureHeight(); this._ensureHeight();
for (let node of this._hostNode.querySelectorAll(".tab, .tabs li")) { for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) {
node.classList.toggle("active", node.classList.contains(tabName)); node.classList.toggle('active', node.classList.contains(tabName));
} }
} }
_ensureHeight() { _ensureHeight() {
this._heightKeeperNode.style.minHeight = this._heightKeeperNode.style.minHeight =
this._heightKeeperNode.getBoundingClientRect().height + "px"; this._heightKeeperNode.getBoundingClientRect().height + 'px';
} }
_forgetHeight() { _forgetHeight() {
this._heightKeeperNode.style.minHeight = null; this._heightKeeperNode.style.minHeight = null;
} }
} };
module.exports = CommentControl; module.exports = CommentControl;

View file

@ -1,10 +1,10 @@
"use strict"; 'use strict';
const events = require("../events.js"); const events = require('../events.js');
const views = require("../util/views.js"); const views = require('../util/views.js');
const CommentControl = require("../controls/comment_control.js"); const CommentControl = require('../controls/comment_control.js');
const template = views.getTemplate("comment-list"); const template = views.getTemplate('comment-list');
class CommentListControl extends events.EventTarget { class CommentListControl extends events.EventTarget {
constructor(hostNode, comments, reversed) { constructor(hostNode, comments, reversed) {
@ -13,8 +13,8 @@ class CommentListControl extends events.EventTarget {
this._comments = comments; this._comments = comments;
this._commentIdToNode = {}; this._commentIdToNode = {};
comments.addEventListener("add", (e) => this._evtAdd(e)); comments.addEventListener('add', e => this._evtAdd(e));
comments.addEventListener("remove", (e) => this._evtRemove(e)); comments.addEventListener('remove', e => this._evtRemove(e));
views.replaceContent(this._hostNode, template()); views.replaceContent(this._hostNode, template());
@ -28,19 +28,16 @@ class CommentListControl extends events.EventTarget {
} }
get _commentListNode() { get _commentListNode() {
return this._hostNode.querySelector("ul"); return this._hostNode.querySelector('ul');
} }
_installCommentNode(comment) { _installCommentNode(comment) {
const commentListItemNode = document.createElement("li"); const commentListItemNode = document.createElement('li');
const commentControl = new CommentControl( const commentControl = new CommentControl(
commentListItemNode, commentListItemNode, comment, false);
comment, events.proxyEvent(commentControl, this, 'submit');
false events.proxyEvent(commentControl, this, 'score');
); events.proxyEvent(commentControl, this, 'delete');
events.proxyEvent(commentControl, this, "submit");
events.proxyEvent(commentControl, this, "score");
events.proxyEvent(commentControl, this, "delete");
this._commentIdToNode[comment.id] = commentListItemNode; this._commentIdToNode[comment.id] = commentListItemNode;
this._commentListNode.appendChild(commentListItemNode); this._commentListNode.appendChild(commentListItemNode);
} }
@ -57,6 +54,6 @@ class CommentListControl extends events.EventTarget {
_evtRemove(e) { _evtRemove(e) {
this._uninstallCommentNode(e.detail.comment); this._uninstallCommentNode(e.detail.comment);
} }
} };
module.exports = CommentListControl; module.exports = CommentListControl;

View file

@ -1,28 +1,26 @@
"use strict"; 'use strict';
const ICON_CLASS_OPENED = "fa-chevron-down"; const ICON_CLASS_OPENED = 'fa-chevron-down';
const ICON_CLASS_CLOSED = "fa-chevron-up"; const ICON_CLASS_CLOSED = 'fa-chevron-up';
const views = require("../util/views.js"); const views = require('../util/views.js');
const template = views.getTemplate("expander"); const template = views.getTemplate('expander');
class ExpanderControl { class ExpanderControl {
constructor(name, title, nodes) { constructor(name, title, nodes) {
this._name = name; this._name = name;
nodes = Array.from(nodes).filter((n) => n); nodes = Array.from(nodes).filter(n => n);
if (!nodes.length) { if (!nodes.length) {
return; return;
} }
const expanderNode = template({ title: title }); const expanderNode = template({title: title});
const toggleLinkNode = expanderNode.querySelector("a"); const toggleLinkNode = expanderNode.querySelector('a');
const toggleIconNode = expanderNode.querySelector("i"); const toggleIconNode = expanderNode.querySelector('i');
const expanderContentNode = expanderNode.querySelector("div"); const expanderContentNode = expanderNode.querySelector('div');
toggleLinkNode.addEventListener("click", (e) => toggleLinkNode.addEventListener('click', e => this._evtToggleClick(e));
this._evtToggleClick(e)
);
nodes[0].parentNode.insertBefore(expanderNode, nodes[0]); nodes[0].parentNode.insertBefore(expanderNode, nodes[0]);
@ -34,29 +32,28 @@ class ExpanderControl {
this._toggleIconNode = toggleIconNode; this._toggleIconNode = toggleIconNode;
expanderNode.classList.toggle( expanderNode.classList.toggle(
"collapsed", 'collapsed',
this._allStates[this._name] === undefined this._allStates[this._name] === undefined ?
? false false :
: !this._allStates[this._name] !this._allStates[this._name]);
);
this._syncIcon(); this._syncIcon();
} }
// eslint-disable-next-line accessor-pairs
set title(newTitle) { set title(newTitle) {
if (this._expanderNode) { if (this._expanderNode) {
this._expanderNode.querySelector("header span").textContent = this._expanderNode
newTitle; .querySelector('header span')
.textContent = newTitle;
} }
} }
get _isOpened() { get _isOpened() {
return !this._expanderNode.classList.contains("collapsed"); return !this._expanderNode.classList.contains('collapsed');
} }
get _allStates() { get _allStates() {
try { try {
return JSON.parse(localStorage.getItem("expander")) || {}; return JSON.parse(localStorage.getItem('expander')) || {};
} catch (e) { } catch (e) {
return {}; return {};
} }
@ -65,12 +62,12 @@ class ExpanderControl {
_save() { _save() {
const newStates = Object.assign({}, this._allStates); const newStates = Object.assign({}, this._allStates);
newStates[this._name] = this._isOpened; newStates[this._name] = this._isOpened;
localStorage.setItem("expander", JSON.stringify(newStates)); localStorage.setItem('expander', JSON.stringify(newStates));
} }
_evtToggleClick(e) { _evtToggleClick(e) {
e.preventDefault(); e.preventDefault();
this._expanderNode.classList.toggle("collapsed"); this._expanderNode.classList.toggle('collapsed');
this._save(); this._save();
this._syncIcon(); this._syncIcon();
} }

View file

@ -1,9 +1,9 @@
"use strict"; 'use strict';
const events = require("../events.js"); const events = require('../events.js');
const views = require("../util/views.js"); const views = require('../util/views.js');
const template = views.getTemplate("file-dropper"); const template = views.getTemplate('file-dropper');
const KEY_RETURN = 13; const KEY_RETURN = 13;
@ -17,53 +17,37 @@ class FileDropperControl extends events.EventTarget {
allowMultiple: options.allowMultiple, allowMultiple: options.allowMultiple,
allowUrls: options.allowUrls, allowUrls: options.allowUrls,
lock: options.lock, lock: options.lock,
id: "file-" + Math.random().toString(36).substring(7), id: 'file-' + Math.random().toString(36).substring(7),
urlPlaceholder: urlPlaceholder:
options.urlPlaceholder || "Alternatively, paste an URL here.", options.urlPlaceholder || 'Alternatively, paste an URL here.',
}); });
this._dropperNode = source.querySelector(".file-dropper"); this._dropperNode = source.querySelector('.file-dropper');
this._urlInputNode = source.querySelector("input[type=text]"); this._urlInputNode = source.querySelector('input[type=text]');
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 = options.allowMultiple || false; this._fileInputNode.multiple = options.allowMultiple || false;
this._counter = 0; this._counter = 0;
this._dropperNode.addEventListener("dragenter", (e) => this._dropperNode.addEventListener(
this._evtDragEnter(e) 'dragenter', e => this._evtDragEnter(e));
); this._dropperNode.addEventListener(
this._dropperNode.addEventListener("dragleave", (e) => 'dragleave', e => this._evtDragLeave(e));
this._evtDragLeave(e) this._dropperNode.addEventListener(
); 'dragover', e => this._evtDragOver(e));
this._dropperNode.addEventListener("dragover", (e) => this._dropperNode.addEventListener(
this._evtDragOver(e) 'drop', e => this._evtDrop(e));
); this._fileInputNode.addEventListener(
this._dropperNode.addEventListener("drop", (e) => this._evtDrop(e)); 'change', e => this._evtFileChange(e));
this._fileInputNode.addEventListener("change", (e) =>
this._evtFileChange(e)
);
if (this._urlInputNode) { if (this._urlInputNode) {
this._urlInputNode.addEventListener("keydown", (e) => this._urlInputNode.addEventListener(
this._evtUrlInputKeyDown(e) 'keydown', e => this._evtUrlInputKeyDown(e));
);
this._urlInputNode.addEventListener("paste", (e) => {
// document.onpaste is used on the post-upload page.
// And this event is used on the post edit page.
if (document.getElementById("post-upload")) return;
this._evtPaste(e)
});
} }
if (this._urlConfirmButtonNode) { if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener("click", (e) => this._urlConfirmButtonNode.addEventListener(
this._evtUrlConfirmButtonClick(e) 'click', e => this._evtUrlConfirmButtonClick(e));
);
}
document.onpaste = (e) => {
if (!document.getElementById("post-upload")) return;
this._evtPaste(e)
} }
this._originalHtml = this._dropperNode.innerHTML; this._originalHtml = this._dropperNode.innerHTML;
@ -72,27 +56,24 @@ class FileDropperControl extends events.EventTarget {
reset() { reset() {
this._dropperNode.innerHTML = this._originalHtml; this._dropperNode.innerHTML = this._originalHtml;
this.dispatchEvent(new CustomEvent("reset")); this.dispatchEvent(new CustomEvent('reset'));
} }
_emitFiles(files) { _emitFiles(files) {
files = Array.from(files); files = Array.from(files);
if (this._options.lock) { if (this._options.lock) {
this._dropperNode.innerText = files this._dropperNode.innerText =
.map((file) => file.name) files.map(file => file.name).join(', ');
.join(", ");
} }
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("fileadd", { detail: { files: files } }) new CustomEvent('fileadd', {detail: {files: files}}));
);
} }
_emitUrls(urls) { _emitUrls(urls) {
urls = Array.from(urls).map((url) => url.trim()); urls = Array.from(urls).map(url => url.trim());
if (this._options.lock) { if (this._options.lock) {
this._dropperNode.innerText = urls this._dropperNode.innerText =
.map((url) => url.split(/\//).reverse()[0]) urls.map(url => url.split(/\//).reverse()[0]).join(', ');
.join(", ");
} }
for (let url of urls) { for (let url of urls) {
if (!url) { if (!url) {
@ -103,20 +84,18 @@ class FileDropperControl extends events.EventTarget {
return; return;
} }
} }
this.dispatchEvent( this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}}));
new CustomEvent("urladd", { detail: { urls: urls } })
);
} }
_evtDragEnter(e) { _evtDragEnter(e) {
this._dropperNode.classList.add("active"); this._dropperNode.classList.add('active');
this._counter++; this._counter++;
} }
_evtDragLeave(e) { _evtDragLeave(e) {
this._counter--; this._counter--;
if (this._counter === 0) { if (this._counter === 0) {
this._dropperNode.classList.remove("active"); this._dropperNode.classList.remove('active');
} }
} }
@ -130,42 +109,31 @@ class FileDropperControl extends events.EventTarget {
_evtDrop(e) { _evtDrop(e) {
e.preventDefault(); e.preventDefault();
this._dropperNode.classList.remove("active"); this._dropperNode.classList.remove('active');
if (!e.dataTransfer.files.length) { if (!e.dataTransfer.files.length) {
window.alert("Only files are supported."); window.alert('Only files are supported.');
} }
if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) { if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) {
window.alert("Cannot select multiple files."); window.alert('Cannot select multiple files.');
} }
this._emitFiles(e.dataTransfer.files); this._emitFiles(e.dataTransfer.files);
} }
_evtPaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
if (!this._options.allowMultiple && fileList.length > 1) {
window.alert("Cannot select multiple files.");
} else if (fileList.length > 0) {
this._emitFiles(fileList);
}
}
_evtUrlInputKeyDown(e) { _evtUrlInputKeyDown(e) {
if (e.which !== KEY_RETURN) { if (e.which !== KEY_RETURN) {
return; return;
} }
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]/));
this._urlInputNode.value = ""; this._urlInputNode.value = '';
} }
_evtUrlConfirmButtonClick(e) { _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]/));
this._urlInputNode.value = ""; this._urlInputNode.value = '';
} }
} }

View file

@ -1,59 +0,0 @@
"use strict";
const misc = require("../util/misc.js");
const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) {
return [...pools]
.sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount;
})
.map((pool) => {
let cssName = misc.makeCssName(pool.category, "pool");
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") +
"</span>";
return {
caption: caption,
value: pool,
};
});
}
class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) {
const minLengthForPartialSearch = 3;
options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text);
const query =
(text.length < minLengthForPartialSearch
? term + "*"
: "*" + term + "*") + " sort:post-count";
return new Promise((resolve, reject) => {
PoolList.search(query, 0, this._options.maxResults, [
"id",
"names",
"category",
"postCount",
"version",
]).then(
(response) =>
resolve(
_poolListToMatches(response.results, this._options)
),
reject
);
});
};
super(input, options);
}
}
module.exports = PoolAutoCompleteControl;

View file

@ -1,195 +0,0 @@
"use strict";
const api = require("../api.js");
const pools = require("../pools.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const settings = require("../models/settings.js");
const events = require("../events.js");
const views = require("../util/views.js");
const PoolAutoCompleteControl = require("./pool_auto_complete_control.js");
const KEY_SPACE = 32;
const KEY_RETURN = 13;
const SOURCE_INIT = "init";
const SOURCE_IMPLICATION = "implication";
const SOURCE_USER_INPUT = "user-input";
const SOURCE_CLIPBOARD = "clipboard";
const template = views.getTemplate("pool-input");
function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) {
if (listItemNode.fadeTimeout) {
window.clearTimeout(listItemNode.fadeTimeout);
}
listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) {
listItemNode.classList.remove(listItemNode.classList.item(0));
}
listItemNode.fadeTimeout = null;
}, 2500);
}
}
class PoolInputControl extends events.EventTarget {
constructor(hostNode, poolList) {
super();
this.pools = poolList;
this._hostNode = hostNode;
this._poolToListItemNode = new Map();
// dom
const editAreaNode = template();
this._editAreaNode = editAreaNode;
this._poolInputNode = editAreaNode.querySelector("input");
this._poolListNode = editAreaNode.querySelector("ul.compact-pools");
this._autoCompleteControl = new PoolAutoCompleteControl(
this._poolInputNode,
{
getTextToFind: () => {
return this._poolInputNode.value;
},
confirm: (pool) => {
this._poolInputNode.value = "";
this.addPool(pool, SOURCE_USER_INPUT);
},
delete: (pool) => {
this._poolInputNode.value = "";
this.deletePool(pool);
},
verticalShift: -2,
}
);
// show
this._hostNode.style.display = "none";
this._hostNode.parentNode.insertBefore(
this._editAreaNode,
hostNode.nextSibling
);
// add existing pools
for (let pool of [...this.pools]) {
const listItemNode = this._createListItemNode(pool);
this._poolListNode.appendChild(listItemNode);
}
}
addPool(pool, source) {
if (source !== SOURCE_INIT && this.pools.hasPoolId(pool.id)) {
return Promise.resolve();
}
this.pools.add(pool, false);
const listItemNode = this._createListItemNode(pool);
if (!pool.category) {
listItemNode.classList.add("new");
}
this._poolListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
this.dispatchEvent(
new CustomEvent("add", {
detail: { pool: pool, source: source },
})
);
this.dispatchEvent(new CustomEvent("change"));
return Promise.resolve();
}
deletePool(pool) {
if (!this.pools.hasPoolId(pool.id)) {
return;
}
this.pools.removeById(pool.id);
this._hideAutoComplete();
this._deleteListItemNode(pool);
this.dispatchEvent(
new CustomEvent("remove", {
detail: { pool: pool },
})
);
this.dispatchEvent(new CustomEvent("change"));
}
_createListItemNode(pool) {
const className = pool.category
? misc.makeCssName(pool.category, "pool")
: null;
const poolLinkNode = document.createElement("a");
if (className) {
poolLinkNode.classList.add(className);
}
poolLinkNode.setAttribute(
"href",
uri.formatClientLink("pool", pool.names[0])
);
const poolIconNode = document.createElement("i");
poolIconNode.classList.add("fa");
poolIconNode.classList.add("fa-pool");
poolLinkNode.appendChild(poolIconNode);
const searchLinkNode = document.createElement("a");
if (className) {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
"href",
uri.formatClientLink("posts", { query: "pool:" + pool.id })
);
searchLinkNode.textContent = pool.names[0] + " ";
const usagesNode = document.createElement("span");
usagesNode.classList.add("pool-usages");
usagesNode.setAttribute("data-pseudo-content", pool.postCount);
const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add("remove-pool");
removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener("click", (e) => {
e.preventDefault();
this.deletePool(pool);
});
const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(poolLinkNode);
listItemNode.appendChild(searchLinkNode);
listItemNode.appendChild(usagesNode);
for (let name of pool.names) {
this._poolToListItemNode.set(name, listItemNode);
}
return listItemNode;
}
_deleteListItemNode(pool) {
const listItemNode = this._getListItemNode(pool);
if (listItemNode) {
listItemNode.parentNode.removeChild(listItemNode);
}
for (let name of pool.names) {
this._poolToListItemNode.delete(name);
}
}
_getListItemNode(pool) {
return this._poolToListItemNode.get(pool.names[0]);
}
_hideAutoComplete() {
this._autoCompleteControl.hide();
}
}
module.exports = PoolInputControl;

View file

@ -1,38 +1,36 @@
"use strict"; 'use strict';
const settings = require("../models/settings.js"); const settings = require('../models/settings.js');
const views = require("../util/views.js"); 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, fitFunctionOverride) { 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; let fitMode = settings.get().fitMode;
if (typeof fitFunctionOverride !== "undefined") { if (typeof fitFunctionOverride !== 'undefined') {
fitMode = fitFunctionOverride; 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, }[fitMode] || this.fitBoth;
}[fitMode] || this.fitBoth;
this._install(); this._install();
this._post.addEventListener("changeContent", (e) => this._post.addEventListener(
this._evtPostContentChange(e) 'changeContent', e => this._evtPostContentChange(e));
);
} }
disableOverlay() { disableOverlay() {
this._hostNode.querySelector(".post-overlay").style.display = "none"; this._hostNode.querySelector('.post-overlay').style.display = 'none';
} }
fitWidth() { fitWidth() {
@ -94,48 +92,22 @@ class PostContentControl {
_resize(width, height) { _resize(width, height) {
const resizeListenerNodes = [this._postContentNode].concat( const resizeListenerNodes = [this._postContentNode].concat(
...this._postContentNode.querySelectorAll(".resize-listener") ...this._postContentNode.querySelectorAll('.resize-listener'));
);
for (let node of resizeListenerNodes) { for (let node of resizeListenerNodes) {
node.style.width = width + "px"; node.style.width = width + 'px';
node.style.height = height + "px"; node.style.height = height + 'px';
} }
} }
_refreshSize() { _refreshSize() {
if (window.innerWidth <= 800) {
const buttons = document.querySelector(".sidebar > .buttons");
if (buttons) {
const content = document.querySelector(".content");
content.insertBefore(buttons, content.querySelector(".post-container + *"));
const afterControls = document.querySelector(".content > .after-mobile-controls");
if (afterControls) {
afterControls.parentElement.parentElement.appendChild(afterControls);
}
}
} else {
const buttons = document.querySelector(".content > .buttons");
if (buttons) {
const sidebar = document.querySelector(".sidebar");
sidebar.insertBefore(buttons, sidebar.firstElementChild);
}
const afterControls = document.querySelector(".content + .after-mobile-controls");
if (afterControls) {
document.querySelector(".content").appendChild(afterControls);
}
}
this._currentFitFunction(); this._currentFitFunction();
} }
_install() { _install() {
this._reinstall(); this._reinstall();
optimizedResize.add(() => this._refreshSize()); optimizedResize.add(() => this._refreshSize());
views.monitorNodeRemoval(this._hostNode, () => { views.monitorNodeRemoval(
this._uninstall(); this._hostNode, () => { this._uninstall(); });
});
} }
_reinstall() { _reinstall() {
@ -144,7 +116,7 @@ class PostContentControl {
autoplay: settings.get().autoplayVideos, autoplay: settings.get().autoplayVideos,
}); });
if (settings.get().transparencyGrid) { if (settings.get().transparencyGrid) {
newNode.classList.add("transparency-grid"); newNode.classList.add('transparency-grid');
} }
if (this._postContentNode) { if (this._postContentNode) {
this._hostNode.replaceChild(newNode, this._postContentNode); this._hostNode.replaceChild(newNode, this._postContentNode);

Some files were not shown because too many files have changed in this diff Show more