Compare commits
200 commits
Author | SHA1 | Date | |
---|---|---|---|
48d5dfb4e6 | |||
541fec20ca | |||
be280acb17 | |||
|
bc7c2c7867 | ||
2e7547d3bc | |||
39bb53528c | |||
5ef62b21a0 | |||
|
61b9f81e39 | ||
|
b721865931 | ||
|
46e3295003 | ||
|
031131506e | ||
|
d102578b54 | ||
|
6edb25d87b | ||
|
93fc15f2a4 | ||
|
4f9d46e1c2 | ||
|
b72e81850d | ||
|
c1c695f082 | ||
|
4b6b231fc8 | ||
|
6b0c3cfc7f | ||
|
4ec8cb3ba2 | ||
|
8d971234a2 | ||
|
a16bb198ab | ||
|
3f182a66ad | ||
|
b52363e82d | ||
|
3bf45e4c0a | ||
|
5596f53744 | ||
|
da425afc49 | ||
|
d7394d672f | ||
|
190d795426 | ||
|
7c92ceaf6a | ||
|
9e00f37464 | ||
|
59c497e168 | ||
|
c292b96f06 | ||
|
7a82e9d581 | ||
|
4806bbe0ed | ||
|
c2fdc2d070 | ||
|
ffdf115714 | ||
|
782f069031 | ||
|
81f7ae8034 | ||
|
648121d7c3 | ||
|
42524503b9 | ||
|
8a03015349 | ||
|
2165b59158 | ||
|
244a0f0b6c | ||
|
da3b4790ad | ||
|
196f92593c | ||
|
d7d2a151a8 | ||
|
75635bbc43 | ||
|
e3062b1c77 | ||
|
e950fe7ea5 | ||
|
86f50ec742 | ||
|
8088ff3bbe | ||
|
da71c672dd | ||
|
42bb364dd0 | ||
|
5b43c5bebd | ||
|
6c3b50d287 | ||
|
6075ae9326 | ||
|
70f2164dc6 | ||
|
1b9ce79f4e | ||
|
7e5d48b6e8 | ||
|
e746f09911 | ||
|
6088e89ea1 | ||
|
79d0efc25b | ||
|
929071ea1a | ||
|
514b846781 | ||
|
b2582b7b0f | ||
|
82541536af | ||
|
8ad9457b24 | ||
|
6de0a74257 | ||
|
a22485afda | ||
|
e2419a30ba | ||
|
d5a6609f75 | ||
|
106dcc4135 | ||
|
a14ead1842 | ||
|
780b7dc6fd | ||
|
9f95e9eb90 | ||
|
9b3123a815 | ||
|
f3aa0eb801 | ||
|
98c0941c97 | ||
|
a5fbaae4b3 | ||
|
d699979d35 | ||
|
d083084407 | ||
|
ad9d3599bc | ||
|
c3b81371d8 | ||
|
c64983002e | ||
|
4f57f49ebe | ||
|
f58079e12e | ||
|
be0c867d25 | ||
|
f5338ca508 | ||
|
e4a253fd25 | ||
|
414106a477 | ||
|
fa4997fbb9 | ||
|
3cabe790a7 | ||
|
f497dca92f | ||
|
5ea9e27e48 | ||
|
027e83a7e7 | ||
|
dc46ed7929 | ||
|
a6886ddb89 | ||
|
a2b68925ac | ||
|
516b3a51a7 | ||
|
f4ca435657 | ||
|
2949431d9a | ||
|
1be2d95bb1 | ||
|
7e27df835c | ||
|
169593ea36 | ||
|
ca77149597 | ||
|
535aa0d8fe | ||
|
4ce72fa712 | ||
|
7c37734fec | ||
|
545b5828b5 | ||
|
7b54551b8e | ||
|
8fa84abdc4 | ||
|
b9451bef4a | ||
|
2b9a4ab786 | ||
|
c732e62844 | ||
|
c7461c7f65 | ||
|
2dfd1c2192 | ||
|
2bdb072296 | ||
|
7515b8e605 | ||
|
b3dbf1f0c6 | ||
|
bc69505382 | ||
|
05823e5dec | ||
|
4a23b615fc | ||
|
e1390633ff | ||
|
ed9b6c1f48 | ||
|
58678b4504 | ||
|
cdb3b7dddc | ||
|
ec0e9f29c7 | ||
|
5945271166 | ||
|
a302b2c4a4 | ||
|
143f633eaa | ||
|
eaa6107a6c | ||
|
afe4c5c847 | ||
|
697bd45420 | ||
|
a896c1a5a7 | ||
|
d4f72de8c2 | ||
|
b5d2e447fc | ||
|
d2b6ecef4d | ||
|
368372e36d | ||
|
06ad8b1882 | ||
|
1ef0419dc2 | ||
|
802051399f | ||
|
0dd427755b | ||
|
67a5dd7c18 | ||
|
4ab6aa5c85 | ||
|
f5111483af | ||
|
e656a3c46a | ||
|
c004eb36c2 | ||
|
1bbcaf11f7 | ||
|
3e69edc117 | ||
|
74c97efdef | ||
|
4595f9a2aa | ||
|
b74492974d | ||
|
3edc07b7f8 | ||
|
9189842524 | ||
|
800a79f95f | ||
|
13e2888ae4 | ||
|
b037ce80c3 | ||
|
0137cf383a | ||
|
342ca9ccba | ||
|
d420609f97 | ||
|
029c112011 | ||
|
b8c5b27195 | ||
|
018e3df31d | ||
|
57193b5715 | ||
|
c06aaa63af | ||
|
454685755b | ||
|
c0d0c4c894 | ||
|
4f46619b91 | ||
|
e7610db054 | ||
|
ea623449e7 | ||
|
c5358f7f83 | ||
|
4329b1620f | ||
|
48c9001194 | ||
|
ea675d20cb | ||
|
b0f1b8c230 | ||
|
1be947e946 | ||
|
7bcefeb347 | ||
|
5ca21f9e7f | ||
|
6b8e3f251f | ||
|
ffba010ae4 | ||
|
8795279a73 | ||
|
e6bf102bc0 | ||
|
d59ecb8e23 | ||
|
6a95a66f12 | ||
|
deffe91fda | ||
|
8c01c7714f | ||
|
377fe52072 | ||
|
cd6683c2d8 | ||
|
2c6434b08d | ||
|
99a69333e6 | ||
|
08e62ec885 | ||
|
65202189e1 | ||
|
c60ec22b92 | ||
|
ed83e11552 | ||
|
db0c33bb14 | ||
|
1a8de9ef3a | ||
|
6cc2a91632 | ||
|
bd9284b7f8 | ||
|
4c78cf8c47 |
404 changed files with 29064 additions and 12424 deletions
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts require LF
|
||||
*.sh text eol=lf
|
108
.github/workflows/build-containers.yml
vendored
Normal file
108
.github/workflows/build-containers.yml
vendored
Normal file
|
@ -0,0 +1,108 @@
|
|||
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
|
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
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
3
.gitignore
vendored
|
@ -13,3 +13,6 @@ server/**/lib/
|
|||
server/**/bin/
|
||||
server/**/pyvenv.cfg
|
||||
__pycache__/
|
||||
|
||||
data/
|
||||
sql/
|
||||
|
|
62
.pre-commit-config.yaml
Normal file
62
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,62 @@
|
|||
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
|
|
@ -3,11 +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](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
scrubbing](https://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)
|
||||
- Post comments
|
||||
- Post notes / annotations, including arbitrary polygons
|
||||
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||
|
@ -19,6 +20,7 @@ scrubbing](http://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
|
||||
|
@ -30,7 +32,8 @@ scrubbing](http://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)
|
||||
|
||||
Users who wish to avoid using Docker may find the [old installation instructions](doc/LEGACY_INSTALL.md) helpful.
|
||||
More installation resources, as well as related projects can be found on the
|
||||
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
|
||||
|
||||
## Screenshots
|
||||
|
||||
|
|
12
client/.eslintrc.yml
Normal file
12
client/.eslintrc.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
env:
|
||||
browser: true
|
||||
commonjs: true
|
||||
es6: true
|
||||
extends: 'prettier'
|
||||
globals:
|
||||
Atomics: readonly
|
||||
SharedArrayBuffer: readonly
|
||||
ignorePatterns:
|
||||
- build.js
|
||||
parserOptions:
|
||||
ecmaVersion: 11
|
4
client/.prettierrc.yml
Normal file
4
client/.prettierrc.yml
Normal file
|
@ -0,0 +1,4 @@
|
|||
parser: babel
|
||||
printWidth: 79
|
||||
tabWidth: 4
|
||||
quoteProps: consistent
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:9 as builder
|
||||
FROM --platform=$BUILDPLATFORM node:lts 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 scratch as approot
|
||||
FROM --platform=$BUILDPLATFORM scratch as approot
|
||||
|
||||
COPY docker-start.sh /
|
||||
|
||||
|
@ -22,7 +22,7 @@ WORKDIR /var/www
|
|||
COPY --from=builder /opt/app/public/ .
|
||||
|
||||
|
||||
FROM nginx:alpine
|
||||
FROM nginx:alpine as release
|
||||
|
||||
RUN apk --no-cache add dumb-init
|
||||
COPY --from=approot / /
|
||||
|
|
292
client/build.js
292
client/build.js
|
@ -4,28 +4,30 @@
|
|||
// -------------------------------------------------
|
||||
|
||||
const webapp_icons = [
|
||||
{name: 'android-chrome-192x192.png', size: 192},
|
||||
{name: 'android-chrome-512x512.png', size: 512},
|
||||
{name: 'apple-touch-icon.png', size: 180},
|
||||
{name: 'mstile-150x150.png', size: 150}
|
||||
{ name: 'android-chrome-192x192.png', size: 192 },
|
||||
{ name: 'android-chrome-512x512.png', size: 512 },
|
||||
{ name: 'apple-touch-icon.png', size: 180 },
|
||||
{ name: 'mstile-150x150.png', size: 150 }
|
||||
];
|
||||
|
||||
const webapp_splash_screens = [
|
||||
{w: 640, h: 1136, center: 320},
|
||||
{w: 750, h: 1294, center: 375},
|
||||
{w: 1125, h: 2436, center: 565},
|
||||
{w: 1242, h: 2148, center: 625},
|
||||
{w: 1536, h: 2048, center: 770},
|
||||
{w: 1668, h: 2224, center: 820},
|
||||
{w: 2048, h: 2732, center: 1024}
|
||||
{ w: 640, h: 1136, center: 320 },
|
||||
{ w: 750, h: 1294, center: 375 },
|
||||
{ w: 1125, h: 2436, center: 565 },
|
||||
{ w: 1242, h: 2148, center: 625 },
|
||||
{ w: 1536, h: 2048, center: 770 },
|
||||
{ w: 1668, h: 2224, center: 820 },
|
||||
{ w: 2048, h: 2732, center: 1024 }
|
||||
];
|
||||
|
||||
const external_js = [
|
||||
'underscore',
|
||||
'superagent',
|
||||
'mousetrap',
|
||||
'dompurify',
|
||||
'js-cookie',
|
||||
'marked',
|
||||
'mousetrap',
|
||||
'nprogress',
|
||||
'superagent',
|
||||
'underscore',
|
||||
];
|
||||
|
||||
const app_manifest = {
|
||||
|
@ -35,7 +37,7 @@ const app_manifest = {
|
|||
src: baseUrl() + 'img/android-chrome-192x192.png',
|
||||
type: 'image/png',
|
||||
sizes: '192x192'
|
||||
},
|
||||
},
|
||||
{
|
||||
src: baseUrl() + 'img/android-chrome-512x512.png',
|
||||
type: 'image/png',
|
||||
|
@ -55,6 +57,11 @@ 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');
|
||||
|
@ -110,7 +117,7 @@ function bundleHtml() {
|
|||
(match, number) => { return placeholders[number]; });
|
||||
|
||||
const functionText = underscore.template(
|
||||
templateText, {variable: 'ctx'}).source;
|
||||
templateText, { variable: 'ctx' }).source;
|
||||
|
||||
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
|
||||
}
|
||||
|
@ -129,7 +136,7 @@ function bundleCss() {
|
|||
|
||||
let css = '';
|
||||
for (const file of glob.sync('./css/**/*.styl')) {
|
||||
css += stylus.render(readTextFile(file), {filename: file});
|
||||
css += stylus.render(readTextFile(file), { filename: file });
|
||||
}
|
||||
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
|
||||
if (process.argv.includes('--gzip')) {
|
||||
|
@ -146,59 +153,69 @@ function bundleCss() {
|
|||
console.info('Bundled CSS');
|
||||
}
|
||||
|
||||
function minifyJs(path) {
|
||||
return require('terser').minify(
|
||||
fs.readFileSync(path, 'utf-8'), { compress: { unused: false } }).code;
|
||||
}
|
||||
|
||||
function writeJsBundle(b, path, compress, callback) {
|
||||
let outputFile = fs.createWriteStream(path);
|
||||
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
|
||||
outputFile.on('finish', () => {
|
||||
if (compress) {
|
||||
fs.writeFileSync(path, minifyJs(path));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function bundleVendorJs(compress) {
|
||||
let b = browserify();
|
||||
for (let lib of external_js) {
|
||||
b.require(lib);
|
||||
}
|
||||
if (!process.argv.includes('--no-transpile')) {
|
||||
b.add(require.resolve('babel-polyfill'));
|
||||
}
|
||||
const file = './public/js/vendor.min.js';
|
||||
writeJsBundle(b, file, compress, () => {
|
||||
if (process.argv.includes('--gzip')) {
|
||||
gzipFile(file);
|
||||
}
|
||||
console.info('Bundled vendor JS');
|
||||
});
|
||||
}
|
||||
|
||||
function bundleAppJs(b, compress, callback) {
|
||||
const file = './public/js/app.min.js';
|
||||
writeJsBundle(b, file, compress, () => {
|
||||
if (process.argv.includes('--gzip')) {
|
||||
gzipFile(file);
|
||||
}
|
||||
console.info('Bundled app JS');
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function bundleJs() {
|
||||
const browserify = require('browserify');
|
||||
|
||||
function minifyJs(path) {
|
||||
return require('terser').minify(
|
||||
fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
|
||||
}
|
||||
|
||||
function writeJsBundle(b, path, compress, callback) {
|
||||
let outputFile = fs.createWriteStream(path);
|
||||
b.bundle().pipe(outputFile);
|
||||
outputFile.on('finish', () => {
|
||||
if (compress) {
|
||||
fs.writeFileSync(path, minifyJs(path));
|
||||
}
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
if (!process.argv.includes('--no-vendor-js')) {
|
||||
let b = browserify();
|
||||
for (let lib of external_js) {
|
||||
b.require(lib);
|
||||
}
|
||||
if (!process.argv.includes('--no-transpile')) {
|
||||
b.add(require.resolve('babel-polyfill'));
|
||||
}
|
||||
const file = './public/js/vendor.min.js';
|
||||
writeJsBundle(b, file, true, () => {
|
||||
if (process.argv.includes('--gzip')) {
|
||||
gzipFile(file);
|
||||
}
|
||||
console.info('Bundled vendor JS');
|
||||
});
|
||||
bundleVendorJs(true);
|
||||
}
|
||||
|
||||
if (!process.argv.includes('--no-app-js')) {
|
||||
let b = browserify({debug: process.argv.includes('--debug')});
|
||||
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');
|
||||
const file = './public/js/app.min.js';
|
||||
writeJsBundle(b, file, compress, () => {
|
||||
if (process.argv.includes('--gzip')) {
|
||||
gzipFile(file);
|
||||
}
|
||||
console.info('Bundled app JS');
|
||||
});
|
||||
bundleAppJs(b, compress, () => { });
|
||||
}
|
||||
}
|
||||
|
||||
const environment = process.argv.includes('--watch') ? "development" : "production";
|
||||
|
||||
function bundleConfig() {
|
||||
function getVersion() {
|
||||
let build_info = process.env.BUILD_INFO;
|
||||
|
@ -214,9 +231,10 @@ function bundleConfig() {
|
|||
}
|
||||
const config = {
|
||||
meta: {
|
||||
version: getVersion(),
|
||||
buildDate: new Date().toUTCString()
|
||||
}
|
||||
version: getVersion(),
|
||||
buildDate: new Date().toUTCString()
|
||||
},
|
||||
environment: environment
|
||||
};
|
||||
|
||||
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
|
||||
|
@ -225,7 +243,6 @@ 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')
|
||||
|
@ -254,31 +271,31 @@ function bundleWebAppFiles() {
|
|||
|
||||
Promise.all(webapp_icons.map(icon => {
|
||||
return Jimp.read('./img/app.png')
|
||||
.then(file => {
|
||||
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||
.write(path.join('./public/img/', icon.name));
|
||||
});
|
||||
.then(file => {
|
||||
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||
.write(path.join('./public/img/', icon.name));
|
||||
});
|
||||
}))
|
||||
.then(() => {
|
||||
console.info('Generated webapp icons');
|
||||
});
|
||||
.then(() => {
|
||||
console.info('Generated webapp icons');
|
||||
});
|
||||
|
||||
Promise.all(webapp_splash_screens.map(dim => {
|
||||
return Jimp.read('./img/splash.png')
|
||||
.then(file => {
|
||||
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||
.background(0xFFFFFFFF)
|
||||
.contain(dim.w, dim.center,
|
||||
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||
.contain(dim.w, dim.h,
|
||||
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||
.write(path.join('./public/img/',
|
||||
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
|
||||
});
|
||||
.then(file => {
|
||||
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
|
||||
.background(0xFFFFFFFF)
|
||||
.contain(dim.w, dim.center,
|
||||
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||
.contain(dim.w, dim.h,
|
||||
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
|
||||
.write(path.join('./public/img/',
|
||||
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
|
||||
});
|
||||
}))
|
||||
.then(() => {
|
||||
console.info('Generated splash screens');
|
||||
});
|
||||
.then(() => {
|
||||
console.info('Generated splash screens');
|
||||
});
|
||||
}
|
||||
|
||||
function makeOutputDirs() {
|
||||
|
@ -297,18 +314,111 @@ 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();
|
||||
bundleBinaryAssets();
|
||||
bundleWebAppFiles();
|
||||
if (!process.argv.includes('--no-html')) {
|
||||
bundleHtml();
|
||||
}
|
||||
if (!process.argv.includes('--no-css')) {
|
||||
bundleCss();
|
||||
}
|
||||
if (!process.argv.includes('--no-js')) {
|
||||
bundleJs();
|
||||
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();
|
||||
}
|
||||
if (!process.argv.includes('--no-css')) {
|
||||
bundleCss();
|
||||
}
|
||||
if (!process.argv.includes('--no-js')) {
|
||||
bundleJs();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
$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
|
||||
|
@ -21,6 +25,7 @@ $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
|
||||
|
@ -35,7 +40,6 @@ $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
|
||||
|
@ -57,3 +61,4 @@ $safety-sketchy = #F3D75F
|
|||
$safety-unsafe = #F3985F
|
||||
$scrollbar-thumb-color = $main-color
|
||||
$scrollbar-bg-color = $input-enabled-background-color
|
||||
$transparency-grid-square-color = #F0F0F0
|
|
@ -1,5 +1,7 @@
|
|||
@import colors
|
||||
$comment-header-background-color = $top-navigation-color
|
||||
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
|
||||
|
||||
$comment-border-color = #DDD
|
||||
|
||||
.comment-container
|
||||
|
@ -81,7 +83,7 @@ $comment-border-color = #DDD
|
|||
|
||||
.edit, .delete, .score-container a, .nickname a
|
||||
&:not(.inactive)
|
||||
color: mix($main-color, $inactive-tab-text-color)
|
||||
color: mix($main-color, $inactive-link-color)
|
||||
|
||||
i
|
||||
margin-right: 0.3em
|
||||
|
@ -112,8 +114,23 @@ $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
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@import colors
|
||||
$comment-border-color = $top-navigation-color
|
||||
$comment-border-color-darktheme = $top-navigation-color-darktheme
|
||||
|
||||
.global-comment-list
|
||||
text-align: left
|
||||
|
@ -46,3 +47,8 @@ $comment-border-color = $top-navigation-color
|
|||
|
||||
.comments-container
|
||||
width: 100%
|
||||
|
||||
.darktheme .global-comment-list
|
||||
&>ul
|
||||
&>li
|
||||
border-top: 3px solid $comment-border-color-darktheme
|
||||
|
|
|
@ -26,6 +26,10 @@ 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
|
||||
|
@ -167,6 +171,16 @@ 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%)
|
||||
|
||||
|
||||
/*
|
||||
|
@ -204,6 +218,21 @@ 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,
|
||||
|
@ -242,8 +271,9 @@ 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
|
||||
|
@ -310,6 +340,10 @@ 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
|
||||
|
@ -319,8 +353,6 @@ input[type=file]:focus+.file-dropper,
|
|||
.file-dropper.active
|
||||
border-color: $main-color
|
||||
|
||||
|
||||
|
||||
.autocomplete
|
||||
position: absolute
|
||||
z-index: 10
|
||||
|
@ -345,6 +377,10 @@ 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
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
@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';
|
||||
|
@ -28,6 +33,10 @@ 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
|
||||
|
@ -62,6 +71,11 @@ 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
|
||||
|
@ -102,12 +116,19 @@ 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
|
||||
|
@ -205,6 +226,24 @@ 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
|
||||
|
||||
|
@ -229,6 +268,18 @@ 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
|
||||
|
@ -239,9 +290,14 @@ a .access-key
|
|||
width: 20px
|
||||
height: 20px
|
||||
&.empty
|
||||
background-image: url('../img/transparency_grid.png')
|
||||
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-repeat: repeat
|
||||
background-size: initial
|
||||
background-size: 20px 20px
|
||||
img
|
||||
opacity: 0
|
||||
width: 100%
|
||||
|
|
|
@ -22,3 +22,11 @@
|
|||
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
|
||||
|
|
|
@ -22,6 +22,14 @@
|
|||
z-index: 1
|
||||
span
|
||||
position: relative
|
||||
background: white
|
||||
background: $window-color
|
||||
padding: 0 1em
|
||||
z-index: 2
|
||||
|
||||
.darktheme .pager
|
||||
.page
|
||||
.page-header
|
||||
&:before
|
||||
background: $top-navigation-color-darktheme
|
||||
span
|
||||
background: $window-color-darktheme
|
||||
|
|
29
client/css/pool-categories-view.styl
Normal file
29
client/css/pool-categories-view.styl
Normal file
|
@ -0,0 +1,29 @@
|
|||
@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
|
58
client/css/pool-input-control.styl
Normal file
58
client/css/pool-input-control.styl
Normal file
|
@ -0,0 +1,58 @@
|
|||
@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
|
63
client/css/pool-list-view.styl
Normal file
63
client/css/pool-list-view.styl
Normal file
|
@ -0,0 +1,63 @@
|
|||
@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
|
33
client/css/pool-view.styl
Normal file
33
client/css/pool-view.styl
Normal file
|
@ -0,0 +1,33 @@
|
|||
#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
|
|
@ -1,6 +1,14 @@
|
|||
@import colors
|
||||
|
||||
.post-container
|
||||
.post-content.transparency-grid img
|
||||
background: url('../img/transparency_grid.png')
|
||||
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
|
||||
|
||||
text-align: center
|
||||
.post-content
|
||||
|
@ -16,3 +24,6 @@
|
|||
bottom: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
img
|
||||
image-orientation: from-image
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
height: 1em
|
||||
text-align: center
|
||||
line-height: 1em
|
||||
font-size: 1.6em
|
||||
font-size: 2.2em
|
||||
&.tagged
|
||||
background: rgba(0, 230, 0, 0.7)
|
||||
&:after
|
||||
|
@ -114,12 +114,36 @@
|
|||
&[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
|
||||
|
@ -134,6 +158,22 @@
|
|||
.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
|
||||
|
@ -147,6 +187,9 @@
|
|||
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
|
||||
|
@ -182,7 +225,7 @@
|
|||
.hint
|
||||
display: none
|
||||
input[name=tag]
|
||||
width: 12em
|
||||
width: 24em
|
||||
@media (max-width: 1000px)
|
||||
display: block
|
||||
width: 100%
|
||||
|
@ -198,7 +241,19 @@
|
|||
.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
|
||||
|
|
|
@ -7,47 +7,61 @@
|
|||
|
||||
>.sidebar
|
||||
margin-right: 1em
|
||||
min-width: 20em
|
||||
max-width: 20em
|
||||
min-width: 21em
|
||||
max-width: 21em
|
||||
line-height: 160%
|
||||
|
||||
a:active
|
||||
border: 0
|
||||
outline: 0
|
||||
|
||||
nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear
|
||||
&:not(.inactive):hover
|
||||
background: lighten($main-color, 90%)
|
||||
i
|
||||
font-size: 140%
|
||||
>.sidebar>nav.buttons, >.content nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
@media (max-width: 800px)
|
||||
margin-top: 2em
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear, box-shadow 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
|
||||
|
||||
>.content
|
||||
width: 100%
|
||||
|
||||
.post-container
|
||||
margin-bottom: 2em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
.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%
|
||||
|
@ -105,7 +119,6 @@
|
|||
h1
|
||||
margin-bottom: 0.5em
|
||||
.thumbnail
|
||||
background-position: 50% 30%
|
||||
width: 4em
|
||||
height: 3em
|
||||
li
|
||||
|
@ -141,6 +154,12 @@
|
|||
margin: 0
|
||||
padding: 0
|
||||
|
||||
.post-source
|
||||
textarea
|
||||
white-space: pre
|
||||
overflow-wrap: normal
|
||||
overflow-x: scroll
|
||||
|
||||
form
|
||||
width: auto
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@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
|
||||
|
||||
|
@ -12,8 +13,12 @@ $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
|
||||
|
||||
|
@ -38,6 +43,12 @@ $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
|
||||
|
||||
|
@ -51,6 +62,14 @@ $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
|
||||
|
@ -149,3 +168,15 @@ $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
|
||||
|
|
|
@ -31,16 +31,34 @@ $snapshot-merged-background-color = #FEC
|
|||
div.operation-created
|
||||
background: $snapshot-created-background-color
|
||||
&+.details
|
||||
background: lighten($snapshot-created-background-color, 50%)
|
||||
background: alpha(@background, 50%)
|
||||
div.operation-modified
|
||||
background: $snapshot-modified-background-color
|
||||
&+.details
|
||||
background: lighten($snapshot-modified-background-color, 50%)
|
||||
background: alpha(@background, 50%)
|
||||
div.operation-deleted
|
||||
background: $snapshot-deleted-background-color
|
||||
&+.details
|
||||
background: lighten($snapshot-deleted-background-color, 50%)
|
||||
background: alpha(@background, 50%)
|
||||
div.operation-merged
|
||||
background: $snapshot-merged-background-color
|
||||
&+.details
|
||||
background: lighten($snapshot-merged-background-color, 50%)
|
||||
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%)
|
||||
|
|
|
@ -27,4 +27,3 @@
|
|||
display: none
|
||||
form
|
||||
width: auto
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ div.tag-input
|
|||
|
||||
.wrapper
|
||||
margin-left: 0.5em
|
||||
background: $tag-suggestions-background-color
|
||||
background: $window-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($tag-suggestions-background-color, 0)
|
||||
border-bottom: 0.5em solid alpha($window-color, 0)
|
||||
li
|
||||
margin: 0
|
||||
font-size: 90%
|
||||
|
@ -86,6 +86,12 @@ 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
|
||||
|
@ -103,18 +109,30 @@ 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
|
||||
|
@ -125,3 +143,19 @@ 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
|
||||
|
|
|
@ -40,6 +40,13 @@
|
|||
.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
|
||||
|
@ -54,3 +61,7 @@
|
|||
vertical-align: middle
|
||||
font-size: 0.95em
|
||||
color: $inactive-link-color
|
||||
|
||||
.darktheme .tag-list-header
|
||||
.append
|
||||
color: $inactive-link-color-darktheme
|
||||
|
|
|
@ -21,10 +21,15 @@
|
|||
.details
|
||||
font-size: 90%
|
||||
line-height: 130%
|
||||
.image
|
||||
margin: 0.25em 0.6em 0.25em 0
|
||||
.thumbnail
|
||||
width: 3em
|
||||
height: 3em
|
||||
margin: 0.25em 0.6em 0 0
|
||||
|
||||
.darktheme .user-list
|
||||
ul li
|
||||
background: $top-navigation-color-darktheme
|
||||
|
||||
.user-list-header
|
||||
label
|
||||
|
@ -40,3 +45,7 @@
|
|||
vertical-align: middle
|
||||
font-size: 0.95em
|
||||
color: $inactive-link-color
|
||||
|
||||
.darktheme .user-list-header
|
||||
.append
|
||||
color: $inactive-link-color-darktheme
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
# Integrate environment variables
|
||||
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||
/etc/nginx/nginx.conf
|
||||
/etc/nginx/nginx.conf
|
||||
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
|
||||
/var/www/index.htm \
|
||||
/var/www/manifest.json
|
||||
/var/www/index.htm \
|
||||
/var/www/manifest.json
|
||||
|
||||
# Start server
|
||||
exec nginx
|
||||
exec nginx
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
#!/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 .
|
|
@ -1,19 +0,0 @@
|
|||
#!/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
|
|
@ -4,6 +4,7 @@
|
|||
--><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>
|
||||
|
||||
|
|
97
client/html/help_search_pools.tpl
Normal file
97
client/html/help_search_pools.tpl
Normal file
|
@ -0,0 +1,97 @@
|
|||
<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>
|
|
@ -20,15 +20,15 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td><code>uploader</code></td>
|
||||
<td>uploaded by given use (accepts wildcards)r</td>
|
||||
<td>uploaded by given user (accepts wildcards)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>upload</code></td>
|
||||
<td>alias of <code>upload</code></td>
|
||||
<td>alias of <code>uploader</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>submit</code></td>
|
||||
<td>alias of <code>upload</code></td>
|
||||
<td>alias of <code>uploader</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>comment</code></td>
|
||||
|
@ -42,6 +42,10 @@
|
|||
<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>
|
||||
|
@ -79,9 +83,17 @@
|
|||
<td>having given flag. <code><value></code> can be either <code>loop</code> or <code>sound</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>content-checksum</code></td>
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file-size</code></td>
|
||||
<td>having given file size (in bytes)</td>
|
||||
|
|
|
@ -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> 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><%- ctx.isDevelopmentMode ? " (DEV MODE)" : "" %> 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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<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'/>
|
||||
|
|
18
client/html/pool.tpl
Normal file
18
client/html/pool.tpl
Normal file
|
@ -0,0 +1,18 @@
|
|||
<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…</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>
|
30
client/html/pool_categories.tpl
Normal file
30
client/html/pool_categories.tpl
Normal file
|
@ -0,0 +1,30 @@
|
|||
<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>
|
43
client/html/pool_category_row.tpl
Normal file
43
client/html/pool_category_row.tpl
Normal file
|
@ -0,0 +1,43 @@
|
|||
<% 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>
|
42
client/html/pool_create.tpl
Normal file
42
client/html/pool_create.tpl
Normal file
|
@ -0,0 +1,42 @@
|
|||
<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>
|
21
client/html/pool_delete.tpl
Normal file
21
client/html/pool_delete.tpl
Normal file
|
@ -0,0 +1,21 @@
|
|||
<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>
|
50
client/html/pool_edit.tpl
Normal file
50
client/html/pool_edit.tpl
Normal file
|
@ -0,0 +1,50 @@
|
|||
<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>
|
7
client/html/pool_input.tpl
Normal file
7
client/html/pool_input.tpl
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div class='pool-input'>
|
||||
<div class='main-control'>
|
||||
<input type='text' placeholder='type to add…'/>
|
||||
</div>
|
||||
|
||||
<ul class='compact-pools'></ul>
|
||||
</div>
|
22
client/html/pool_merge.tpl
Normal file
22
client/html/pool_merge.tpl
Normal file
|
@ -0,0 +1,22 @@
|
|||
<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>
|
23
client/html/pool_summary.tpl
Normal file
23
client/html/pool_summary.tpl
Normal file
|
@ -0,0 +1,23 @@
|
|||
<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>
|
22
client/html/pools_header.tpl
Normal file
22
client/html/pools_header.tpl
Normal file
|
@ -0,0 +1,22 @@
|
|||
<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>
|
48
client/html/pools_page.tpl
Normal file
48
client/html/pools_page.tpl
Normal file
|
@ -0,0 +1,48 @@
|
|||
<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>
|
|
@ -73,6 +73,12 @@
|
|||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPoolPosts) { %>
|
||||
<section class='pools'>
|
||||
<%= ctx.makeTextInput({}) %>
|
||||
</section>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canEditPostNotes) { %>
|
||||
<section class='notes'>
|
||||
<a href class='add'>Add a note</a>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<span class='vim-nav-hint'>Next post ></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) %>'>
|
||||
|
@ -36,16 +37,13 @@
|
|||
<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 href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</article>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<div class='sidebar-container'></div>
|
||||
|
@ -54,13 +52,16 @@
|
|||
<div class='content'>
|
||||
<div class='post-container'></div>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
<div class='after-mobile-controls'>
|
||||
<div class='description'></div>
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-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>
|
||||
|
|
|
@ -36,8 +36,13 @@
|
|||
'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] +
|
||||
' (' +
|
||||
|
|
|
@ -9,11 +9,16 @@
|
|||
'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><% } %><!--
|
||||
|
@ -52,7 +57,8 @@
|
|||
<section class='search'>
|
||||
Search on
|
||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
||||
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
</section>
|
||||
|
||||
<section class='social'>
|
||||
|
@ -91,12 +97,12 @@
|
|||
--></a><!--
|
||||
--><% } %><!--
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||
--><% } %><!--
|
||||
--><%- ctx.getPrettyTagName(tag.names[0]) %> <!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %><!--
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--></a><!--
|
||||
--><% } %><!--
|
||||
--><% } %> <!--
|
||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||
--></li><!--
|
||||
--><% } %><!--
|
||||
|
|
|
@ -7,12 +7,28 @@
|
|||
|
||||
<span class='skip-duplicates'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Skip duplicates',
|
||||
text: 'Skip duplicate',
|
||||
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>
|
||||
|
||||
|
|
|
@ -61,16 +61,7 @@
|
|||
text: 'Upload anonymously',
|
||||
name: 'anonymous',
|
||||
checked: ctx.uploadable.anonymous,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (['video'].includes(ctx.uploadable.type)) { %>
|
||||
<div class='loop-video'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Loop video',
|
||||
name: 'loop-video',
|
||||
checked: ctx.uploadable.flags.includes('loop'),
|
||||
readonly: ctx.uploadable.forceAnonymous,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
%><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><%
|
||||
|
@ -28,4 +27,11 @@
|
|||
%><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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<div class='post-list'>
|
||||
<% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %>
|
||||
<% if (ctx.response.results.length) { %>
|
||||
<ul>
|
||||
<% for (let post of ctx.response.results) { %>
|
||||
|
@ -50,6 +50,10 @@
|
|||
<% } %>
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
|
||||
<a href class='delete-flipper'>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
|
|
|
@ -22,6 +22,15 @@
|
|||
}) %>
|
||||
</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',
|
||||
|
@ -38,6 +47,15 @@
|
|||
<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',
|
||||
|
@ -66,8 +84,8 @@
|
|||
|
||||
<li>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Display underscores as spaces in tags',
|
||||
name: 'tag-underscores-as-spaces',
|
||||
text: 'Display underscores as spaces',
|
||||
name: '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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class='content-wrapper' id='tag'>
|
||||
<h1><%- ctx.getPrettyTagName(ctx.tag.names[0]) %></h1>
|
||||
<h1><%- ctx.getPrettyName(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><!--
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<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>
|
||||
|
@ -21,7 +22,7 @@
|
|||
|
||||
<div class='messages'></div>
|
||||
|
||||
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
|
||||
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %>
|
||||
<div class='buttons'>
|
||||
<input type='submit' class='save' value='Save changes'>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,13 @@
|
|||
<%- 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}) %>'>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class='tag-delete'>
|
||||
<form>
|
||||
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||
|
||||
<ul class='input'>
|
||||
<li>
|
||||
|
|
|
@ -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.escapeColons(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
@ -3,35 +3,35 @@
|
|||
<table>
|
||||
<thead>
|
||||
<th class='names'>
|
||||
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
|
||||
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.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.query == 'sort:implication-count') { %>
|
||||
<% if (ctx.parameters.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.query == 'sort:suggestion-count') { %>
|
||||
<% if (ctx.parameters.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.query == 'sort:usages') { %>
|
||||
<% if (ctx.parameters.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.query == 'sort:creation-time') { %>
|
||||
<% if (ctx.parameters.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.
Before Width: | Height: | Size: 100 B |
255
client/js/api.js
255
client/js/api.js
|
@ -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,11 +43,12 @@ 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) {
|
||||
|
@ -67,10 +68,9 @@ class Api extends events.EventTarget {
|
|||
|
||||
fetchConfig() {
|
||||
if (remoteConfig === null) {
|
||||
return this.get(uri.formatApiLink('info'))
|
||||
.then(response => {
|
||||
remoteConfig = response.config;
|
||||
});
|
||||
return this.get(uri.formatApiLink("info")).then((response) => {
|
||||
remoteConfig = response.config;
|
||||
});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -84,6 +84,10 @@ class Api extends events.EventTarget {
|
|||
return remoteConfig.tagNameRegex;
|
||||
}
|
||||
|
||||
getPoolNameRegex() {
|
||||
return remoteConfig.poolNameRegex;
|
||||
}
|
||||
|
||||
getPasswordRegex() {
|
||||
return remoteConfig.passwordRegex;
|
||||
}
|
||||
|
@ -111,7 +115,8 @@ class Api extends events.EventTarget {
|
|||
continue;
|
||||
}
|
||||
const rankIndex = this.allRanks.indexOf(
|
||||
remoteConfig.privileges[p]);
|
||||
remoteConfig.privileges[p]
|
||||
);
|
||||
if (minViableRank === null || rankIndex < minViableRank) {
|
||||
minViableRank = rankIndex;
|
||||
}
|
||||
|
@ -119,17 +124,16 @@ 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) {
|
||||
|
@ -137,63 +141,74 @@ 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);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -202,8 +217,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;
|
||||
|
@ -211,22 +226,26 @@ 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() {
|
||||
|
@ -234,17 +253,19 @@ 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;
|
||||
}
|
||||
|
@ -255,8 +276,7 @@ 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];
|
||||
|
@ -281,7 +301,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(() => {
|
||||
|
@ -289,33 +309,40 @@ 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);
|
||||
});
|
||||
|
@ -327,13 +354,17 @@ 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 => {
|
||||
abortFunction = () => {};
|
||||
return resolve(response.token);
|
||||
}, reject);
|
||||
return uploadPromise.then((response) => {
|
||||
abortFunction = () => {};
|
||||
return resolve(response.token);
|
||||
}, reject);
|
||||
});
|
||||
returnedPromise.abort = () => abortFunction();
|
||||
return returnedPromise;
|
||||
|
@ -348,7 +379,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);
|
||||
|
@ -358,7 +389,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());
|
||||
}
|
||||
|
@ -367,9 +398,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);
|
||||
}
|
||||
}
|
||||
|
@ -377,19 +408,29 @@ class Api extends events.EventTarget {
|
|||
try {
|
||||
if (this.userName && this.token) {
|
||||
req.auth = null;
|
||||
req.set('Authorization', 'Token '
|
||||
+ new Buffer(this.userName + ":" + this.token).toString('base64'))
|
||||
// eslint-disable-next-line no-undef
|
||||
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) {
|
||||
|
@ -397,10 +438,11 @@ class Api extends events.EventTarget {
|
|||
}
|
||||
|
||||
abortFunction = () => {
|
||||
req.abort(); // does *NOT* call the callback passed in .end()
|
||||
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) => {
|
||||
|
@ -409,7 +451,8 @@ 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);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
const config = require('./.config.autogen.json');
|
||||
const config = require("./.config.autogen.json");
|
||||
module.exports = config;
|
||||
|
|
|
@ -1,36 +1,40 @@
|
|||
'use strict';
|
||||
"use strict";
|
||||
|
||||
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');
|
||||
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");
|
||||
|
||||
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();
|
||||
}, error => {
|
||||
pools.refreshCategoryColorMap();
|
||||
},
|
||||
(error) => {
|
||||
this._loginView.showError(error.message);
|
||||
this._loginView.enableForm();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,15 +43,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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,49 +1,55 @@
|
|||
'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;
|
||||
},
|
||||
});
|
||||
|
@ -52,25 +58,27 @@ class CommentsController {
|
|||
_evtUpdate(e) {
|
||||
// TODO: disable form
|
||||
e.detail.comment.text = e.detail.text;
|
||||
e.detail.comment.save()
|
||||
.catch(error => {
|
||||
e.detail.target.showError(error.message);
|
||||
// TODO: enable form
|
||||
});
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
'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'),
|
||||
canListSnapshots: api.hasPrivilege("snapshots:list"),
|
||||
canListPosts: api.hasPrivilege("posts:list"),
|
||||
isDevelopmentMode: config.environment == "development",
|
||||
});
|
||||
|
||||
Info.get()
|
||||
.then(info => {
|
||||
Info.get().then(
|
||||
(info) => {
|
||||
this._homeView.setStats({
|
||||
diskUsage: info.diskUsage,
|
||||
postCount: info.postCount,
|
||||
|
@ -31,7 +32,8 @@ class HomeController {
|
|||
featuringTime: info.featuringTime,
|
||||
});
|
||||
},
|
||||
error => this._homeView.showError(error.message));
|
||||
(error) => this._homeView.showError(error.message)
|
||||
);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
|
@ -41,9 +43,9 @@ class HomeController {
|
|||
showError(message) {
|
||||
this._homeView.showError(message);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = router => {
|
||||
module.exports = (router) => {
|
||||
router.enter([], (ctx, next) => {
|
||||
ctx.controller = new HomeController();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
'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) {
|
||||
|
@ -21,15 +22,20 @@ 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();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,26 +44,30 @@ 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(() => {
|
||||
const ctx = router.show(uri.formatClientLink());
|
||||
ctx.controller.showSuccess('New password: ' + password);
|
||||
}, error => {
|
||||
const ctx = router.show(uri.formatClientLink());
|
||||
ctx.controller.showError(error.message);
|
||||
});
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
const ctx = router.show(uri.formatClientLink());
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
69
client/js/controllers/pool_categories_controller.js
Normal file
69
client/js/controllers/pool_categories_controller.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
"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);
|
||||
});
|
||||
};
|
176
client/js/controllers/pool_controller.js
Normal file
176
client/js/controllers/pool_controller.js
Normal file
|
@ -0,0 +1,176 @@
|
|||
"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");
|
||||
});
|
||||
};
|
65
client/js/controllers/pool_create_controller.js
Normal file
65
client/js/controllers/pool_create_controller.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
"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");
|
||||
});
|
||||
};
|
121
client/js/controllers/pool_list_controller.js
Normal file
121
client/js/controllers/pool_list_controller.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
"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);
|
||||
});
|
||||
};
|
|
@ -1,28 +1,33 @@
|
|||
'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 => {
|
||||
this._id = ctx.parameters.id;
|
||||
post.addEventListener('change', e => this._evtSaved(e, section));
|
||||
this._installView(post, section);
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
Post.get(ctx.parameters.id).then(
|
||||
(post) => {
|
||||
this._id = ctx.parameters.id;
|
||||
post.addEventListener("change", (e) =>
|
||||
this._evtSaved(e, section)
|
||||
);
|
||||
this._installView(post, section);
|
||||
},
|
||||
(error) => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
|
@ -33,56 +38,68 @@ 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 => {
|
||||
this._view.selectPost(post);
|
||||
this._view.enableForm();
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
Post.get(e.detail.postId).then(
|
||||
(post) => {
|
||||
this._view.selectPost(post);
|
||||
this._view.enableForm();
|
||||
},
|
||||
(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.');
|
||||
router.replace(
|
||||
uri.formatClientLink(
|
||||
'post', e.detail.targetPost.id, 'merge'),
|
||||
null, false);
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
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) => {
|
||||
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");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,47 +1,68 @@
|
|||
'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) {
|
||||
if (!api.hasPrivilege('posts:list')) {
|
||||
this._pageController = new PageController();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
topNavigation.activate('posts');
|
||||
topNavigation.setTitle('Listing posts');
|
||||
|
||||
this._ctx = ctx;
|
||||
this._pageController = new PageController();
|
||||
|
||||
topNavigation.activate("posts");
|
||||
topNavigation.setTitle("Listing posts");
|
||||
|
||||
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'),
|
||||
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
|
||||
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
|
||||
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -50,34 +71,67 @@ 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));
|
||||
e.detail.post.save().catch((error) => window.alert(error.message));
|
||||
}
|
||||
|
||||
_evtMarkForDeletion(e) {
|
||||
const postId = e.detail;
|
||||
|
||||
// Add or remove post from delete list
|
||||
if (e.detail.delete) {
|
||||
this._postsMarkedForDeletion.push(e.detail.post);
|
||||
} else {
|
||||
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
|
||||
(x) => x.id != e.detail.post.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_evtDeleteSelectedPosts(e) {
|
||||
if (this._postsMarkedForDeletion.length == 0) return;
|
||||
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
|
||||
)
|
||||
) {
|
||||
Promise.all(
|
||||
this._postsMarkedForDeletion.map((post) => post.delete())
|
||||
)
|
||||
.catch((error) => window.alert(error.message))
|
||||
.then(() => {
|
||||
this._postsMarkedForDeletion = [];
|
||||
this._headerView._navigate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_syncPageController() {
|
||||
|
@ -85,37 +139,51 @@ 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'),
|
||||
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"),
|
||||
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("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)
|
||||
);
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
@ -18,77 +18,110 @@ class PostMainController extends BasePostController {
|
|||
|
||||
let parameters = ctx.parameters;
|
||||
Promise.all([
|
||||
Post.get(ctx.parameters.id),
|
||||
PostList.getAround(
|
||||
ctx.parameters.id,
|
||||
parameters ? parameters.query : null),
|
||||
]).then(responses => {
|
||||
const [post, aroundResponse] = responses;
|
||||
Post.get(ctx.parameters.id),
|
||||
PostList.getAround(
|
||||
ctx.parameters.id,
|
||||
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);
|
||||
router.replace(url, ctx.state, false);
|
||||
// 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);
|
||||
router.replace(url, ctx.state, false);
|
||||
}
|
||||
|
||||
this._post = post;
|
||||
this._view = new PostMainView({
|
||||
post: post,
|
||||
editMode: editMode,
|
||||
prevPostId: aroundResponse.prev
|
||||
? aroundResponse.prev.id
|
||||
: null,
|
||||
nextPostId: aroundResponse.next
|
||||
? aroundResponse.next.id
|
||||
: null,
|
||||
canEditPosts: api.hasPrivilege("posts:edit"),
|
||||
canDeletePosts: api.hasPrivilege("posts:delete"),
|
||||
canFeaturePosts: api.hasPrivilege("posts:feature"),
|
||||
canListComments: api.hasPrivilege("comments:list"),
|
||||
canCreateComments: api.hasPrivilege("comments:create"),
|
||||
parameters: parameters,
|
||||
});
|
||||
|
||||
if (this._view.sidebarControl) {
|
||||
this._view.sidebarControl.addEventListener(
|
||||
"favorite",
|
||||
(e) => this._evtFavoritePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener(
|
||||
"unfavorite",
|
||||
(e) => this._evtUnfavoritePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("score", (e) =>
|
||||
this._evtScorePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener(
|
||||
"fitModeChange",
|
||||
(e) => this._evtFitModeChange(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("change", (e) =>
|
||||
this._evtPostChange(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("submit", (e) =>
|
||||
this._evtUpdatePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener(
|
||||
"feature",
|
||||
(e) => this._evtFeaturePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("delete", (e) =>
|
||||
this._evtDeletePost(e)
|
||||
);
|
||||
this._view.sidebarControl.addEventListener("merge", (e) =>
|
||||
this._evtMergePost(e)
|
||||
);
|
||||
}
|
||||
|
||||
if (this._view.commentControl) {
|
||||
this._view.commentControl.addEventListener("change", (e) =>
|
||||
this._evtCommentChange(e)
|
||||
);
|
||||
this._view.commentControl.addEventListener("submit", (e) =>
|
||||
this._evtCreateComment(e)
|
||||
);
|
||||
}
|
||||
|
||||
if (this._view.commentListControl) {
|
||||
this._view.commentListControl.addEventListener(
|
||||
"submit",
|
||||
(e) => this._evtUpdateComment(e)
|
||||
);
|
||||
this._view.commentListControl.addEventListener(
|
||||
"score",
|
||||
(e) => this._evtScoreComment(e)
|
||||
);
|
||||
this._view.commentListControl.addEventListener(
|
||||
"delete",
|
||||
(e) => this._evtDeleteComment(e)
|
||||
);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
|
||||
this._post = post;
|
||||
this._view = new PostMainView({
|
||||
post: post,
|
||||
editMode: editMode,
|
||||
prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
|
||||
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
|
||||
canEditPosts: api.hasPrivilege('posts:edit'),
|
||||
canDeletePosts: api.hasPrivilege('posts:delete'),
|
||||
canFeaturePosts: api.hasPrivilege('posts:feature'),
|
||||
canListComments: api.hasPrivilege('comments:list'),
|
||||
canCreateComments: api.hasPrivilege('comments:create'),
|
||||
parameters: parameters,
|
||||
});
|
||||
|
||||
if (this._view.sidebarControl) {
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'favorite', e => this._evtFavoritePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'unfavorite', e => this._evtUnfavoritePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'score', e => this._evtScorePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'fitModeChange', e => this._evtFitModeChange(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'change', e => this._evtPostChange(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'submit', e => this._evtUpdatePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'feature', e => this._evtFeaturePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'delete', e => this._evtDeletePost(e));
|
||||
this._view.sidebarControl.addEventListener(
|
||||
'merge', e => this._evtMergePost(e));
|
||||
}
|
||||
|
||||
if (this._view.commentControl) {
|
||||
this._view.commentControl.addEventListener(
|
||||
'change', e => this._evtCommentChange(e));
|
||||
this._view.commentControl.addEventListener(
|
||||
'submit', e => this._evtCreateComment(e));
|
||||
}
|
||||
|
||||
if (this._view.commentListControl) {
|
||||
this._view.commentListControl.addEventListener(
|
||||
'submit', e => this._evtUpdateComment(e));
|
||||
this._view.commentListControl.addEventListener(
|
||||
'score', e => this._evtScoreComment(e));
|
||||
this._view.commentListControl.addEventListener(
|
||||
'delete', e => this._evtDeleteComment(e));
|
||||
}
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
_evtFitModeChange(e) {
|
||||
|
@ -100,65 +133,74 @@ 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) {
|
||||
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||
post.safety = e.detail.safety;
|
||||
}
|
||||
if (e.detail.flags !== undefined) {
|
||||
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||
post.flags = e.detail.flags;
|
||||
}
|
||||
if (e.detail.relations !== undefined) {
|
||||
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||
post.relations = e.detail.relations;
|
||||
}
|
||||
if (e.detail.content !== undefined) {
|
||||
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||
post.newContent = e.detail.content;
|
||||
}
|
||||
if (e.detail.thumbnail !== undefined) {
|
||||
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||
post.newThumbnail = e.detail.thumbnail;
|
||||
}
|
||||
if (e.detail.source !== undefined) {
|
||||
if (e.detail.source !== undefined && e.detail.source !== null) {
|
||||
post.source = e.detail.source;
|
||||
}
|
||||
post.save()
|
||||
.then(() => {
|
||||
this._view.sidebarControl.showSuccess('Post saved.');
|
||||
if (e.detail.desc !== undefined && e.detail.desc !== null) {
|
||||
post.desc = e.detail.desc;
|
||||
}
|
||||
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) {
|
||||
|
@ -173,79 +215,82 @@ 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.target.showError(error.message);
|
||||
// TODO: enable form
|
||||
});
|
||||
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) => {
|
||||
// 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) => {
|
||||
// restore parameters from history state
|
||||
if (ctx.state.parameters) {
|
||||
Object.assign(ctx.parameters, ctx.state.parameters);
|
||||
}
|
||||
ctx.controller = new PostMainController(ctx, false);
|
||||
});
|
||||
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) => {
|
||||
// restore parameters from history state
|
||||
if (ctx.state.parameters) {
|
||||
Object.assign(ctx.parameters, ctx.state.parameters);
|
||||
}
|
||||
ctx.controller = new PostMainController(ctx, false);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 of the posts needs your attention; ' +
|
||||
"One or more 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,89 +55,130 @@ class PostUploadController {
|
|||
_evtSubmit(e) {
|
||||
this._view.disableForm();
|
||||
this._view.clearMessages();
|
||||
let anyFailures = false;
|
||||
|
||||
e.detail.uploadables.reduce(
|
||||
(promise, uploadable) =>
|
||||
promise.then(() => this._uploadSinglePost(
|
||||
uploadable, e.detail.skipDuplicates)),
|
||||
Promise.resolve())
|
||||
.then(() => {
|
||||
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()
|
||||
)
|
||||
.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 => {
|
||||
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.showError(
|
||||
error.message, error.uploadable);
|
||||
}
|
||||
} else {
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
const ctx = router.show(uri.formatClientLink("posts"));
|
||||
ctx.controller.showSuccess("Posts uploaded.");
|
||||
},
|
||||
(error) => {
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.enableForm();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_uploadSinglePost(uploadable, skipDuplicates) {
|
||||
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
|
||||
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 => {
|
||||
if (searchResult) {
|
||||
// notify about exact duplicate
|
||||
if (searchResult.exactPost) {
|
||||
if (skipDuplicates) {
|
||||
this._view.removeUploadable(uploadable);
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
let error = new Error('Post already uploaded ' +
|
||||
`(@${searchResult.exactPost.id})`);
|
||||
return reverseSearchPromise
|
||||
.then((searchResult) => {
|
||||
if (searchResult) {
|
||||
// notify about exact duplicate
|
||||
if (searchResult.exactPost) {
|
||||
if (skipDuplicates) {
|
||||
this._view.removeUploadable(uploadable);
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
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
|
||||
) {
|
||||
let error = new Error(
|
||||
`Found ${searchResult.similarPosts.length} similar ` +
|
||||
"posts.\nYou can resume or discard this upload."
|
||||
);
|
||||
error.uploadable = uploadable;
|
||||
error.similarPosts = searchResult.similarPosts;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// notify about similar posts
|
||||
if (searchResult.similarPosts.length) {
|
||||
let error = new Error(
|
||||
`Found ${searchResult.similarPosts.length} similar ` +
|
||||
'posts.\nYou can resume or discard this upload.');
|
||||
error.uploadable = uploadable;
|
||||
error.similarPosts = searchResult.similarPosts;
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// no duplicates, proceed with saving
|
||||
let post = this._uploadableToPost(uploadable);
|
||||
let savePromise = post.save(uploadable.anonymous)
|
||||
.then(() => {
|
||||
// no duplicates, proceed with saving
|
||||
let post = this._uploadableToPost(uploadable);
|
||||
let savePromise = post.save(uploadable.anonymous).then(() => {
|
||||
this._view.removeUploadable(uploadable);
|
||||
return Promise.resolve();
|
||||
});
|
||||
this._lastCancellablePromise = savePromise;
|
||||
return savePromise;
|
||||
}).then(result => {
|
||||
progress.done();
|
||||
return Promise.resolve(result);
|
||||
}, error => {
|
||||
error.uploadable = uploadable;
|
||||
progress.done();
|
||||
return Promise.reject(error);
|
||||
});
|
||||
this._lastCancellablePromise = savePromise;
|
||||
return savePromise;
|
||||
})
|
||||
.then(
|
||||
(result) => {
|
||||
progress.done();
|
||||
return Promise.resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
error.uploadable = uploadable;
|
||||
progress.done();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_uploadableToPost(uploadable) {
|
||||
|
@ -153,13 +194,15 @@ 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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,41 +1,43 @@
|
|||
'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);
|
||||
},
|
||||
|
@ -43,7 +45,8 @@ 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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,57 +1,68 @@
|
|||
'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 => {
|
||||
this._tagCategories = response.results;
|
||||
this._view = new TagCategoriesView({
|
||||
tagCategories: this._tagCategories,
|
||||
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 = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
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"
|
||||
),
|
||||
});
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,63 +1,80 @@
|
|||
'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 => {
|
||||
const [tagCategoriesResponse, tag] = 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));
|
||||
this._name = ctx.parameters.name;
|
||||
tag.addEventListener("change", (e) =>
|
||||
this._evtSaved(e, section)
|
||||
);
|
||||
|
||||
const categories = {};
|
||||
for (let category of tagCategoriesResponse.results) {
|
||||
categories[category.name] = category.name;
|
||||
const categories = {};
|
||||
for (let category of tagCategoriesResponse.results) {
|
||||
categories[category.name] = category.name;
|
||||
}
|
||||
|
||||
this._view = new TagView({
|
||||
tag: tag,
|
||||
section: section,
|
||||
canEditAnything: api.hasPrivilege("tags:edit"),
|
||||
canEditNames: api.hasPrivilege("tags:edit:names"),
|
||||
canEditCategory: api.hasPrivilege("tags:edit:category"),
|
||||
canEditImplications: api.hasPrivilege(
|
||||
"tags:edit:implications"
|
||||
),
|
||||
canEditSuggestions: api.hasPrivilege(
|
||||
"tags:edit:suggestions"
|
||||
),
|
||||
canEditDescription: api.hasPrivilege(
|
||||
"tags:edit:description"
|
||||
),
|
||||
canMerge: api.hasPrivilege("tags:merge"),
|
||||
canDelete: api.hasPrivilege("tags:delete"),
|
||||
categories: categories,
|
||||
escapeTagName: uri.escapeTagName,
|
||||
});
|
||||
|
||||
this._view.addEventListener("change", (e) =>
|
||||
this._evtChange(e)
|
||||
);
|
||||
this._view.addEventListener("submit", (e) =>
|
||||
this._evtUpdate(e)
|
||||
);
|
||||
this._view.addEventListener("merge", (e) => this._evtMerge(e));
|
||||
this._view.addEventListener("delete", (e) =>
|
||||
this._evtDelete(e)
|
||||
);
|
||||
},
|
||||
(error) => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
|
||||
this._view = new TagView({
|
||||
tag: tag,
|
||||
section: section,
|
||||
canEditAnything: api.hasPrivilege('tags:edit'),
|
||||
canEditNames: api.hasPrivilege('tags:edit:names'),
|
||||
canEditCategory: api.hasPrivilege('tags:edit:category'),
|
||||
canEditImplications: api.hasPrivilege('tags:edit:implications'),
|
||||
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
|
||||
canEditDescription: api.hasPrivilege('tags:edit:description'),
|
||||
canMerge: api.hasPrivilege('tags:merge'),
|
||||
canDelete: api.hasPrivilege('tags:delete'),
|
||||
categories: categories,
|
||||
escapeColons: uri.escapeColons,
|
||||
});
|
||||
|
||||
this._view.addEventListener('change', e => this._evtChange(e));
|
||||
this._view.addEventListener('submit', e => this._evtUpdate(e));
|
||||
this._view.addEventListener('merge', e => this._evtMerge(e));
|
||||
this._view.addEventListener('delete', e => this._evtDelete(e));
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
_evtChange(e) {
|
||||
|
@ -68,75 +85,88 @@ 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) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.tag.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.tag.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.tag.description = e.detail.description;
|
||||
}
|
||||
e.detail.tag.save().then(() => {
|
||||
this._view.showSuccess('Tag saved.');
|
||||
this._view.enableForm();
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
e.detail.tag.save().then(
|
||||
() => {
|
||||
this._view.showSuccess("Tag saved.");
|
||||
this._view.enableForm();
|
||||
},
|
||||
(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");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,44 +1,47 @@
|
|||
'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) {
|
||||
if (!api.hasPrivilege('tags:list')) {
|
||||
this._pageController = new PageController();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
topNavigation.activate('tags');
|
||||
topNavigation.setTitle('Listing tags');
|
||||
|
||||
this._ctx = ctx;
|
||||
this._pageController = new PageController();
|
||||
|
||||
topNavigation.activate("tags");
|
||||
topNavigation.setTitle("Listing tags");
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -53,7 +56,8 @@ 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();
|
||||
}
|
||||
|
@ -63,23 +67,29 @@ 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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
'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();
|
||||
});
|
||||
|
@ -28,37 +29,41 @@ 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("users:list")) {
|
||||
topNavigation.hide("users");
|
||||
}
|
||||
if (!api.hasPrivilege("pools:list")) {
|
||||
topNavigation.hide("pools");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,10 +71,11 @@ 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 : ""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,25 @@
|
|||
'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;
|
||||
}
|
||||
|
||||
|
@ -25,100 +27,128 @@ 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 => {
|
||||
const [userTokens, user] = responses;
|
||||
const isLoggedIn = api.isLoggedIn(user);
|
||||
const infix = isLoggedIn ? 'self' : 'any';
|
||||
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";
|
||||
|
||||
this._name = userName;
|
||||
user.addEventListener('change', e => this._evtSaved(e, section));
|
||||
this._name = userName;
|
||||
user.addEventListener("change", (e) =>
|
||||
this._evtSaved(e, section)
|
||||
);
|
||||
|
||||
const myRankIndex = api.user ?
|
||||
api.allRanks.indexOf(api.user.rank) :
|
||||
0;
|
||||
let ranks = {};
|
||||
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
|
||||
if (rankIdentifier === 'anonymous') {
|
||||
continue;
|
||||
const myRankIndex = api.user
|
||||
? api.allRanks.indexOf(api.user.rank)
|
||||
: 0;
|
||||
let ranks = {};
|
||||
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
|
||||
if (rankIdentifier === "anonymous") {
|
||||
continue;
|
||||
}
|
||||
if (rankIdx > myRankIndex) {
|
||||
continue;
|
||||
}
|
||||
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
|
||||
}
|
||||
if (rankIdx > myRankIndex) {
|
||||
continue;
|
||||
|
||||
if (isLoggedIn) {
|
||||
topNavigation.activate("account");
|
||||
} else {
|
||||
topNavigation.activate("users");
|
||||
}
|
||||
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
|
||||
|
||||
this._view = new UserView({
|
||||
user: user,
|
||||
section: section,
|
||||
isLoggedIn: isLoggedIn,
|
||||
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
|
||||
canEditPassword: api.hasPrivilege(
|
||||
`users:edit:${infix}:pass`
|
||||
),
|
||||
canEditEmail: api.hasPrivilege(
|
||||
`users:edit:${infix}:email`
|
||||
),
|
||||
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
|
||||
canEditAvatar: api.hasPrivilege(
|
||||
`users:edit:${infix}:avatar`
|
||||
),
|
||||
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
|
||||
canListTokens: api.hasPrivilege(
|
||||
`userTokens:list:${infix}`
|
||||
),
|
||||
canCreateToken: api.hasPrivilege(
|
||||
`userTokens:create:${infix}`
|
||||
),
|
||||
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
|
||||
canDeleteToken: api.hasPrivilege(
|
||||
`userTokens:delete:${infix}`
|
||||
),
|
||||
canDelete: api.hasPrivilege(`users:delete:${infix}`),
|
||||
ranks: ranks,
|
||||
tokens: userTokens,
|
||||
});
|
||||
this._view.addEventListener("change", (e) =>
|
||||
this._evtChange(e)
|
||||
);
|
||||
this._view.addEventListener("submit", (e) =>
|
||||
this._evtUpdate(e)
|
||||
);
|
||||
this._view.addEventListener("delete", (e) =>
|
||||
this._evtDelete(e)
|
||||
);
|
||||
this._view.addEventListener("create-token", (e) =>
|
||||
this._evtCreateToken(e)
|
||||
);
|
||||
this._view.addEventListener("delete-token", (e) =>
|
||||
this._evtDeleteToken(e)
|
||||
);
|
||||
this._view.addEventListener("update-token", (e) =>
|
||||
this._evtUpdateToken(e)
|
||||
);
|
||||
|
||||
for (let message of this._successMessages) {
|
||||
this.showSuccess(message);
|
||||
}
|
||||
|
||||
for (let message of this._errorMessages) {
|
||||
this.showError(message);
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
|
||||
if (isLoggedIn) {
|
||||
topNavigation.activate('account');
|
||||
} else {
|
||||
topNavigation.activate('users');
|
||||
}
|
||||
|
||||
this._view = new UserView({
|
||||
user: user,
|
||||
section: section,
|
||||
isLoggedIn: isLoggedIn,
|
||||
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
|
||||
canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`),
|
||||
canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`),
|
||||
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
|
||||
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
|
||||
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
|
||||
canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
|
||||
canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
|
||||
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
|
||||
canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`),
|
||||
canDelete: api.hasPrivilege(`users:delete:${infix}`),
|
||||
ranks: ranks,
|
||||
tokens: userTokens,
|
||||
});
|
||||
this._view.addEventListener('change', e => this._evtChange(e));
|
||||
this._view.addEventListener('submit', e => this._evtUpdate(e));
|
||||
this._view.addEventListener('delete', e => this._evtDelete(e));
|
||||
this._view.addEventListener('create-token', e => this._evtCreateToken(e));
|
||||
this._view.addEventListener('delete-token', e => this._evtDeleteToken(e));
|
||||
this._view.addEventListener('update-token', e => this._evtUpdateToken(e));
|
||||
|
||||
for (let message of this._successMessages) {
|
||||
this.showSuccess(message);
|
||||
}
|
||||
|
||||
for (let message of this._errorMessages) {
|
||||
this.showError(message);
|
||||
}
|
||||
|
||||
}, error => {
|
||||
this._view = new EmptyView();
|
||||
this._view.showError(error.message);
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
showSuccess(message) {
|
||||
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);
|
||||
}
|
||||
|
@ -132,8 +162,10 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,95 +173,128 @@ 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) {
|
||||
if (e.detail.name !== undefined && e.detail.name !== null) {
|
||||
e.detail.user.name = e.detail.name;
|
||||
}
|
||||
if (e.detail.email !== undefined) {
|
||||
if (e.detail.email !== undefined && e.detail.email !== null) {
|
||||
e.detail.user.email = e.detail.email;
|
||||
}
|
||||
if (e.detail.rank !== undefined) {
|
||||
if (e.detail.rank !== undefined && e.detail.rank !== null) {
|
||||
e.detail.user.rank = e.detail.rank;
|
||||
}
|
||||
|
||||
if (e.detail.password !== undefined) {
|
||||
if (e.detail.password !== undefined && e.detail.password !== null) {
|
||||
e.detail.user.password = e.detail.password;
|
||||
}
|
||||
|
||||
if (e.detail.avatarStyle !== undefined) {
|
||||
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
|
||||
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.name || api.userName,
|
||||
e.detail.password || api.userPassword,
|
||||
false) :
|
||||
Promise.resolve();
|
||||
}).then(() => {
|
||||
this._view.showSuccess('Settings updated.');
|
||||
this._view.enableForm();
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
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.");
|
||||
this._view.enableForm();
|
||||
},
|
||||
(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();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,31 +302,42 @@ class UserController {
|
|||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
|
||||
if (e.detail.note !== undefined) {
|
||||
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||
e.detail.userToken.note = e.detail.note;
|
||||
}
|
||||
|
||||
e.detail.userToken.save(e.detail.user.name).then(response => {
|
||||
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
|
||||
ctx.controller.showSuccess('Token ' + response.token + ' updated.');
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
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");
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,35 +1,37 @@
|
|||
'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) {
|
||||
if (!api.hasPrivilege('users:list')) {
|
||||
this._pageController = new PageController();
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -40,7 +42,8 @@ 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();
|
||||
}
|
||||
|
@ -50,17 +53,22 @@ 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);
|
||||
},
|
||||
|
@ -68,8 +76,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);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,30 +30,35 @@ class UserRegistrationController {
|
|||
user.email = e.detail.email;
|
||||
user.password = e.detail.password;
|
||||
const isLoggedIn = api.isLoggedIn();
|
||||
user.save().then(() => {
|
||||
if (isLoggedIn) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
api.forget();
|
||||
return api.login(e.detail.name, e.detail.password, false);
|
||||
}
|
||||
}).then(() => {
|
||||
if (isLoggedIn) {
|
||||
const ctx = router.show(uri.formatClientLink('users'));
|
||||
ctx.controller.showSuccess('User added!');
|
||||
} else {
|
||||
const ctx = router.show(uri.formatClientLink());
|
||||
ctx.controller.showSuccess('Welcome aboard!');
|
||||
}
|
||||
}, error => {
|
||||
this._view.showError(error.message);
|
||||
this._view.enableForm();
|
||||
});
|
||||
user.save()
|
||||
.then(() => {
|
||||
if (isLoggedIn) {
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
api.forget();
|
||||
return api.login(e.detail.name, e.detail.password, false);
|
||||
}
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
if (isLoggedIn) {
|
||||
const ctx = router.show(uri.formatClientLink("users"));
|
||||
ctx.controller.showSuccess("User added!");
|
||||
} else {
|
||||
const ctx = router.show(uri.formatClientLink());
|
||||
ctx.controller.showSuccess("Welcome aboard!");
|
||||
}
|
||||
},
|
||||
(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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,18 +27,22 @@ class AutoCompleteControl {
|
|||
constructor(sourceInputNode, options) {
|
||||
this._sourceInputNode = sourceInputNode;
|
||||
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+/, '');
|
||||
Object.assign(
|
||||
this._options,
|
||||
{
|
||||
verticalShift: 2,
|
||||
maxResults: 15,
|
||||
getTextToFind: () => {
|
||||
const value = sourceInputNode.value;
|
||||
const start = _getSelectionStart(sourceInputNode);
|
||||
return value.substring(0, start).replace(/.*\s+/, "");
|
||||
},
|
||||
confirm: null,
|
||||
delete: null,
|
||||
getMatches: null,
|
||||
},
|
||||
confirm: null,
|
||||
delete: null,
|
||||
getMatches: null,
|
||||
}, options);
|
||||
options
|
||||
);
|
||||
|
||||
this._showTimeout = null;
|
||||
this._results = [];
|
||||
|
@ -49,22 +53,25 @@ 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 index = middle.lastIndexOf(' ');
|
||||
const spaceIndex = middle.lastIndexOf(" ");
|
||||
const commaIndex = middle.lastIndexOf(",");
|
||||
const index = spaceIndex < commaIndex ? commaIndex : spaceIndex;
|
||||
const delimiter = spaceIndex < commaIndex ? "" : " ";
|
||||
if (index !== -1) {
|
||||
prefix = this._sourceInputNode.value.substring(0, index + 1);
|
||||
middle = this._sourceInputNode.value.substring(index + 1);
|
||||
}
|
||||
this._sourceInputNode.value = (
|
||||
prefix + result.toString() + ' ' + suffix.trimLeft());
|
||||
this._sourceInputNode.value =
|
||||
prefix + result.toString() + delimiter + suffix.trimLeft();
|
||||
if (!addSpace) {
|
||||
this._sourceInputNode.value = this._sourceInputNode.value.trim();
|
||||
}
|
||||
|
@ -86,7 +93,7 @@ class AutoCompleteControl {
|
|||
}
|
||||
|
||||
_show() {
|
||||
this._suggestionDiv.style.display = 'block';
|
||||
this._suggestionDiv.style.display = "block";
|
||||
this._isVisible = true;
|
||||
}
|
||||
|
||||
|
@ -101,27 +108,32 @@ 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() {
|
||||
|
@ -137,13 +149,21 @@ 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());
|
||||
|
@ -164,14 +184,17 @@ 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() {
|
||||
|
@ -182,9 +205,11 @@ 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() {
|
||||
|
@ -192,15 +217,18 @@ 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);
|
||||
|
@ -223,34 +251,30 @@ 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 => {
|
||||
e.preventDefault();
|
||||
this._activeResult = resultIndexWorkaround;
|
||||
this._refreshActiveResult();
|
||||
});
|
||||
link.addEventListener(
|
||||
'mousedown',
|
||||
e => {
|
||||
e.preventDefault();
|
||||
this._activeResult = resultIndexWorkaround;
|
||||
this._confirm(this._getActiveSuggestion());
|
||||
this.hide();
|
||||
});
|
||||
link.setAttribute("href", "");
|
||||
link.setAttribute("data-key", resultItem.value);
|
||||
link.addEventListener("mouseenter", (e) => {
|
||||
e.preventDefault();
|
||||
this._activeResult = resultIndexWorkaround;
|
||||
this._refreshActiveResult();
|
||||
});
|
||||
link.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
this._activeResult = resultIndexWorkaround;
|
||||
this._confirm(this._getActiveSuggestion());
|
||||
this.hide();
|
||||
});
|
||||
listItem.appendChild(link);
|
||||
this._suggestionList.appendChild(listItem);
|
||||
}
|
||||
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();
|
||||
|
@ -264,38 +288,44 @@ 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;
|
||||
|
|
|
@ -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,104 +16,111 @@ 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({
|
||||
comment: comment,
|
||||
user: comment ? comment.user : api.user,
|
||||
canViewUsers: api.hasPrivilege('users:view'),
|
||||
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
|
||||
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
|
||||
onlyEditing: onlyEditing,
|
||||
}));
|
||||
const infix = isLoggedIn ? "own" : "any";
|
||||
views.replaceContent(
|
||||
this._hostNode,
|
||||
template({
|
||||
comment: comment,
|
||||
user: comment ? comment.user : api.user,
|
||||
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() {
|
||||
|
@ -122,32 +129,35 @@ 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();
|
||||
|
@ -173,27 +183,31 @@ 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', {
|
||||
detail: {
|
||||
comment: this._comment,
|
||||
score: this._comment.ownScore === score ? 0 : 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', {
|
||||
detail: {
|
||||
comment: this._comment,
|
||||
},
|
||||
}));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("delete", {
|
||||
detail: {
|
||||
comment: this._comment,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_evtChange(e) {
|
||||
|
@ -206,26 +220,24 @@ class CommentControl extends events.EventTarget {
|
|||
|
||||
_evtPreviewEditingClick(e) {
|
||||
e.preventDefault();
|
||||
this._contentNode.innerHTML =
|
||||
misc.formatMarkdown(this._textareaNode.value);
|
||||
this._selectTab('edit');
|
||||
this._selectTab('preview');
|
||||
}
|
||||
|
||||
_evtEditClick(e) {
|
||||
e.preventDefault();
|
||||
this.enterEditMode();
|
||||
this._contentNode.innerHTML = misc.formatMarkdown(
|
||||
this._textareaNode.value
|
||||
);
|
||||
this._selectTab("edit");
|
||||
this._selectTab("preview");
|
||||
}
|
||||
|
||||
_evtSaveChangesClick(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(new CustomEvent('submit', {
|
||||
detail: {
|
||||
target: this,
|
||||
comment: this._comment,
|
||||
text: this._textareaNode.value,
|
||||
},
|
||||
}));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("submit", {
|
||||
detail: {
|
||||
target: this,
|
||||
comment: this._comment,
|
||||
text: this._textareaNode.value,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_evtCancelEditingClick(e) {
|
||||
|
@ -239,27 +251,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;
|
||||
|
|
|
@ -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,16 +28,19 @@ 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);
|
||||
}
|
||||
|
@ -54,6 +57,6 @@ class CommentListControl extends events.EventTarget {
|
|||
_evtRemove(e) {
|
||||
this._uninstallCommentNode(e.detail.comment);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = CommentListControl;
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
'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 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)
|
||||
);
|
||||
|
||||
nodes[0].parentNode.insertBefore(expanderNode, nodes[0]);
|
||||
|
||||
|
@ -32,28 +34,29 @@ 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 {};
|
||||
}
|
||||
|
@ -62,12 +65,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();
|
||||
}
|
||||
|
|
|
@ -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,37 +17,53 @@ 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("keydown", (e) =>
|
||||
this._evtUrlInputKeyDown(e)
|
||||
);
|
||||
this._urlInputNode.addEventListener("paste", (e) => {
|
||||
// document.onpaste is used on the post-upload page.
|
||||
// And this event is used on the post edit page.
|
||||
if (document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
});
|
||||
}
|
||||
if (this._urlConfirmButtonNode) {
|
||||
this._urlConfirmButtonNode.addEventListener(
|
||||
'click', e => this._evtUrlConfirmButtonClick(e));
|
||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
||||
this._evtUrlConfirmButtonClick(e)
|
||||
);
|
||||
}
|
||||
|
||||
document.onpaste = (e) => {
|
||||
if (!document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
}
|
||||
|
||||
this._originalHtml = this._dropperNode.innerHTML;
|
||||
|
@ -56,24 +72,27 @@ 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) {
|
||||
|
@ -84,18 +103,20 @@ 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');
|
||||
counter++;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,31 +130,42 @@ 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 = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
59
client/js/controls/pool_auto_complete_control.js
Normal file
59
client/js/controls/pool_auto_complete_control.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
"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;
|
195
client/js/controls/pool_input_control.js
Normal file
195
client/js/controls/pool_input_control.js
Normal file
|
@ -0,0 +1,195 @@
|
|||
"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;
|
|
@ -1,36 +1,38 @@
|
|||
'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,
|
||||
}[fitMode] || this.fitBoth;
|
||||
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() {
|
||||
|
@ -92,22 +94,48 @@ 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() {
|
||||
|
@ -116,7 +144,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);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue