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/**/pyvenv.cfg
__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,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
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
- 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 notes / annotations, including arbitrary polygons
- 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 implications (adding a tag automatically adds another)
- Tag aliases
- Pools and pool categories
- Duplicate detection
- Post rating and favoriting; comment rating
- 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.
[See installation instructions.](doc/INSTALL.md)
More installation resources, as well as related projects can be found on the
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
Users who wish to avoid using Docker may find the [old installation instructions](doc/LEGACY_INSTALL.md) helpful.
## 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
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}
FROM --platform=$BUILDPLATFORM scratch as approot
FROM scratch as approot
COPY docker-start.sh /
@ -22,7 +22,7 @@ WORKDIR /var/www
COPY --from=builder /opt/app/public/ .
FROM nginx:alpine as release
FROM nginx:alpine
RUN apk --no-cache add dumb-init
COPY --from=approot / /

View file

@ -21,13 +21,11 @@ const webapp_splash_screens = [
];
const external_js = [
'dompurify',
'js-cookie',
'marked',
'mousetrap',
'nprogress',
'superagent',
'underscore',
'superagent',
'mousetrap',
'js-cookie',
'nprogress',
];
const app_manifest = {
@ -57,11 +55,6 @@ const glob = require('glob');
const path = require('path');
const util = require('util');
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) {
return fs.readFileSync(path, 'utf-8');
@ -153,6 +146,9 @@ function bundleCss() {
console.info('Bundled CSS');
}
function bundleJs() {
const browserify = require('browserify');
function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
@ -160,7 +156,7 @@ function minifyJs(path) {
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
b.bundle().pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
@ -169,7 +165,7 @@ function writeJsBundle(b, path, compress, callback) {
});
}
function bundleVendorJs(compress) {
if (!process.argv.includes('--no-vendor-js')) {
let b = browserify();
for (let lib of external_js) {
b.require(lib);
@ -178,7 +174,7 @@ function bundleVendorJs(compress) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
writeJsBundle(b, file, compress, () => {
writeJsBundle(b, file, true, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
@ -186,36 +182,23 @@ function bundleVendorJs(compress) {
});
}
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() {
if (!process.argv.includes('--no-vendor-js')) {
bundleVendorJs(true);
}
if (!process.argv.includes('--no-app-js')) {
let watchify = require('watchify');
let b = browserify({debug: process.argv.includes('--debug')});
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
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 getVersion() {
let build_info = process.env.BUILD_INFO;
@ -233,8 +216,7 @@ function bundleConfig() {
meta: {
version: getVersion(),
buildDate: new Date().toUTCString()
},
environment: environment
}
};
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
@ -243,6 +225,7 @@ function bundleConfig() {
function bundleBinaryAssets() {
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
fs.copyFileSync('./img/transparency_grid.png', './public/img/transparency_grid.png');
console.info('Copied images');
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
@ -314,104 +297,12 @@ 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();
bundleConfig();
if (process.argv.includes('--watch')) {
watch();
} else {
if (!process.argv.includes('--no-binary-assets')) {
bundleBinaryAssets();
}
if (!process.argv.includes('--no-web-app-files')) {
bundleWebAppFiles();
}
if (!process.argv.includes('--no-html')) {
bundleHtml();
}
@ -421,4 +312,3 @@ if (process.argv.includes('--watch')) {
if (!process.argv.includes('--no-js')) {
bundleJs();
}
}

View file

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

View file

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

View file

@ -1,6 +1,5 @@
@import colors
$comment-border-color = $top-navigation-color
$comment-border-color-darktheme = $top-navigation-color-darktheme
.global-comment-list
text-align: left
@ -47,8 +46,3 @@ $comment-border-color-darktheme = $top-navigation-color-darktheme
.comments-container
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%
line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal
display: inline-block
margin-bottom: 1em
@ -171,16 +167,6 @@ input[type=time]
background: $input-disabled-background-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
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]+.radio,
input[readonly]+.checkbox,
@ -271,9 +242,8 @@ form.show-validation .input
outline: 0
border: 2px solid $input-good-border-color
background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/*
* Buttons
@ -340,10 +310,6 @@ input::-moz-focus-inner
button
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper
cursor: default
opacity: .5
@ -353,6 +319,8 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active
border-color: $main-color
.autocomplete
position: absolute
z-index: 10
@ -377,10 +345,6 @@ input[type=file]:focus+.file-dropper,
.disabled
color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete
display: none

View file

@ -1,11 +1,6 @@
@import colors
@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 */
@font-face
font-family: 'Open Sans';
@ -33,10 +28,6 @@ body
@media (max-width: 1200px)
font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3
font-weight: normal
margin-bottom: 1em
@ -71,11 +62,6 @@ a
.vim-nav-hint
position: absolute
visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append
margin-left: 1em
@ -116,19 +102,12 @@ form .fa-question-circle-o
>*:last-child
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
hr
border: 0
border-top: 1px solid $line-color
margin: 1em 0
padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav
ul
list-style-type: none
@ -226,24 +205,6 @@ nav
@media (max-width: 1000px)
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
text-decoration: underline
@ -268,18 +229,6 @@ a .access-key
border: 1px solid $message-success-border-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
/*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle
@ -290,14 +239,9 @@ a .access-key
width: 20px
height: 20px
&.empty
background-image:
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-image: url('../img/transparency_grid.png')
background-repeat: repeat
background-size: 20px 20px
background-size: initial
img
opacity: 0
width: 100%

View file

@ -22,11 +22,3 @@
line-height: 2em
.expander-content
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
span
position: relative
background: $window-color
background: white
padding: 0 1em
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-content.transparency-grid img
background-image:
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
background: url('../img/transparency_grid.png')
text-align: center
.post-content

View file

@ -70,7 +70,7 @@
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
font-size: 1.6em
&.tagged
background: rgba(0, 230, 0, 0.7)
&:after
@ -114,36 +114,12 @@
&[data-disabled]
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
background-position: 50% 30%
width: 100%
height: 100%
outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags
.thumbnail
@ -158,22 +134,6 @@
.thumbnail
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
white-space: nowrap
text-align: left
@ -187,9 +147,6 @@
vertical-align: top
@media (max-width: 1000px)
display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input
margin-bottom: 0.25em
margin-right: 0.25em
@ -225,7 +182,7 @@
.hint
display: none
input[name=tag]
width: 24em
width: 12em
@media (max-width: 1000px)
display: block
width: 100%
@ -241,19 +198,7 @@
.append
@media (max-width: 1000px)
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
margin-right: 0.25em
&.safety-safe

View file

@ -7,15 +7,15 @@
>.sidebar
margin-right: 1em
min-width: 21em
max-width: 21em
min-width: 20em
max-width: 20em
line-height: 160%
a:active
border: 0
outline: 0
>.sidebar>nav.buttons, >.content nav.buttons
nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
@ -27,41 +27,27 @@
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
transition: background 0.2s linear
&: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
margin-top: 2em
>.content
width: 100%
.post-container
margin-bottom: 0.6em
margin-bottom: 2em
.post-content
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)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
@ -119,6 +105,7 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
li

View file

@ -1,6 +1,5 @@
@import colors
$upload-header-background-color = $top-navigation-color
$upload-header-background-color-darktheme = $top-navigation-color-darktheme
$upload-border-color = #DDD
$cancel-button-color = tomato
@ -13,12 +12,8 @@ $cancel-button-color = tomato
&.inactive input[type=submit],
&.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit],
&.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel
display: none
@ -43,12 +38,6 @@ $cancel-button-color = tomato
.skip-duplicates
margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
margin-top: 1em
@ -62,14 +51,6 @@ $cancel-button-color = tomato
margin: 0 0 1.2em 0
padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper
float: left
width: 12em
@ -168,15 +149,3 @@ $cancel-button-color = tomato
color: $inactive-link-color
&:last-child .move-down
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
background: $snapshot-created-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-created-background-color, 50%)
div.operation-modified
background: $snapshot-modified-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-modified-background-color, 50%)
div.operation-deleted
background: $snapshot-deleted-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-deleted-background-color, 50%)
div.operation-merged
background: $snapshot-merged-background-color
&+.details
background: alpha(@background, 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%)
background: lighten($snapshot-merged-background-color, 50%)

View file

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

View file

@ -46,7 +46,7 @@ div.tag-input
.wrapper
margin-left: 0.5em
background: $window-color
background: $tag-suggestions-background-color
border: 1px solid $tag-suggestions-border-color
width: 15em
word-break: break-all
@ -62,7 +62,7 @@ div.tag-input
max-height: 20em
padding: 0.5em 1em 0 1em
li:last-child
border-bottom: 0.5em solid alpha($window-color, 0)
border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0)
li
margin: 0
font-size: 90%
@ -86,12 +86,6 @@ div.tag-input
font-size: 90%
unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags
width: 100%
margin: 0.5em 0 0 0
@ -109,30 +103,18 @@ ul.compact-tags
a:focus
outline: 0
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
background: $implied-tag-background-color
color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new
background: $new-tag-background-color
color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i
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
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color
@ -143,19 +125,3 @@ div.tag-input, ul.compact-tags
margin-left: 0.7em
.remove-tag
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
display: none
.darktheme .tag-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.tag-list-header
label
display: none !important
@ -61,7 +54,3 @@
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

View file

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

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='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='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</li><!--
--></ul><!--
--></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>
<td><code>uploader</code></td>
<td>uploaded by given user (accepts wildcards)</td>
<td>uploaded by given use (accepts wildcards)r</td>
</tr>
<tr>
<td><code>upload</code></td>
<td>alias of <code>uploader</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr>
<td><code>submit</code></td>
<td>alias of <code>uploader</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr>
<td><code>comment</code></td>
@ -42,10 +42,6 @@
<td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td>
</tr>
<tr>
<td><code>pool</code></td>
<td>belonging to the pool with the given ID</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>having given number of tags</td>
@ -82,17 +78,9 @@
<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>
</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>
<td><code>content-checksum</code></td>
<td>alias of <code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>file-size</code></td>

View file

@ -1,7 +1,7 @@
<ul>
<li><%- ctx.postCount %> posts</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><% } %>
</ul>

View file

@ -2,7 +2,7 @@
<html>
<head>
<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='apple-mobile-web-app-capable' content='yes'/>
<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>
<% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPostNotes) { %>
<section class='notes'>
<a href class='add'>Add a note</a>

View file

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

View file

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

View file

@ -9,16 +9,11 @@
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %><!--
--></a>
}[ctx.post.mimeType] %>
</a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
@ -57,8 +52,7 @@
<section class='search'>
Search on
<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://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -97,12 +91,12 @@
--></a><!--
--><% } %><!--
--><% 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) { %><!--
--></a><!--
--><% } %>&#32;<!--
--><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View file

@ -7,28 +7,12 @@
<span class='skip-duplicates'>
<%= ctx.makeCheckbox({
text: 'Skip duplicate',
text: 'Skip duplicates',
name: 'skip-duplicates',
checked: false,
}) %>
</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'/>
</div>

View file

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

View file

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

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) { %>
<ul>
<% for (let post of ctx.response.results) { %>
@ -50,10 +50,6 @@
<% } %>
</span>
<% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span>
</li>
<% } %>

View file

@ -22,15 +22,6 @@
}) %>
</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>
<%= ctx.makeCheckbox({
text: 'Upscale small posts',
@ -47,15 +38,6 @@
<p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p>
</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>
<%= ctx.makeCheckbox({
text: 'Enable transparency grid',
@ -84,8 +66,8 @@
<li>
<%= ctx.makeCheckbox({
text: 'Display underscores as spaces',
name: 'underscores-as-spaces',
text: 'Display underscores as spaces in tags',
name: 'tag-underscores-as-spaces',
checked: ctx.browsingSettings.tagUnderscoresAsSpaces,
}) %>
<p class='hint'>Display all underscores as if they were spaces. This is only a visual change, which means that you'll still have to use underscores when searching or editing tags.</p>

View file

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

View file

@ -7,7 +7,6 @@
<tr>
<th class='name'>Category name</th>
<th class='color'>CSS color</th>
<th class='order'>Order</th>
<th class='usages'>Usages</th>
</tr>
</thead>
@ -22,7 +21,7 @@
<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'>
<input type='submit' class='save' value='Save changes'>
</div>

View file

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

View file

@ -1,6 +1,6 @@
<div class='tag-delete'>
<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'>
<li>

View file

@ -36,6 +36,6 @@
<section class='description'>
<hr/>
<%= 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>
</div>

View file

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

View file

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

View file

@ -1,19 +1,19 @@
"use strict";
'use strict';
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const EmptyView = require("../views/empty_view.js");
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const EmptyView = require('../views/empty_view.js');
class BasePostController {
constructor(ctx) {
if (!api.hasPrivilege("posts:view")) {
if (!api.hasPrivilege('posts:view')) {
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;
}
topNavigation.activate("posts");
topNavigation.setTitle("Post #" + ctx.parameters.id.toString());
topNavigation.activate('posts');
topNavigation.setTitle('Post #' + ctx.parameters.id.toString());
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,20 +1,19 @@
"use strict";
'use strict';
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const topNavigation = require("../models/top_navigation.js");
const PasswordResetView = require("../views/password_reset_view.js");
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const PasswordResetView = require('../views/password_reset_view.js');
class PasswordResetController {
constructor() {
topNavigation.activate("login");
topNavigation.setTitle("Password reminder");
topNavigation.activate('login');
topNavigation.setTitle('Password reminder');
this._passwordResetView = new PasswordResetView();
this._passwordResetView.addEventListener("submit", (e) =>
this._evtReset(e)
);
this._passwordResetView.addEventListener(
'submit', e => this._evtReset(e));
}
_evtReset(e) {
@ -22,20 +21,15 @@ class PasswordResetController {
this._passwordResetView.disableForm();
api.forget();
api.logout();
api.get(
uri.formatApiLink("password-reset", e.detail.userNameOrEmail)
).then(
() => {
api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
.then(() => {
this._passwordResetView.showSuccess(
"E-mail has been sent. To finish the procedure, " +
"please click the link it contains."
);
},
(error) => {
'E-mail has been sent. To finish the procedure, ' +
'please click the link it contains.');
}, error => {
this._passwordResetView.showError(error.message);
this._passwordResetView.enableForm();
}
);
});
}
}
@ -44,30 +38,26 @@ class PasswordResetFinishController {
api.forget();
api.logout();
let password = null;
api.post(uri.formatApiLink("password-reset", name), { token: token })
.then((response) => {
api.post(uri.formatApiLink('password-reset', name), {token: token})
.then(response => {
password = response.password;
return api.login(name, password, false);
})
.then(
() => {
}).then(() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("New password: " + password);
},
(error) => {
ctx.controller.showSuccess('New password: ' + password);
}, error => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
}
);
});
}
}
module.exports = (router) => {
router.enter(["password-reset"], (ctx, next) => {
module.exports = router => {
router.enter(['password-reset'], (ctx, next) => {
ctx.controller = new PasswordResetController();
});
router.enter(["password-reset", ":descriptor"], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(":", 2);
router.enter(['password-reset', ':descriptor'], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(':', 2);
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 api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const settings = require("../models/settings.js");
const Post = require("../models/post.js");
const PostList = require("../models/post_list.js");
const PostDetailView = require("../views/post_detail_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const settings = require('../models/settings.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const PostDetailView = require('../views/post_detail_view.js');
const BasePostController = require('./base_post_controller.js');
const EmptyView = require('../views/empty_view.js');
class PostDetailController extends BasePostController {
constructor(ctx, section) {
super(ctx);
Post.get(ctx.parameters.id).then(
(post) => {
Post.get(ctx.parameters.id).then(post => {
this._id = ctx.parameters.id;
post.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
post.addEventListener('change', e => this._evtSaved(e, section));
this._installView(post, section);
},
(error) => {
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
});
}
showSuccess(message) {
@ -38,68 +33,56 @@ class PostDetailController extends BasePostController {
this._view = new PostDetailView({
post: post,
section: section,
canMerge: api.hasPrivilege("posts:merge"),
canMerge: api.hasPrivilege('posts:merge'),
});
this._view.addEventListener("select", (e) => this._evtSelect(e));
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener('select', e => this._evtSelect(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
}
_evtSelect(e) {
this._view.clearMessages();
this._view.disableForm();
Post.get(e.detail.postId).then(
(post) => {
Post.get(e.detail.postId).then(post => {
this._view.selectPost(post);
this._view.enableForm();
},
(error) => {
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) {
router.replace(
uri.formatClientLink("post", e.detail.post.id, section),
null,
false
);
uri.formatClientLink('post', e.detail.post.id, section),
null, false);
}
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.post
.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(
() => {
this._installView(e.detail.post, "merge");
this._view.showSuccess("Post merged.");
e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(() => {
this._installView(e.detail.post, 'merge');
this._view.showSuccess('Post merged.');
router.replace(
uri.formatClientLink(
"post",
e.detail.targetPost.id,
"merge"
),
null,
false
);
},
(error) => {
'post', e.detail.targetPost.id, 'merge'),
null, false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
}
module.exports = (router) => {
router.enter(["post", ":id", "merge"], (ctx, next) => {
ctx.controller = new PostDetailController(ctx, "merge");
module.exports = router => {
router.enter(
['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 api = require("../api.js");
const settings = require("../models/settings.js");
const uri = require("../util/uri.js");
const PostList = require("../models/post_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PostsHeaderView = require("../views/posts_header_view.js");
const PostsPageView = require("../views/posts_page_view.js");
const EmptyView = require("../views/empty_view.js");
const router = require('../router.js');
const api = require('../api.js');
const settings = require('../models/settings.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require('../views/posts_page_view.js');
const EmptyView = require('../views/empty_view.js');
const fields = [
"id",
"thumbnailUrl",
"type",
"safety",
"score",
"favoriteCount",
"commentCount",
"tags",
"version",
];
'id', 'thumbnailUrl', 'type', 'safety',
'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
class PostListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege("posts:list")) {
if (!api.hasPrivilege('posts:list')) {
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;
}
this._ctx = ctx;
topNavigation.activate('posts');
topNavigation.setTitle('Listing posts');
topNavigation.activate("posts");
topNavigation.setTitle("Listing posts");
this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
bulkEdit: {
tags: this._bulkEditTags,
tags: this._bulkEditTags
},
});
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
if (this._headerView._bulkDeleteEditor) {
this._headerView._bulkDeleteEditor.addEventListener(
"deleteSelectedPosts",
(e) => {
this._evtDeleteSelectedPosts(e);
}
);
}
this._postsMarkedForDeletion = [];
this._syncPageController();
}
@ -71,67 +50,34 @@ class PostListController {
}
get _bulkEditTags() {
return (this._ctx.parameters.tag || "").split(/\s+/).filter((s) => s);
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink("posts", e.detail.parameters)
);
uri.formatClientLink('posts', e.detail.parameters));
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
_evtTag(e) {
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())
.catch((error) => window.alert(error.message));
.catch(error => window.alert(error.message));
}
_evtUntag(e) {
for (let tag of this._bulkEditTags) {
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) {
e.detail.post.safety = e.detail.safety;
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();
});
}
e.detail.post.save().catch(error => window.alert(error.message));
}
_syncPageController() {
@ -139,51 +85,37 @@ class PostListController {
parameters: this._ctx.parameters,
defaultLimit: parseInt(settings.get().postsPerPage),
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("posts", parameters);
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('posts', parameters);
},
requestPage: (offset, limit) => {
return PostList.search(
this._ctx.parameters.query,
offset,
limit,
fields
);
this._ctx.parameters.query, offset, limit, fields);
},
pageRenderer: (pageCtx) => {
pageRenderer: pageCtx => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege("posts:view"),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege(
"posts:bulk-edit:safety"
),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety:
api.hasPrivilege('posts:bulk-edit:safety'),
bulkEdit: {
tags: this._bulkEditTags,
markedForDeletion: this._postsMarkedForDeletion,
},
postFlow: settings.get().postFlow,
});
const view = new PostsPageView(pageCtx);
view.addEventListener("tag", (e) => this._evtTag(e));
view.addEventListener("untag", (e) => this._evtUntag(e));
view.addEventListener("changeSafety", (e) =>
this._evtChangeSafety(e)
);
view.addEventListener("markForDeletion", (e) =>
this._evtMarkForDeletion(e)
);
view.addEventListener('tag', e => this._evtTag(e));
view.addEventListener('untag', e => this._evtUntag(e));
view.addEventListener(
'changeSafety', e => this._evtChangeSafety(e));
return view;
},
});
}
}
module.exports = (router) => {
router.enter(["posts"], (ctx, next) => {
ctx.controller = new PostListController(ctx);
});
module.exports = router => {
router.enter(
['posts'],
(ctx, next) => { ctx.controller = new PostListController(ctx); });
};

View file

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

View file

@ -1,40 +1,40 @@
"use strict";
'use strict';
const api = require("../api.js");
const router = require("../router.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const progress = require("../util/progress.js");
const topNavigation = require("../models/top_navigation.js");
const Post = require("../models/post.js");
const Tag = require("../models/tag.js");
const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require("../views/empty_view.js");
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const progress = require('../util/progress.js');
const topNavigation = require('../models/top_navigation.js');
const Post = require('../models/post.js');
const Tag = require('../models/tag.js');
const PostUploadView = require('../views/post_upload_view.js');
const EmptyView = require('../views/empty_view.js');
const genericErrorMessage =
"One or more posts needs your attention; " +
'One of the posts needs your attention; ' +
'click "resume upload" when you\'re ready.';
class PostUploadController {
constructor() {
this._lastCancellablePromise = null;
if (!api.hasPrivilege("posts:create")) {
if (!api.hasPrivilege('posts:create')) {
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;
}
topNavigation.activate("upload");
topNavigation.setTitle("Upload");
topNavigation.activate('upload');
topNavigation.setTitle('Upload');
this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege("posts:create:anonymous"),
canViewPosts: api.hasPrivilege("posts:view"),
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
canViewPosts: api.hasPrivilege('posts:view'),
enableSafety: api.safetyEnabled(),
});
this._view.addEventListener("change", (e) => this._evtChange(e));
this._view.addEventListener("submit", (e) => this._evtSubmit(e));
this._view.addEventListener("cancel", (e) => this._evtCancel(e));
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e));
this._view.addEventListener('cancel', e => this._evtCancel(e));
}
_evtChange(e) {
@ -55,79 +55,47 @@ class PostUploadController {
_evtSubmit(e) {
this._view.disableForm();
this._view.clearMessages();
let anyFailures = false;
e.detail.uploadables
.reduce(
e.detail.uploadables.reduce(
(promise, uploadable) =>
promise.then(() =>
this._uploadSinglePost(
uploadable,
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()
)
promise.then(() => this._uploadSinglePost(
uploadable, e.detail.skipDuplicates)),
Promise.resolve())
.then(() => {
if (anyFailures) {
return Promise.reject();
}
})
.then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Posts uploaded.");
},
(error) => {
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Posts uploaded.');
}, error => {
if (error.uploadable) {
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.enableForm();
this._view.showError(
error.message, error.uploadable);
}
);
} else {
this._view.showError(error.message);
}
this._view.enableForm();
});
}
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
_uploadSinglePost(uploadable, skipDuplicates) {
progress.start();
let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) {
reverseSearchPromise = Post.reverseSearch(
uploadable.url || uploadable.file
);
reverseSearchPromise =
Post.reverseSearch(uploadable.url || uploadable.file);
}
this._lastCancellablePromise = reverseSearchPromise;
return reverseSearchPromise
.then((searchResult) => {
return reverseSearchPromise.then(searchResult => {
if (searchResult) {
// notify about exact duplicate
if (searchResult.exactPost) {
@ -135,24 +103,18 @@ class PostUploadController {
this._view.removeUploadable(uploadable);
return Promise.resolve();
} else {
let error = new Error(
"Post already uploaded " +
`(@${searchResult.exactPost.id})`
);
let error = new Error('Post already uploaded ' +
`(@${searchResult.exactPost.id})`);
error.uploadable = uploadable;
return Promise.reject(error);
}
}
// notify about similar posts
if (
searchResult.similarPosts.length &&
!alwaysUploadSimilar
) {
if (searchResult.similarPosts.length) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload."
);
'posts.\nYou can resume or discard this upload.');
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
@ -161,24 +123,21 @@ class PostUploadController {
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous).then(() => {
let savePromise = post.save(uploadable.anonymous)
.then(() => {
this._view.removeUploadable(uploadable);
return Promise.resolve();
});
this._lastCancellablePromise = savePromise;
return savePromise;
})
.then(
(result) => {
}).then(result => {
progress.done();
return Promise.resolve(result);
},
(error) => {
}, error => {
error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
}
);
});
}
_uploadableToPost(uploadable) {
@ -194,15 +153,13 @@ class PostUploadController {
post.newContent = uploadable.url || uploadable.file;
// if uploadable.source is ever going to be a valid field (e.g when setting source directly in the upload window)
// you'll need to change the line below to `post.source = uploadable.source || uploadable.url;`
if (uploadable.url) {
post.source = uploadable.url;
}
if (uploadable.url) post.source = uploadable.url;
return post;
}
}
module.exports = (router) => {
router.enter(["upload"], (ctx, next) => {
module.exports = router => {
router.enter(['upload'], (ctx, next) => {
ctx.controller = new PostUploadController();
});
};

View file

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

View file

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

View file

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

View file

@ -1,37 +1,34 @@
"use strict";
'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 Tag = require("../models/tag.js");
const TagCategoryList = require("../models/tag_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const TagView = require("../views/tag_view.js");
const EmptyView = require("../views/empty_view.js");
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Tag = require('../models/tag.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagView = require('../views/tag_view.js');
const EmptyView = require('../views/empty_view.js');
class TagController {
constructor(ctx, section) {
if (!api.hasPrivilege("tags:view")) {
if (!api.hasPrivilege('tags:view')) {
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;
}
Promise.all([
TagCategoryList.get(),
Tag.get(ctx.parameters.name),
]).then(
(responses) => {
]).then(responses => {
const [tagCategoriesResponse, tag] = responses;
topNavigation.activate("tags");
topNavigation.setTitle("Tag #" + tag.names[0]);
topNavigation.activate('tags');
topNavigation.setTitle('Tag #' + tag.names[0]);
this._name = ctx.parameters.name;
tag.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
tag.addEventListener('change', e => this._evtSaved(e, section));
const categories = {};
for (let category of tagCategoriesResponse.results) {
@ -41,40 +38,26 @@ class TagController {
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"),
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,
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.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) {
@ -85,88 +68,75 @@ class TagController {
misc.disableExitConfirmation();
if (this._name !== e.detail.tag.names[0]) {
router.replace(
uri.formatClientLink("tag", e.detail.tag.names[0], section),
null,
false
);
uri.formatClientLink('tag', e.detail.tag.names[0], section),
null, false);
}
}
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) {
if (e.detail.names !== undefined) {
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;
}
if (e.detail.description !== undefined && e.detail.description !== null) {
if (e.detail.description !== undefined) {
e.detail.tag.description = e.detail.description;
}
e.detail.tag.save().then(
() => {
this._view.showSuccess("Tag saved.");
e.detail.tag.save().then(() => {
this._view.showSuccess('Tag saved.');
this._view.enableForm();
},
(error) => {
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag.merge(e.detail.targetTagName, e.detail.addAlias).then(
() => {
this._view.showSuccess("Tag merged.");
e.detail.tag
.merge(e.detail.targetTagName, e.detail.addAlias)
.then(() => {
this._view.showSuccess('Tag merged.');
this._view.enableForm();
router.replace(
uri.formatClientLink(
"tag",
e.detail.targetTagName,
"merge"
),
null,
false
);
},
(error) => {
'tag', e.detail.targetTagName, 'merge'),
null, false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("tags"));
ctx.controller.showSuccess("Tag deleted.");
},
(error) => {
e.detail.tag.delete()
.then(() => {
const ctx = router.show(uri.formatClientLink('tags'));
ctx.controller.showSuccess('Tag deleted.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
}
module.exports = (router) => {
router.enter(["tag", ":name", "edit"], (ctx, next) => {
ctx.controller = new TagController(ctx, "edit");
module.exports = router => {
router.enter(['tag', ':name', 'edit'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'edit');
});
router.enter(["tag", ":name", "merge"], (ctx, next) => {
ctx.controller = new TagController(ctx, "merge");
router.enter(['tag', ':name', 'merge'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'merge');
});
router.enter(["tag", ":name", "delete"], (ctx, next) => {
ctx.controller = new TagController(ctx, "delete");
router.enter(['tag', ':name', 'delete'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'delete');
});
router.enter(["tag", ":name"], (ctx, next) => {
ctx.controller = new TagController(ctx, "summary");
router.enter(['tag', ':name'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'summary');
});
};

View file

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

View file

@ -1,20 +1,19 @@
"use strict";
'use strict';
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const TopNavigationView = require("../views/top_navigation_view.js");
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require('../views/top_navigation_view.js');
class TopNavigationController {
constructor() {
api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener("activate", (e) =>
this._evtActivate(e)
);
topNavigation.addEventListener(
'activate', e => this._evtActivate(e));
api.addEventListener("login", (e) => this._evtAuthChange(e));
api.addEventListener("logout", (e) => this._evtAuthChange(e));
api.addEventListener('login', e => this._evtAuthChange(e));
api.addEventListener('logout', e => this._evtAuthChange(e));
this._render();
});
@ -29,41 +28,37 @@ class TopNavigationController {
}
_updateNavigationFromPrivileges() {
topNavigation.get("account").url = "user/" + api.userName;
topNavigation.get("account").imageUrl = api.user
? api.user.avatarUrl
: null;
topNavigation.get('account').url = 'user/' + api.userName;
topNavigation.get('account').imageUrl =
api.user ? api.user.avatarUrl : null;
topNavigation.showAll();
if (!api.hasPrivilege("posts:list")) {
topNavigation.hide("posts");
if (!api.hasPrivilege('posts:list')) {
topNavigation.hide('posts');
}
if (!api.hasPrivilege("posts:create")) {
topNavigation.hide("upload");
if (!api.hasPrivilege('posts:create')) {
topNavigation.hide('upload');
}
if (!api.hasPrivilege("comments:list")) {
topNavigation.hide("comments");
if (!api.hasPrivilege('comments:list')) {
topNavigation.hide('comments');
}
if (!api.hasPrivilege("tags:list")) {
topNavigation.hide("tags");
if (!api.hasPrivilege('tags:list')) {
topNavigation.hide('tags');
}
if (!api.hasPrivilege("users:list")) {
topNavigation.hide("users");
}
if (!api.hasPrivilege("pools:list")) {
topNavigation.hide("pools");
if (!api.hasPrivilege('users:list')) {
topNavigation.hide('users');
}
if (api.isLoggedIn()) {
if (!api.hasPrivilege("users:create:any")) {
topNavigation.hide("register");
if (!api.hasPrivilege('users:create:any')) {
topNavigation.hide('register');
}
topNavigation.hide("login");
topNavigation.hide('login');
} else {
if (!api.hasPrivilege("users:create:self")) {
topNavigation.hide("register");
if (!api.hasPrivilege('users:create:self')) {
topNavigation.hide('register');
}
topNavigation.hide("account");
topNavigation.hide("logout");
topNavigation.hide('account');
topNavigation.hide('logout');
}
}
@ -71,11 +66,10 @@ class TopNavigationController {
this._updateNavigationFromPrivileges();
this._topNavigationView.render({
items: topNavigation.getAll(),
name: api.getName(),
name: api.getName()
});
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 api = require("../api.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const User = require("../models/user.js");
const UserToken = require("../models/user_token.js");
const topNavigation = require("../models/top_navigation.js");
const UserView = require("../views/user_view.js");
const EmptyView = require("../views/empty_view.js");
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const User = require('../models/user.js');
const UserToken = require('../models/user_token.js');
const topNavigation = require('../models/top_navigation.js');
const UserView = require('../views/user_view.js');
const EmptyView = require('../views/empty_view.js');
class UserController {
constructor(ctx, section) {
const userName = ctx.parameters.name;
if (
!api.hasPrivilege("users:view") &&
!api.isLoggedIn({ name: userName })
) {
if (!api.hasPrivilege('users:view') &&
!api.isLoggedIn({name: userName})) {
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;
}
@ -27,39 +25,36 @@ class UserController {
this._errorMessages = [];
let userTokenPromise = Promise.resolve([]);
if (section === "list-tokens") {
userTokenPromise = UserToken.get(userName).then(
(userTokens) => {
return userTokens.map((token) => {
token.isCurrentAuthToken =
api.isCurrentAuthToken(token);
if (section === 'list-tokens') {
userTokenPromise = UserToken.get(userName)
.then(userTokens => {
return userTokens.map(token => {
token.isCurrentAuthToken = api.isCurrentAuthToken(token);
return token;
});
},
(error) => {
}, error => {
return [];
}
);
});
}
topNavigation.setTitle("User " + userName);
Promise.all([userTokenPromise, User.get(userName)]).then(
(responses) => {
topNavigation.setTitle('User ' + userName);
Promise.all([
userTokenPromise,
User.get(userName)
]).then(responses => {
const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? "self" : "any";
const infix = isLoggedIn ? 'self' : 'any';
this._name = userName;
user.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
user.addEventListener('change', e => this._evtSaved(e, section));
const myRankIndex = api.user
? api.allRanks.indexOf(api.user.rank)
: 0;
const myRankIndex = api.user ?
api.allRanks.indexOf(api.user.rank) :
0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === "anonymous") {
if (rankIdentifier === 'anonymous') {
continue;
}
if (rankIdx > myRankIndex) {
@ -69,9 +64,9 @@ class UserController {
}
if (isLoggedIn) {
topNavigation.activate("account");
topNavigation.activate('account');
} else {
topNavigation.activate("users");
topNavigation.activate('users');
}
this._view = new UserView({
@ -79,49 +74,25 @@ class UserController {
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`
),
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`
),
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}`
),
canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
canDeleteToken: api.hasPrivilege(
`userTokens:delete:${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)
);
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);
@ -130,25 +101,24 @@ class UserController {
for (let message of this._errorMessages) {
this.showError(message);
}
},
(error) => {
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
});
}
showSuccess(message) {
if (typeof this._view === "undefined") {
this._successMessages.push(message);
if (typeof this._view === 'undefined') {
this._successMessages.push(message)
} else {
this._view.showSuccess(message);
}
}
showError(message) {
if (typeof this._view === "undefined") {
this._errorMessages.push(message);
if (typeof this._view === 'undefined') {
this._errorMessages.push(message)
} else {
this._view.showError(message);
}
@ -162,10 +132,8 @@ class UserController {
misc.disableExitConfirmation();
if (this._name !== e.detail.user.name) {
router.replace(
uri.formatClientLink("user", e.detail.user.name, section),
null,
false
);
uri.formatClientLink('user', e.detail.user.name, section),
null, false);
}
}
@ -173,128 +141,95 @@ class UserController {
this._view.clearMessages();
this._view.disableForm();
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;
}
if (e.detail.email !== undefined && e.detail.email !== null) {
if (e.detail.email !== undefined) {
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;
}
if (e.detail.password !== undefined && e.detail.password !== null) {
if (e.detail.password !== undefined) {
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;
if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent;
}
}
e.detail.user
.save()
.then(() => {
return isLoggedIn
? api.login(
e.detail.user.save().then(() => {
return isLoggedIn ?
api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false
)
: Promise.resolve();
})
.then(
() => {
this._view.showSuccess("Settings updated.");
false) :
Promise.resolve();
}).then(() => {
this._view.showSuccess('Settings updated.');
this._view.enableForm();
},
(error) => {
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
e.detail.user.delete().then(
() => {
e.detail.user.delete()
.then(() => {
if (isLoggedIn) {
api.forget();
api.logout();
}
if (api.hasPrivilege("users:list")) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("Account deleted.");
if (api.hasPrivilege('users:list')) {
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('Account deleted.');
} else {
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.enableForm();
}
);
});
}
_evtCreateToken(e) {
this._view.clearMessages();
this._view.disableForm();
UserToken.create(
e.detail.user.name,
e.detail.note,
e.detail.expirationTime
).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " created."
);
},
(error) => {
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
.then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' created.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
_evtDeleteToken(e) {
this._view.clearMessages();
this._view.disableForm();
if (api.isCurrentAuthToken(e.detail.userToken)) {
router.show(uri.formatClientLink("logout"));
router.show(uri.formatClientLink('logout'));
} else {
e.detail.userToken.delete(e.detail.user.name).then(
() => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + e.detail.userToken.token + " deleted."
);
},
(error) => {
e.detail.userToken.delete(e.detail.user.name)
.then(() => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
}
@ -302,42 +237,31 @@ class UserController {
this._view.clearMessages();
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.save(e.detail.user.name).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " updated."
);
},
(error) => {
e.detail.userToken.save(e.detail.user.name).then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' updated.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
}
module.exports = (router) => {
router.enter(["user", ":name"], (ctx, next) => {
ctx.controller = new UserController(ctx, "summary");
module.exports = router => {
router.enter(['user', ':name'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary');
});
router.enter(["user", ":name", "edit"], (ctx, next) => {
ctx.controller = new UserController(ctx, "edit");
router.enter(['user', ':name', 'edit'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
});
router.enter(["user", ":name", "list-tokens"], (ctx, next) => {
ctx.controller = new UserController(ctx, "list-tokens");
router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'list-tokens');
});
router.enter(["user", ":name", "delete"], (ctx, next) => {
ctx.controller = new UserController(ctx, "delete");
router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
});
};

View file

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

View file

@ -1,25 +1,25 @@
"use strict";
'use strict';
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const User = require("../models/user.js");
const topNavigation = require("../models/top_navigation.js");
const RegistrationView = require("../views/registration_view.js");
const EmptyView = require("../views/empty_view.js");
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
const EmptyView = require('../views/empty_view.js');
class UserRegistrationController {
constructor() {
if (!api.hasPrivilege("users:create:self")) {
if (!api.hasPrivilege('users:create:self')) {
this._view = new EmptyView();
this._view.showError("Registration is closed.");
this._view.showError('Registration is closed.');
return;
}
topNavigation.activate("register");
topNavigation.setTitle("Registration");
topNavigation.activate('register');
topNavigation.setTitle('Registration');
this._view = new RegistrationView();
this._view.addEventListener("submit", (e) => this._evtRegister(e));
this._view.addEventListener('submit', e => this._evtRegister(e));
}
_evtRegister(e) {
@ -30,35 +30,30 @@ class UserRegistrationController {
user.email = e.detail.email;
user.password = e.detail.password;
const isLoggedIn = api.isLoggedIn();
user.save()
.then(() => {
user.save().then(() => {
if (isLoggedIn) {
return Promise.resolve();
} else {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}
})
.then(
() => {
}).then(() => {
if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("User added!");
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('User added!');
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("Welcome aboard!");
ctx.controller.showSuccess('Welcome aboard!');
}
},
(error) => {
}, error => {
this._view.showError(error.message);
this._view.enableForm();
}
);
});
}
}
module.exports = (router) => {
router.enter(["register"], (ctx, next) => {
module.exports = router => {
router.enter(['register'], (ctx, next) => {
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_RETURN = 13;
@ -10,14 +10,14 @@ const KEY_UP = 38;
const KEY_DOWN = 40;
function _getSelectionStart(input) {
if ("selectionStart" in input) {
if ('selectionStart' in input) {
return input.selectionStart;
}
if (document.selection) {
input.focus();
const sel = document.selection.createRange();
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 0;
@ -27,22 +27,18 @@ class AutoCompleteControl {
constructor(sourceInputNode, options) {
this._sourceInputNode = sourceInputNode;
this._options = {};
Object.assign(
this._options,
{
Object.assign(this._options, {
verticalShift: 2,
maxResults: 15,
getTextToFind: () => {
const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, "");
return value.substring(0, start).replace(/.*\s+/, '');
},
confirm: null,
delete: null,
getMatches: null,
},
options
);
}, options);
this._showTimeout = null;
this._results = [];
@ -53,25 +49,22 @@ class AutoCompleteControl {
hide() {
window.clearTimeout(this._showTimeout);
this._suggestionDiv.style.display = "none";
this._suggestionDiv.style.display = 'none';
this._isVisible = false;
}
replaceSelectedText(result, addSpace) {
const start = _getSelectionStart(this._sourceInputNode);
let prefix = "";
let prefix = '';
let suffix = this._sourceInputNode.value.substring(start);
let middle = this._sourceInputNode.value.substring(0, start);
const spaceIndex = middle.lastIndexOf(" ");
const commaIndex = middle.lastIndexOf(",");
const index = spaceIndex < commaIndex ? commaIndex : spaceIndex;
const delimiter = spaceIndex < commaIndex ? "" : " ";
const index = middle.lastIndexOf(' ');
if (index !== -1) {
prefix = this._sourceInputNode.value.substring(0, index + 1);
middle = this._sourceInputNode.value.substring(index + 1);
}
this._sourceInputNode.value =
prefix + result.toString() + delimiter + suffix.trimLeft();
this._sourceInputNode.value = (
prefix + result.toString() + ' ' + suffix.trimLeft());
if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim();
}
@ -93,7 +86,7 @@ class AutoCompleteControl {
}
_show() {
this._suggestionDiv.style.display = "block";
this._suggestionDiv.style.display = 'block';
this._isVisible = true;
}
@ -108,32 +101,27 @@ class AutoCompleteControl {
_install() {
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(
"Autocompletion was already added for this element"
);
'Autocompletion was already added for this element');
}
this._sourceInputNode.setAttribute("data-autocomplete", true);
this._sourceInputNode.setAttribute("autocomplete", "off");
this._sourceInputNode.setAttribute('data-autocomplete', true);
this._sourceInputNode.setAttribute('autocomplete', 'off');
this._sourceInputNode.addEventListener("keydown", (e) =>
this._evtKeyDown(e)
);
this._sourceInputNode.addEventListener("blur", (e) =>
this._evtBlur(e)
);
this._sourceInputNode.addEventListener(
'keydown', e => this._evtKeyDown(e));
this._sourceInputNode.addEventListener(
'blur', e => this._evtBlur(e));
this._suggestionDiv = views.htmlToDom(
'<div class="autocomplete"><ul></ul></div>'
);
this._suggestionList = this._suggestionDiv.querySelector("ul");
'<div class="autocomplete"><ul></ul></div>');
this._suggestionList = this._suggestionDiv.querySelector('ul');
document.body.appendChild(this._suggestionDiv);
views.monitorNodeRemoval(this._sourceInputNode, () => {
this._uninstall();
});
views.monitorNodeRemoval(
this._sourceInputNode, () => { this._uninstall(); });
}
_uninstall() {
@ -149,21 +137,13 @@ class AutoCompleteControl {
if (key === KEY_ESCAPE) {
func = this.hide;
} else if (key === KEY_TAB && shift) {
func = () => {
this._selectPrevious();
};
func = () => { this._selectPrevious(); };
} else if (key === KEY_TAB && !shift) {
func = () => {
this._selectNext();
};
func = () => { this._selectNext(); };
} else if (key === KEY_UP) {
func = () => {
this._selectPrevious();
};
func = () => { this._selectPrevious(); };
} else if (key === KEY_DOWN) {
func = () => {
this._selectNext();
};
func = () => { this._selectNext(); };
} else if (key === KEY_RETURN && this._activeResult >= 0) {
func = () => {
this._confirm(this._getActiveSuggestion());
@ -184,17 +164,14 @@ class AutoCompleteControl {
func();
} else {
window.clearTimeout(this._showTimeout);
this._showTimeout = window.setTimeout(() => {
this._showOrHide();
}, 250);
this._showTimeout = window.setTimeout(
() => { this._showOrHide(); }, 250);
}
}
_evtBlur(e) {
window.clearTimeout(this._showTimeout);
window.setTimeout(() => {
this.hide();
}, 50);
window.setTimeout(() => { this.hide(); }, 50);
}
_getActiveSuggestion() {
@ -205,11 +182,9 @@ class AutoCompleteControl {
}
_selectPrevious() {
this._select(
this._activeResult === -1
? this._results.length - 1
: this._activeResult - 1
);
this._select(this._activeResult === -1 ?
this._results.length - 1 :
this._activeResult - 1);
}
_selectNext() {
@ -217,18 +192,15 @@ class AutoCompleteControl {
}
_select(newActiveResult) {
this._activeResult = newActiveResult.between(
0,
this._results.length - 1,
true
)
? newActiveResult
: -1;
this._activeResult =
newActiveResult.between(0, this._results.length - 1, true) ?
newActiveResult :
-1;
this._refreshActiveResult();
}
_updateResults(textToFind) {
this._options.getMatches(textToFind).then((matches) => {
this._options.getMatches(textToFind).then(matches => {
const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults);
const oldResultsHash = JSON.stringify(oldResults);
@ -251,17 +223,21 @@ class AutoCompleteControl {
}
for (let [resultIndex, resultItem] of this._results.entries()) {
let resultIndexWorkaround = resultIndex;
const listItem = document.createElement("li");
const link = document.createElement("a");
const listItem = document.createElement('li');
const link = document.createElement('a');
link.innerHTML = resultItem.caption;
link.setAttribute("href", "");
link.setAttribute("data-key", resultItem.value);
link.addEventListener("mouseenter", (e) => {
link.setAttribute('href', '');
link.setAttribute('data-key', resultItem.value);
link.addEventListener(
'mouseenter',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._refreshActiveResult();
});
link.addEventListener("mousedown", (e) => {
link.addEventListener(
'mousedown',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
@ -273,8 +249,8 @@ class AutoCompleteControl {
this._refreshActiveResult();
// display the suggestions offscreen to get the height
this._suggestionDiv.style.left = "-9999px";
this._suggestionDiv.style.top = "-9999px";
this._suggestionDiv.style.left = '-9999px';
this._suggestionDiv.style.top = '-9999px';
this._show();
const verticalShift = this._options.verticalShift;
const inputRect = this._sourceInputNode.getBoundingClientRect();
@ -288,44 +264,38 @@ class AutoCompleteControl {
inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
let x = inputRect.left - bodyRect.left;
let y =
direction === 1
? inputRect.bottom - bodyRect.top - verticalShift
: inputRect.top -
bodyRect.top -
listRect.height +
verticalShift;
let y = direction == 1 ?
inputRect.bottom - bodyRect.top - verticalShift :
inputRect.top - bodyRect.top - listRect.height + verticalShift;
// remove offscreen items until whole suggestion list can fit on the
// screen
while (
(y < 0 || y + listRect.height > viewPortHeight) &&
this._suggestionList.childNodes.length
) {
while ((y < 0 || y + listRect.height > viewPortHeight) &&
this._suggestionList.childNodes.length) {
this._suggestionList.removeChild(this._suggestionList.lastChild);
const prevHeight = listRect.height;
listRect = this._suggestionDiv.getBoundingClientRect();
const heightDelta = prevHeight - listRect.height;
if (direction === -1) {
if (direction == -1) {
y += heightDelta;
}
}
this._suggestionDiv.style.left = x + "px";
this._suggestionDiv.style.top = y + "px";
this._suggestionDiv.style.left = x + 'px';
this._suggestionDiv.style.top = y + 'px';
}
_refreshActiveResult() {
let activeItem = this._suggestionList.querySelector("li.active");
let activeItem = this._suggestionList.querySelector('li.active');
if (activeItem) {
activeItem.classList.remove("active");
activeItem.classList.remove('active');
}
if (this._activeResult >= 0) {
const allItems = this._suggestionList.querySelectorAll("li");
const allItems = this._suggestionList.querySelectorAll('li');
activeItem = allItems[this._activeResult];
activeItem.classList.add("active");
}
activeItem.classList.add('active');
}
}
};
module.exports = AutoCompleteControl;

View file

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

View file

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

View file

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

View file

@ -1,9 +1,9 @@
"use strict";
'use strict';
const events = require("../events.js");
const views = require("../util/views.js");
const events = require('../events.js');
const views = require('../util/views.js');
const template = views.getTemplate("file-dropper");
const template = views.getTemplate('file-dropper');
const KEY_RETURN = 13;
@ -17,53 +17,37 @@ class FileDropperControl extends events.EventTarget {
allowMultiple: options.allowMultiple,
allowUrls: options.allowUrls,
lock: options.lock,
id: "file-" + Math.random().toString(36).substring(7),
id: 'file-' + Math.random().toString(36).substring(7),
urlPlaceholder:
options.urlPlaceholder || "Alternatively, paste an URL here.",
options.urlPlaceholder || 'Alternatively, paste an URL here.',
});
this._dropperNode = source.querySelector(".file-dropper");
this._urlInputNode = source.querySelector("input[type=text]");
this._urlConfirmButtonNode = source.querySelector("button");
this._fileInputNode = source.querySelector("input[type=file]");
this._fileInputNode.style.display = "none";
this._dropperNode = source.querySelector('.file-dropper');
this._urlInputNode = source.querySelector('input[type=text]');
this._urlConfirmButtonNode = source.querySelector('button');
this._fileInputNode = source.querySelector('input[type=file]');
this._fileInputNode.style.display = 'none';
this._fileInputNode.multiple = options.allowMultiple || false;
this._counter = 0;
this._dropperNode.addEventListener("dragenter", (e) =>
this._evtDragEnter(e)
);
this._dropperNode.addEventListener("dragleave", (e) =>
this._evtDragLeave(e)
);
this._dropperNode.addEventListener("dragover", (e) =>
this._evtDragOver(e)
);
this._dropperNode.addEventListener("drop", (e) => this._evtDrop(e));
this._fileInputNode.addEventListener("change", (e) =>
this._evtFileChange(e)
);
this._dropperNode.addEventListener(
'dragenter', e => this._evtDragEnter(e));
this._dropperNode.addEventListener(
'dragleave', e => this._evtDragLeave(e));
this._dropperNode.addEventListener(
'dragover', e => this._evtDragOver(e));
this._dropperNode.addEventListener(
'drop', e => this._evtDrop(e));
this._fileInputNode.addEventListener(
'change', e => this._evtFileChange(e));
if (this._urlInputNode) {
this._urlInputNode.addEventListener("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)
});
this._urlInputNode.addEventListener(
'keydown', e => this._evtUrlInputKeyDown(e));
}
if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener("click", (e) =>
this._evtUrlConfirmButtonClick(e)
);
}
document.onpaste = (e) => {
if (!document.getElementById("post-upload")) return;
this._evtPaste(e)
this._urlConfirmButtonNode.addEventListener(
'click', e => this._evtUrlConfirmButtonClick(e));
}
this._originalHtml = this._dropperNode.innerHTML;
@ -72,27 +56,24 @@ class FileDropperControl extends events.EventTarget {
reset() {
this._dropperNode.innerHTML = this._originalHtml;
this.dispatchEvent(new CustomEvent("reset"));
this.dispatchEvent(new CustomEvent('reset'));
}
_emitFiles(files) {
files = Array.from(files);
if (this._options.lock) {
this._dropperNode.innerText = files
.map((file) => file.name)
.join(", ");
this._dropperNode.innerText =
files.map(file => file.name).join(', ');
}
this.dispatchEvent(
new CustomEvent("fileadd", { detail: { files: files } })
);
new CustomEvent('fileadd', {detail: {files: files}}));
}
_emitUrls(urls) {
urls = Array.from(urls).map((url) => url.trim());
urls = Array.from(urls).map(url => url.trim());
if (this._options.lock) {
this._dropperNode.innerText = urls
.map((url) => url.split(/\//).reverse()[0])
.join(", ");
this._dropperNode.innerText =
urls.map(url => url.split(/\//).reverse()[0]).join(', ');
}
for (let url of urls) {
if (!url) {
@ -103,20 +84,18 @@ class FileDropperControl extends events.EventTarget {
return;
}
}
this.dispatchEvent(
new CustomEvent("urladd", { detail: { urls: urls } })
);
this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}}));
}
_evtDragEnter(e) {
this._dropperNode.classList.add("active");
this._dropperNode.classList.add('active');
this._counter++;
}
_evtDragLeave(e) {
this._counter--;
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) {
e.preventDefault();
this._dropperNode.classList.remove("active");
this._dropperNode.classList.remove('active');
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) {
window.alert("Cannot select multiple files.");
window.alert('Cannot select multiple 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) {
if (e.which !== KEY_RETURN) {
return;
}
e.preventDefault();
this._dropperNode.classList.remove("active");
this._dropperNode.classList.remove('active');
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = "";
this._urlInputNode.value = '';
}
_evtUrlConfirmButtonClick(e) {
e.preventDefault();
this._dropperNode.classList.remove("active");
this._dropperNode.classList.remove('active');
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 views = require("../util/views.js");
const optimizedResize = require("../util/optimized_resize.js");
const settings = require('../models/settings.js');
const views = require('../util/views.js');
const optimizedResize = require('../util/optimized_resize.js');
class PostContentControl {
constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) {
this._post = post;
this._viewportSizeCalculator = viewportSizeCalculator;
this._hostNode = hostNode;
this._template = views.getTemplate("post-content");
this._template = views.getTemplate('post-content');
let fitMode = settings.get().fitMode;
if (typeof fitFunctionOverride !== "undefined") {
if (typeof fitFunctionOverride !== 'undefined') {
fitMode = fitFunctionOverride;
}
this._currentFitFunction =
{
"fit-both": this.fitBoth,
"fit-original": this.fitOriginal,
"fit-width": this.fitWidth,
"fit-height": this.fitHeight,
this._currentFitFunction = {
'fit-both': this.fitBoth,
'fit-original': this.fitOriginal,
'fit-width': this.fitWidth,
'fit-height': this.fitHeight,
}[fitMode] || this.fitBoth;
this._install();
this._post.addEventListener("changeContent", (e) =>
this._evtPostContentChange(e)
);
this._post.addEventListener(
'changeContent', e => this._evtPostContentChange(e));
}
disableOverlay() {
this._hostNode.querySelector(".post-overlay").style.display = "none";
this._hostNode.querySelector('.post-overlay').style.display = 'none';
}
fitWidth() {
@ -94,48 +92,22 @@ class PostContentControl {
_resize(width, height) {
const resizeListenerNodes = [this._postContentNode].concat(
...this._postContentNode.querySelectorAll(".resize-listener")
);
...this._postContentNode.querySelectorAll('.resize-listener'));
for (let node of resizeListenerNodes) {
node.style.width = width + "px";
node.style.height = height + "px";
node.style.width = width + 'px';
node.style.height = height + 'px';
}
}
_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();
}
_install() {
this._reinstall();
optimizedResize.add(() => this._refreshSize());
views.monitorNodeRemoval(this._hostNode, () => {
this._uninstall();
});
views.monitorNodeRemoval(
this._hostNode, () => { this._uninstall(); });
}
_reinstall() {
@ -144,7 +116,7 @@ class PostContentControl {
autoplay: settings.get().autoplayVideos,
});
if (settings.get().transparencyGrid) {
newNode.classList.add("transparency-grid");
newNode.classList.add('transparency-grid');
}
if (this._postContentNode) {
this._hostNode.replaceChild(newNode, this._postContentNode);

View file

@ -1,17 +1,16 @@
"use strict";
'use strict';
const api = require("../api.js");
const events = require("../events.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const Note = require("../models/note.js");
const Point = require("../models/point.js");
const TagInputControl = require("./tag_input_control.js");
const PoolInputControl = require("./pool_input_control.js");
const ExpanderControl = require("../controls/expander_control.js");
const FileDropperControl = require("../controls/file_dropper_control.js");
const api = require('../api.js');
const events = require('../events.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const Note = require('../models/note.js');
const Point = require('../models/point.js');
const TagInputControl = require('./tag_input_control.js');
const ExpanderControl = require('../controls/expander_control.js');
const FileDropperControl = require('../controls/file_dropper_control.js');
const template = views.getTemplate("post-edit-sidebar");
const template = views.getTemplate('post-edit-sidebar');
class PostEditSidebarControl extends events.EventTarget {
constructor(hostNode, post, postContentControl, postNotesOverlayControl) {
@ -24,229 +23,170 @@ class PostEditSidebarControl extends events.EventTarget {
this._postNotesOverlayControl.switchToPassiveEdit();
views.replaceContent(
this._hostNode,
template({
views.replaceContent(this._hostNode, template({
post: this._post,
enableSafety: api.safetyEnabled(),
hasClipboard: document.queryCommandSupported("copy"),
canEditPostSafety: api.hasPrivilege("posts:edit:safety"),
canEditPostSource: api.hasPrivilege("posts:edit:source"),
canEditPostTags: api.hasPrivilege("posts:edit:tags"),
canEditPostRelations: api.hasPrivilege("posts:edit:relations"),
canEditPostNotes:
api.hasPrivilege("posts:edit:notes") &&
post.type !== "video" &&
post.type !== "flash",
canEditPostFlags: api.hasPrivilege("posts:edit:flags"),
canEditPostContent: api.hasPrivilege("posts:edit:content"),
canEditPostThumbnail: api.hasPrivilege("posts:edit:thumbnail"),
canEditPoolPosts: api.hasPrivilege("pools:edit:posts"),
canCreateAnonymousPosts: api.hasPrivilege(
"posts:create:anonymous"
),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canMergePosts: api.hasPrivilege("posts:merge"),
})
);
hasClipboard: document.queryCommandSupported('copy'),
canEditPostSafety: api.hasPrivilege('posts:edit:safety'),
canEditPostSource: api.hasPrivilege('posts:edit:source'),
canEditPostTags: api.hasPrivilege('posts:edit:tags'),
canEditPostRelations: api.hasPrivilege('posts:edit:relations'),
canEditPostNotes: api.hasPrivilege('posts:edit:notes') &&
post.type !== 'video' &&
post.type !== 'flash',
canEditPostFlags: api.hasPrivilege('posts:edit:flags'),
canEditPostContent: api.hasPrivilege('posts:edit:content'),
canEditPostThumbnail: api.hasPrivilege('posts:edit:thumbnail'),
canEditPostSource : api.hasPrivilege('posts:edit:source'),
canCreateAnonymousPosts: api.hasPrivilege('posts:create:anonymous'),
canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'),
canMergePosts: api.hasPrivilege('posts:merge'),
}));
new ExpanderControl(
"post-info",
"Basic info",
this._hostNode.querySelectorAll(
".safety, .relations, .flags, .post-source"
)
);
'post-info',
'Basic info',
this._hostNode.querySelectorAll('.safety, .relations, .flags, .post-source'));
this._tagsExpander = new ExpanderControl(
"post-tags",
'post-tags',
`Tags (${this._post.tags.length})`,
this._hostNode.querySelectorAll(".tags")
);
this._hostNode.querySelectorAll('.tags'));
this._notesExpander = new ExpanderControl(
"post-notes",
"Notes",
this._hostNode.querySelectorAll(".notes")
);
this._poolsExpander = new ExpanderControl(
"post-pools",
`Pools (${this._post.pools.length})`,
this._hostNode.querySelectorAll(".pools")
);
'post-notes',
'Notes',
this._hostNode.querySelectorAll('.notes'));
new ExpanderControl(
"post-content",
"Content",
this._hostNode.querySelectorAll(".post-content, .post-thumbnail")
);
'post-content',
'Content',
this._hostNode.querySelectorAll('.post-content, .post-thumbnail'));
new ExpanderControl(
"post-management",
"Management",
this._hostNode.querySelectorAll(".management")
);
'post-management',
'Management',
this._hostNode.querySelectorAll('.management'));
this._syncExpanderTitles();
if (this._formNode) {
this._formNode.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
this._formNode.addEventListener('submit', e => this._evtSubmit(e));
}
if (this._tagInputNode) {
this._tagControl = new TagInputControl(
this._tagInputNode,
post.tags
);
}
if (this._poolInputNode) {
this._poolControl = new PoolInputControl(
this._poolInputNode,
post.pools
);
this._tagInputNode, post.tags);
}
if (this._contentInputNode) {
this._contentFileDropper = new FileDropperControl(
this._contentInputNode,
{
this._contentInputNode, {
allowUrls: true,
lock: true,
urlPlaceholder: "...or paste an URL here.",
}
);
this._contentFileDropper.addEventListener("fileadd", (e) => {
urlPlaceholder: '...or paste an URL here.'});
this._contentFileDropper.addEventListener('fileadd', e => {
this._newPostContent = e.detail.files[0];
});
this._contentFileDropper.addEventListener("urladd", (e) => {
this._contentFileDropper.addEventListener('urladd', e => {
this._newPostContent = e.detail.urls[0];
});
}
if (this._thumbnailInputNode) {
this._thumbnailFileDropper = new FileDropperControl(
this._thumbnailInputNode,
{ lock: true }
);
this._thumbnailFileDropper.addEventListener("fileadd", (e) => {
this._thumbnailInputNode, {lock: true});
this._thumbnailFileDropper.addEventListener('fileadd', e => {
this._newPostThumbnail = e.detail.files[0];
this._thumbnailRemovalLinkNode.style.display = "block";
this._thumbnailRemovalLinkNode.style.display = 'block';
});
}
if (this._thumbnailRemovalLinkNode) {
this._thumbnailRemovalLinkNode.addEventListener("click", (e) =>
this._evtRemoveThumbnailClick(e)
);
this._thumbnailRemovalLinkNode.style.display = this._post
.hasCustomThumbnail
? "block"
: "none";
this._thumbnailRemovalLinkNode.addEventListener(
'click', e => this._evtRemoveThumbnailClick(e));
this._thumbnailRemovalLinkNode.style.display =
this._post.hasCustomThumbnail ? 'block' : 'none';
}
if (this._addNoteLinkNode) {
this._addNoteLinkNode.addEventListener("click", (e) =>
this._evtAddNoteClick(e)
);
this._addNoteLinkNode.addEventListener(
'click', e => this._evtAddNoteClick(e));
}
if (this._copyNotesLinkNode) {
this._copyNotesLinkNode.addEventListener("click", (e) =>
this._evtCopyNotesClick(e)
);
this._copyNotesLinkNode.addEventListener(
'click', e => this._evtCopyNotesClick(e));
}
if (this._pasteNotesLinkNode) {
this._pasteNotesLinkNode.addEventListener("click", (e) =>
this._evtPasteNotesClick(e)
);
this._pasteNotesLinkNode.addEventListener(
'click', e => this._evtPasteNotesClick(e));
}
if (this._deleteNoteLinkNode) {
this._deleteNoteLinkNode.addEventListener("click", (e) =>
this._evtDeleteNoteClick(e)
);
this._deleteNoteLinkNode.addEventListener(
'click', e => this._evtDeleteNoteClick(e));
}
if (this._featureLinkNode) {
this._featureLinkNode.addEventListener("click", (e) =>
this._evtFeatureClick(e)
);
this._featureLinkNode.addEventListener(
'click', e => this._evtFeatureClick(e));
}
if (this._mergeLinkNode) {
this._mergeLinkNode.addEventListener("click", (e) =>
this._evtMergeClick(e)
);
this._mergeLinkNode.addEventListener(
'click', e => this._evtMergeClick(e));
}
if (this._deleteLinkNode) {
this._deleteLinkNode.addEventListener("click", (e) =>
this._evtDeleteClick(e)
);
this._deleteLinkNode.addEventListener(
'click', e => this._evtDeleteClick(e));
}
this._postNotesOverlayControl.addEventListener("blur", (e) =>
this._evtNoteBlur(e)
);
this._postNotesOverlayControl.addEventListener(
'blur', e => this._evtNoteBlur(e));
this._postNotesOverlayControl.addEventListener("focus", (e) =>
this._evtNoteFocus(e)
);
this._postNotesOverlayControl.addEventListener(
'focus', e => this._evtNoteFocus(e));
this._post.addEventListener("changeContent", (e) =>
this._evtPostContentChange(e)
);
this._post.addEventListener(
'changeContent', e => this._evtPostContentChange(e));
this._post.addEventListener("changeThumbnail", (e) =>
this._evtPostThumbnailChange(e)
);
this._post.addEventListener(
'changeThumbnail', e => this._evtPostThumbnailChange(e));
if (this._formNode) {
const inputNodes =
this._formNode.querySelectorAll("input, textarea");
const inputNodes = this._formNode.querySelectorAll(
'input, textarea');
for (let node of inputNodes) {
node.addEventListener("change", (e) =>
this.dispatchEvent(new CustomEvent("change"))
);
node.addEventListener(
'change',
e => this.dispatchEvent(new CustomEvent('change')));
}
this._postNotesOverlayControl.addEventListener("change", (e) =>
this.dispatchEvent(new CustomEvent("change"))
);
this._postNotesOverlayControl.addEventListener(
'change',
e => this.dispatchEvent(new CustomEvent('change')));
}
for (let eventType of ["add", "remove"]) {
this._post.notes.addEventListener(eventType, (e) => {
this._syncExpanderTitles();
});
this._post.pools.addEventListener(eventType, (e) => {
for (let eventType of ['add', 'remove']) {
this._post.notes.addEventListener(eventType, e => {
this._syncExpanderTitles();
});
}
this._tagControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._tagControl.addEventListener(
'change', e => {
this.dispatchEvent(new CustomEvent('change'));
this._syncExpanderTitles();
});
if (this._noteTextareaNode) {
this._noteTextareaNode.addEventListener("change", (e) =>
this._evtNoteTextChangeRequest(e)
);
}
if (this._poolControl) {
this._poolControl.addEventListener("change", (e) => {
this.dispatchEvent(new CustomEvent("change"));
this._syncExpanderTitles();
});
this._noteTextareaNode.addEventListener(
'change', e => this._evtNoteTextChangeRequest(e));
}
}
_syncExpanderTitles() {
this._notesExpander.title = `Notes (${this._post.notes.length})`;
this._tagsExpander.title = `Tags (${this._post.tags.length})`;
this._poolsExpander.title = `Pools (${this._post.pools.length})`;
}
_evtPostContentChange(e) {
@ -261,43 +201,37 @@ class PostEditSidebarControl extends events.EventTarget {
e.preventDefault();
this._thumbnailFileDropper.reset();
this._newPostThumbnail = null;
this._thumbnailRemovalLinkNode.style.display = "none";
this._thumbnailRemovalLinkNode.style.display = 'none';
}
_evtFeatureClick(e) {
e.preventDefault();
if (confirm("Are you sure you want to feature this post?")) {
this.dispatchEvent(
new CustomEvent("feature", {
if (confirm('Are you sure you want to feature this post?')) {
this.dispatchEvent(new CustomEvent('feature', {
detail: {
post: this._post,
},
})
);
}));
}
}
_evtMergeClick(e) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent("merge", {
this.dispatchEvent(new CustomEvent('merge', {
detail: {
post: this._post,
},
})
);
}));
}
_evtDeleteClick(e) {
e.preventDefault();
if (confirm("Are you sure you want to delete this post?")) {
this.dispatchEvent(
new CustomEvent("delete", {
if (confirm('Are you sure you want to delete this post?')) {
this.dispatchEvent(new CustomEvent('delete', {
detail: {
post: this._post,
},
})
);
}));
}
}
@ -309,64 +243,59 @@ class PostEditSidebarControl extends events.EventTarget {
_evtNoteFocus(e) {
this._editedNote = e.detail.note;
this._addNoteLinkNode.classList.remove("inactive");
this._deleteNoteLinkNode.classList.remove("inactive");
this._noteTextareaNode.removeAttribute("disabled");
this._addNoteLinkNode.classList.remove('inactive');
this._deleteNoteLinkNode.classList.remove('inactive');
this._noteTextareaNode.removeAttribute('disabled');
this._noteTextareaNode.value = e.detail.note.text;
}
_evtNoteBlur(e) {
this._evtNoteTextChangeRequest(null);
this._addNoteLinkNode.classList.remove("inactive");
this._deleteNoteLinkNode.classList.add("inactive");
this._addNoteLinkNode.classList.remove('inactive');
this._deleteNoteLinkNode.classList.add('inactive');
this._noteTextareaNode.blur();
this._noteTextareaNode.setAttribute("disabled", "disabled");
this._noteTextareaNode.value = "";
this._noteTextareaNode.setAttribute('disabled', 'disabled');
this._noteTextareaNode.value = '';
}
_evtAddNoteClick(e) {
e.preventDefault();
if (e.target.classList.contains("inactive")) {
if (e.target.classList.contains('inactive')) {
return;
}
this._addNoteLinkNode.classList.add("inactive");
this._addNoteLinkNode.classList.add('inactive');
this._postNotesOverlayControl.switchToDrawing();
}
_evtCopyNotesClick(e) {
e.preventDefault();
let textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.opacity = "0";
textarea.value = JSON.stringify(
[...this._post.notes].map((note) => ({
polygon: [...note.polygon].map((point) => [point.x, point.y]),
let textarea = document.createElement('textarea');
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
textarea.value = JSON.stringify([...this._post.notes].map(note => ({
polygon: [...note.polygon].map(
point => [point.x, point.y]),
text: note.text,
}))
);
})));
document.body.appendChild(textarea);
textarea.select();
let success = false;
try {
success = document.execCommand("copy");
success = document.execCommand('copy');
} catch (err) {
// continue regardless of error
}
textarea.blur();
document.body.removeChild(textarea);
alert(
success
? "Notes copied to clipboard."
: "Failed to copy the text to clipboard. Sorry."
);
alert(success
? 'Notes copied to clipboard.'
: 'Failed to copy the text to clipboard. Sorry.');
}
_evtPasteNotesClick(e) {
e.preventDefault();
const text = window.prompt(
"Please enter the exported notes snapshot:"
);
'Please enter the exported notes snapshot:');
if (!text) {
return;
}
@ -384,7 +313,7 @@ class PostEditSidebarControl extends events.EventTarget {
_evtDeleteNoteClick(e) {
e.preventDefault();
if (e.target.classList.contains("inactive")) {
if (e.target.classList.contains('inactive')) {
return;
}
this._post.notes.remove(this._editedNote);
@ -393,148 +322,125 @@ class PostEditSidebarControl extends events.EventTarget {
_evtSubmit(e) {
e.preventDefault();
this.dispatchEvent(
new CustomEvent("submit", {
this.dispatchEvent(new CustomEvent('submit', {
detail: {
post: this._post,
safety: this._safetyButtonNodes.length
? Array.from(this._safetyButtonNodes)
.filter((node) => node.checked)[0]
.value.toLowerCase()
: undefined,
safety: this._safetyButtonNodes.length ?
Array.from(this._safetyButtonNodes)
.filter(node => node.checked)[0]
.value.toLowerCase() :
undefined,
flags: this._videoFlags,
tags: this._tagInputNode
? misc.splitByWhitespace(this._tagInputNode.value)
: undefined,
tags: this._tagInputNode ?
misc.splitByWhitespace(this._tagInputNode.value) :
undefined,
pools: this._poolInputNode
? misc.splitByWhitespace(this._poolInputNode.value)
: undefined,
relations: this._relationsInputNode ?
misc.splitByWhitespace(this._relationsInputNode.value)
.map(x => parseInt(x)) :
undefined,
relations: this._relationsInputNode
? misc
.splitByWhitespace(
this._relationsInputNode.value
)
.map((x) => parseInt(x))
: undefined,
content: this._newPostContent ?
this._newPostContent :
undefined,
content: this._newPostContent
? this._newPostContent
: undefined,
thumbnail: this._newPostThumbnail !== undefined ?
this._newPostThumbnail :
undefined,
thumbnail:
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
? this._newPostThumbnail
: undefined,
source: this._sourceInputNode
? this._sourceInputNode.value
: undefined,
source: this._sourceInputNode ?
this._sourceInputNode.value :
undefined,
},
})
);
}));
}
get _formNode() {
return this._hostNode.querySelector("form");
return this._hostNode.querySelector('form');
}
get _submitButtonNode() {
return this._hostNode.querySelector(".submit");
return this._hostNode.querySelector('.submit');
}
get _safetyButtonNodes() {
return this._formNode.querySelectorAll(".safety input");
return this._formNode.querySelectorAll('.safety input');
}
get _tagInputNode() {
return this._formNode.querySelector(".tags input");
}
get _poolInputNode() {
return this._formNode.querySelector(".pools input");
return this._formNode.querySelector('.tags input');
}
get _loopVideoInputNode() {
return this._formNode.querySelector(".flags input[name=loop]");
return this._formNode.querySelector('.flags input[name=loop]');
}
get _soundVideoInputNode() {
return this._formNode.querySelector(".flags input[name=sound]");
return this._formNode.querySelector('.flags input[name=sound]');
}
get _videoFlags() {
if (!this._loopVideoInputNode) {
return undefined;
}
if (!this._loopVideoInputNode) return undefined;
let ret = [];
if (this._loopVideoInputNode.checked) {
ret.push("loop");
}
if (this._soundVideoInputNode.checked) {
ret.push("sound");
}
if (this._loopVideoInputNode.checked) ret.push('loop');
if (this._soundVideoInputNode.checked) ret.push('sound');
return ret;
}
get _relationsInputNode() {
return this._formNode.querySelector(".relations input");
return this._formNode.querySelector('.relations input');
}
get _contentInputNode() {
return this._formNode.querySelector(
".post-content .dropper-container"
);
return this._formNode.querySelector('.post-content .dropper-container');
}
get _thumbnailInputNode() {
return this._formNode.querySelector(
".post-thumbnail .dropper-container"
);
'.post-thumbnail .dropper-container');
}
get _thumbnailRemovalLinkNode() {
return this._formNode.querySelector(".post-thumbnail a");
return this._formNode.querySelector('.post-thumbnail a');
}
get _sourceInputNode() {
return this._formNode.querySelector(".post-source textarea");
return this._formNode.querySelector('.post-source textarea');
}
get _featureLinkNode() {
return this._formNode.querySelector(".management .feature");
return this._formNode.querySelector('.management .feature');
}
get _mergeLinkNode() {
return this._formNode.querySelector(".management .merge");
return this._formNode.querySelector('.management .merge');
}
get _deleteLinkNode() {
return this._formNode.querySelector(".management .delete");
return this._formNode.querySelector('.management .delete');
}
get _addNoteLinkNode() {
return this._formNode.querySelector(".notes .add");
return this._formNode.querySelector('.notes .add');
}
get _copyNotesLinkNode() {
return this._formNode.querySelector(".notes .copy");
return this._formNode.querySelector('.notes .copy');
}
get _pasteNotesLinkNode() {
return this._formNode.querySelector(".notes .paste");
return this._formNode.querySelector('.notes .paste');
}
get _deleteNoteLinkNode() {
return this._formNode.querySelector(".notes .delete");
return this._formNode.querySelector('.notes .delete');
}
get _noteTextareaNode() {
return this._formNode.querySelector(".notes textarea");
return this._formNode.querySelector('.notes textarea');
}
enableForm() {
@ -556,6 +462,6 @@ class PostEditSidebarControl extends events.EventTarget {
showError(message) {
views.showError(this._hostNode, message);
}
}
};
module.exports = PostEditSidebarControl;

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