Compare commits

..

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

466 changed files with 13539 additions and 39029 deletions

5
.gitattributes vendored
View file

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

View file

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

View file

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

14
.gitignore vendored
View file

@ -1,18 +1,4 @@
# User-specific configuration
config.yaml
.env
# Client Development Artifacts
*/*_modules/
client/public
# Server Development Artifacts
.coverage
.cache
server/**/lib/
server/**/bin/
server/**/pyvenv.cfg
__pycache__/
data/
sql/

View file

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

File diff suppressed because it is too large Load diff

179
INSTALL.md Normal file
View file

@ -0,0 +1,179 @@
This guide assumes Arch Linux. Although exact instructions for other
distributions are different, the steps stay roughly the same.
### Installing hard dependencies
```console
user@host:~$ sudo pacman -S postgresql
user@host:~$ sudo pacman -S python
user@host:~$ sudo pacman -S python-pip
user@host:~$ sudo pacman -S ffmpeg
user@host:~$ sudo pacman -S npm
user@host:~$ sudo pip install virtualenv
user@host:~$ python --version
Python 3.5.1
```
The reason `ffmpeg` is used over, say, `ImageMagick` or even `PIL` is because of
Flash and video posts.
### Setting up a database
First, basic `postgres` configuration:
```console
user@host:~$ sudo -i -u postgres initdb --locale en_US.UTF-8 -E UTF8 -D /var/lib/postgres/data
user@host:~$ sudo systemctl start postgresql
user@host:~$ sudo systemctl enable postgresql
```
Then creating a database:
```console
user@host:~$ sudo -i -u postgres createuser --interactive
Enter name of role to add: szuru
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
user@host:~$ sudo -i -u postgres createdb szuru
user@host:~$ sudo -i -u postgres psql -c "ALTER USER szuru PASSWORD 'dog';"
```
### Preparing environment
Getting `szurubooru`:
```console
user@host:~$ git clone https://github.com/rr-/szurubooru.git szuru
user@host:~$ cd szuru
```
Installing frontend dependencies:
```console
user@host:szuru$ cd client
user@host:szuru/client$ npm install
```
`npm` sandboxes dependencies by default, i.e. installs them to
`./node_modules`. This is good, because it avoids polluting the system with the
project's dependencies. To make Python work the same way, we'll use
`virtualenv`. Installing backend dependencies with `virtualenv` looks like
this:
```console
user@host:szuru/client$ cd ../server
user@host:szuru/server$ virtualenv python_modules # consistent with node_modules
user@host:szuru/server$ source python_modules/bin/activate # enters the sandbox
(python_modules) user@host:szuru/server$ pip install -r requirements.txt # installs the dependencies
```
### Preparing `szurubooru` for first run
1. Configure things:
```console
user@host:szuru$ cp config.yaml.dist config.yaml
user@host:szuru$ vim config.yaml
```
Pay extra attention to these fields:
- base URL,
- API URL,
- data directory,
- data URL,
- database,
- the `smtp` section.
2. Compile the frontend:
```console
user@host:szuru$ cd client
user@host:szuru/client$ npm run build
```
3. Upgrade the database:
```console
user@host:szuru/client$ cd ../server
user@host:szuru/server$ source python_modules/bin/activate
(python_modules) user@host:szuru/server$ alembic upgrade head
```
`alembic` should have been installed during installation of `szurubooru`'s
dependencies.
4. Run the tests:
```console
(python_modules) user@host:szuru/server$ ./test
```
It is recommended to rebuild the frontend after each change to configuration.
### Wiring `szurubooru` to the web server
`szurubooru` is divided into two parts: public static files, and the API. It
tries not to impose any networking configurations on the user, so it is the
user's responsibility to wire these to their web server.
Below are described the methods to integrate the API into a web server:
1. Run API locally with `waitress`, and bind it with a reverse proxy. In this
approach, the user needs to (from within `virtualenv`) install `waitress`
with `pip install waitress` and then start `szurubooru` with `./host-waitress`
from within the `server/` directory (see `--help` for details). Then the
user needs to add a virtual host that delegates the API requests to the
local API server, and the browser requests to the `client/public/`
directory.
2. Alternatively, Apache users can use `mod_wsgi`.
3. Alternatively, users can use other WSGI frontends such as `gunicorn` or
`uwsgi`, but they'll need to write wrapper scripts themselves.
Note that the API URL in the virtual host configuration needs to be the same as
the one in the `config.yaml`, so that client knows how to access the backend!
#### Example
**nginx configuration** - wiring API `http://great.dude/api/` to
`localhost:6666` to avoid fiddling with CORS:
```nginx
server {
listen 80;
server_name great.dude;
merge_slashes off; # to support post tags such as ///
location ~ ^/api$ {
return 302 /api/;
}
location ~ ^/api/(.*)$ {
proxy_pass http://127.0.0.1:6666/$1$is_args$args;
}
location / {
root /home/rr-/src/maintained/szurubooru/client/public;
try_files $uri /index.htm;
}
}
```
**`config.yaml`**:
```yaml
api_url: 'http://big.dude/api/'
base_url: 'http://big.dude/'
data_url: 'http://big.dude/data/'
data_dir: '/home/rr-/src/maintained/szurubooru/client/public/data'
```
Then the backend is started with `host-waitress` from within `virtualenv` and
`./server/` directory.

View file

@ -3,16 +3,14 @@
Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments
- Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](doc/API.md))
- Token based authentication for clients
- Rich JSON REST API ([see documentation](https://github.com/rr-/szurubooru/blob/master/API.md))
- Rich search system
- Rich privilege system
- Autocomplete in search and while editing tags
@ -20,20 +18,20 @@ scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
- Tag suggestions
- Tag implications (adding a tag automatically adds another)
- Tag aliases
- Pools and pool categories
- Duplicate detection
- Post rating and favoriting; comment rating
- Polished UI
- Browser configurable endless paging
- Browser configurable backdrop grid for transparent images
## Installation
## Requirements
It is recommended that you use Docker for deployment.
[See installation instructions.](doc/INSTALL.md)
- Python
- Postgres
- FFmpeg
- node.js
More installation resources, as well as related projects can be found on the
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
[See installation instructions.](https://github.com/rr-/szurubooru/blob/master/INSTALL.md)
## Screenshots
@ -47,4 +45,4 @@ Post view:
## License
[GPLv3](LICENSE.md).
[GPLv3](https://github.com/rr-/szurubooru/blob/master/LICENSE.md).

View file

@ -1 +1 @@
{ "presets": ["env"] }
{ "presets": ["es2015"] }

View file

@ -1,4 +0,0 @@
node_modules/*
Dockerfile
.dockerignore
**/.gitignore

View file

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

View file

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

View file

@ -1,44 +0,0 @@
FROM --platform=$BUILDPLATFORM node:lts as builder
WORKDIR /opt/app
COPY package.json package-lock.json ./
RUN npm install
COPY . ./
ARG BUILD_INFO="docker-latest"
ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
FROM --platform=$BUILDPLATFORM scratch as approot
COPY docker-start.sh /
WORKDIR /etc/nginx
COPY nginx.conf.docker ./nginx.conf
WORKDIR /var/www
COPY --from=builder /opt/app/public/ .
FROM nginx:alpine as release
RUN apk --no-cache add dumb-init
COPY --from=approot / /
CMD ["/docker-start.sh"]
VOLUME ["/data"]
ARG DOCKER_REPO
ARG BUILD_DATE
ARG SOURCE_COMMIT
LABEL \
maintainer="" \
org.opencontainers.image.title="${DOCKER_REPO}" \
org.opencontainers.image.url="https://github.com/rr-/szurubooru" \
org.opencontainers.image.documentation="https://github.com/rr-/szurubooru/blob/${SOURCE_COMMIT}/doc/INSTALL.md" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.source="https://github.com/rr-/szurubooru" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.licenses="GPL-3.0"

492
client/build.js Executable file → Normal file
View file

@ -1,109 +1,108 @@
#!/usr/bin/env node
'use strict';
// -------------------------------------------------
const webapp_icons = [
{ name: 'android-chrome-192x192.png', size: 192 },
{ name: 'android-chrome-512x512.png', size: 512 },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'mstile-150x150.png', size: 150 }
];
const webapp_splash_screens = [
{ w: 640, h: 1136, center: 320 },
{ w: 750, h: 1294, center: 375 },
{ w: 1125, h: 2436, center: 565 },
{ w: 1242, h: 2148, center: 625 },
{ w: 1536, h: 2048, center: 770 },
{ w: 1668, h: 2224, center: 820 },
{ w: 2048, h: 2732, center: 1024 }
];
const external_js = [
'dompurify',
'js-cookie',
'marked',
'mousetrap',
'nprogress',
'superagent',
'underscore',
];
const app_manifest = {
name: 'szurubooru',
icons: [
{
src: baseUrl() + 'img/android-chrome-192x192.png',
type: 'image/png',
sizes: '192x192'
},
{
src: baseUrl() + 'img/android-chrome-512x512.png',
type: 'image/png',
sizes: '512x512'
}
],
start_url: baseUrl(),
theme_color: '#24aadd',
background_color: '#ffffff',
display: 'standalone'
}
// -------------------------------------------------
const fs = require('fs');
const 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();
const camelcase = require('camelcase');
function convertKeysToCamelCase(input) {
let result = {};
Object.keys(input).map((key, _) => {
const value = input[key];
if (value !== null && value.constructor == Object) {
result[camelcase(key)] = convertKeysToCamelCase(value);
} else {
result[camelcase(key)] = value;
}
});
return result;
}
function readTextFile(path) {
return fs.readFileSync(path, 'utf-8');
}
function gzipFile(file) {
file = path.normalize(file);
execSync('gzip -6 -k ' + file);
function writeFile(path, content) {
return fs.writeFileSync(path, content);
}
function baseUrl() {
return process.env.BASE_URL ? process.env.BASE_URL : '/';
function getVersion() {
return execSync('git describe --always --dirty --long --tags')
.toString()
.trim();
}
// -------------------------------------------------
function getConfig() {
const yaml = require('js-yaml');
const merge = require('merge');
const camelcaseKeys = require('camelcase-keys');
function bundleHtml() {
const underscore = require('underscore');
const babelify = require('babelify');
function parseConfigFile(path) {
let result = yaml.load(readTextFile(path, 'utf-8'));
return convertKeysToCamelCase(result);
}
function minifyHtml(html) {
let config = parseConfigFile('../config.yaml.dist');
try {
const localConfig = parseConfigFile('../config.yaml');
config = merge.recursive(config, localConfig);
} catch (e) {
console.warn('Local config does not exist, ignoring');
}
config.canSendMails = !!config.smtp.host;
delete config.secret;
delete config.smtp;
delete config.database;
config.meta = {
version: getVersion(),
buildDate: new Date().toUTCString(),
};
return config;
}
function copyFile(source, target) {
fs.createReadStream(source).pipe(fs.createWriteStream(target));
}
function minifyJs(path) {
return require('uglify-js').minify(path, {compress: {unused: false}}).code;
}
function minifyCss(css) {
return require('csso').minify(css);
}
function minifyHtml(html) {
return require('html-minifier').minify(html, {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true,
}).trim();
}
}
const baseHtml = readTextFile('./html/index.htm')
.replace('<!-- Base HTML Placeholder -->', `<base href="${baseUrl()}"/>`);
fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml));
function bundleHtml(config) {
const underscore = require('underscore');
const babelify = require('babelify');
const baseHtml = readTextFile('./html/index.htm', 'utf-8');
const finalHtml = baseHtml
.replace(
/(<title>)(.*)(<\/title>)/,
util.format('$1%s$3', config.name));
writeFile('./public/index.htm', minifyHtml(finalHtml));
let compiledTemplateJs = [
`'use strict';`,
`let _ = require('underscore');`,
`let templates = {};`
];
for (const file of glob.sync('./html/**/*.tpl')) {
glob('./html/**/*.tpl', {}, (er, files) => {
let compiledTemplateJs = '\'use strict\'\n';
compiledTemplateJs += 'let _ = require(\'underscore\');';
compiledTemplateJs += 'let templates = {};';
for (const file of files) {
const name = path.basename(file, '.tpl').replace(/_/g, '-');
const placeholders = [];
let templateText = readTextFile(file);
let templateText = readTextFile(file, 'utf-8');
templateText = templateText.replace(
/<%.*?%>/ig,
(match) => {
@ -117,308 +116,119 @@ function bundleHtml() {
(match, number) => { return placeholders[number]; });
const functionText = underscore.template(
templateText, { variable: 'ctx' }).source;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
templateText, {variable: 'ctx'}).source;
compiledTemplateJs += `templates['${name}'] = ${functionText};`;
}
compiledTemplateJs.push('module.exports = templates;');
fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n'));
compiledTemplateJs += 'module.exports = templates;';
writeFile('./js/.templates.autogen.js', compiledTemplateJs);
console.info('Bundled HTML');
});
}
function bundleCss() {
const stylus = require('stylus');
function minifyCss(css) {
return require('csso').minify(css).css;
}
glob('./css/**/*.styl', {}, (er, files) => {
let css = '';
for (const file of glob.sync('./css/**/*.styl')) {
css += stylus.render(readTextFile(file), { filename: file });
}
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/app.min.css');
for (const file of files) {
css += stylus.render(
readTextFile(file), {filename: file});
}
writeFile('./public/css/app.min.css', minifyCss(css));
fs.copyFileSync(
copyFile(
'./node_modules/font-awesome/css/font-awesome.min.css',
'./public/css/vendor.min.css');
if (process.argv.includes('--gzip')) {
gzipFile('./public/css/vendor.min.css');
}
console.info('Bundled CSS');
}
function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), { compress: { unused: false } }).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
function bundleVendorJs(compress) {
function bundleJs(config) {
const browserify = require('browserify');
const external = [
'underscore',
'superagent',
'mousetrap',
'js-cookie',
'nprogress',
];
function writeJsBundle(b, path, message, compress) {
let outputFile = fs.createWriteStream(path);
b.bundle().pipe(outputFile);
outputFile.on('finish', function() {
if (compress) {
writeFile(path, minifyJs(path));
}
console.info(message);
});
}
glob('./js/**/*.js', {}, (er, files) => {
if (!process.argv.includes('--no-vendor-js')) {
let b = browserify();
for (let lib of external_js) {
for (let lib of external) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
if (config.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() {
if (!process.argv.includes('--no-vendor-js')) {
bundleVendorJs(true);
writeJsBundle(
b, './public/js/vendor.min.js', 'Bundled vendor JS', true);
}
if (!process.argv.includes('--no-app-js')) {
let watchify = require('watchify');
let b = browserify({ debug: process.argv.includes('--debug') });
if (!process.argv.includes('--no-transpile')) {
let outputFile = fs.createWriteStream('./public/js/app.min.js');
let b = browserify({debug: config.debug});
if (config.transpile) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = !process.argv.includes('--debug');
bundleAppJs(b, compress, () => { });
writeJsBundle(
b.external(external).add(files),
'./public/js/app.min.js',
'Bundled app JS',
!config.debug);
}
});
}
const environment = process.argv.includes('--watch') ? "development" : "production";
function bundleConfig() {
function getVersion() {
let build_info = process.env.BUILD_INFO;
if (!build_info) {
try {
build_info = execSync('git describe --always --dirty --long --tags').toString();
} catch (e) {
console.warn('Cannot find build version');
build_info = 'unknown';
}
}
return build_info.trim();
}
const config = {
meta: {
version: getVersion(),
buildDate: new Date().toUTCString()
},
environment: environment
};
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
console.info('Generated config file');
}
function bundleBinaryAssets() {
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
console.info('Copied images');
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
for (let file of glob.sync('./node_modules/font-awesome/fonts/*.*')) {
function bundleConfig(config) {
writeFile(
'./js/.config.autogen.json', JSON.stringify(config));
glob('./node_modules/font-awesome/fonts/*.*', {}, (er, files) => {
for (let file of files) {
if (fs.lstatSync(file).isDirectory()) {
continue;
}
fs.copyFileSync(file, path.join('./public/fonts/', path.basename(file)));
copyFile(file, path.join('./public/fonts/', path.basename(file)));
}
if (process.argv.includes('--gzip')) {
for (let file of glob.sync('./public/fonts/*.*')) {
if (file.endsWith('woff2')) {
continue;
}
gzipFile(file);
}
}
console.info('Copied fonts')
}
function bundleWebAppFiles() {
const Jimp = require('jimp');
fs.writeFileSync('./public/manifest.json', JSON.stringify(app_manifest));
console.info('Generated app manifest');
Promise.all(webapp_icons.map(icon => {
return Jimp.read('./img/app.png')
.then(file => {
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name));
});
}))
.then(() => {
console.info('Generated webapp icons');
});
Promise.all(webapp_splash_screens.map(dim => {
return Jimp.read('./img/splash.png')
.then(file => {
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF)
.contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/',
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
});
}))
.then(() => {
console.info('Generated splash screens');
});
}
function makeOutputDirs() {
const dirs = [
'./public',
'./public/css',
'./public/fonts',
'./public/img',
'./public/js'
];
for (let dir of dirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, 0o755);
console.info('Created directory: ' + dir);
}
function bundleBinaryAssets() {
glob('./img/*.png', {}, (er, files) => {
for (let file of files) {
copyFile(file, path.join('./public/img/', path.basename(file)));
}
});
}
function watch() {
let wss = new WebSocket.Server({ port: 8080 });
const liveReload = !process.argv.includes('--no-live-reload');
process.on('uncaughtException', (error) => {
const stack = error.stack;
delete error.stack;
console.log(error);
console.log(stack);
});
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();
const config = getConfig();
bundleConfig(config);
bundleBinaryAssets();
if (!process.argv.includes('--no-html')) {
bundleHtml(config);
}
if (!process.argv.includes('--no-css')) {
bundleCss();
bundleHtml();
bundleVendorJs(true);
let watchify = require('watchify');
let b = browserify({
debug: process.argv.includes('--debug'),
entries: ['js/main.js'],
cache: {},
packageCache: {},
});
b.plugin(watchify);
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = false;
function bundle(id) {
console.info("Rebundling app JS...");
let start = new Date();
bundleAppJs(b, compress, () => {
let end = new Date() - start;
console.info('Rebundled in %ds.', end / 1000)
emitReload();
});
}
b.on('update', bundle);
bundle();
}
// -------------------------------------------------
console.log("Building for '" + environment + "' environment.");
makeOutputDirs();
bundleConfig();
if (process.argv.includes('--watch')) {
watch();
} else {
if (!process.argv.includes('--no-binary-assets')) {
bundleBinaryAssets();
}
if (!process.argv.includes('--no-web-app-files')) {
bundleWebAppFiles();
}
if (!process.argv.includes('--no-html')) {
bundleHtml();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
}
if (!process.argv.includes('--no-js')) {
bundleJs(config);
}

View file

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

View file

@ -1,15 +1,60 @@
@import colors
$comment-header-background-color = $top-navigation-color
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD
.comment-form-container
&:not(.editing)
.tabs nav
display: none
.tabs .edit.tab
display: none
.comment-content
margin-left: 0.5em
&.editing
.tab:not(.active)
display: none
.tabs-wrapper
background: $active-tab-background-color
padding: 0.3em
.tab-wrapper[data-tab='preview']
background: $window-color
.tab.preview
padding: 1em
.tab.edit
textarea
resize: vertical
width: 100%
max-height: 80vh
box-sizing: padding-box
vertical-align: top /* ghost margin on chrome */
.comment-container
padding: 0 0 0 60px
form
width: auto
margin: 0
&:after
display: block
height: 1px
content: ' '
clear: both
nav
vertical-align: middle !important
&.buttons
margin: 0 0.3em 0.5em 0 !important
float: left
&.actions
float: left
margin: 0.3em 0 0.5em 0 !important
.comment
margin: 0 0 1em 0
padding: 0
display: -webkit-flex
display: flex
.avatar
float: left
margin-left: -60px
margin-right: 1em
-webkit-flex-shrink: 0
flex-shrink: 0
vertical-align: top
.thumbnail
@ -18,72 +63,25 @@ $comment-border-color = #DDD
a
display: inline-block
nav:not(.active), .tab:not(.active)
display: none
.comment
border: 1px solid $comment-border-color
.body
flex-grow: 1
header
white-space: nowrap
font-size: 95%
line-height: 16pt
vertical-align: middle
position: relative
background: $comment-header-background-color
border-bottom: 1px solid $comment-border-color
margin-bottom: 0.5em
background: $top-navigation-color
padding: 0.2em 0.5em
nav.edit
padding: 0.25em 1em 0 1em
line-height: 2em
ul
list-style-type: none
margin: -1px 0 -1px 0
padding: 0
li
display: inline-block
border: 1px solid transparent
a
padding: 0 1em
&.active
background: $window-color
border: 1px solid $comment-border-color
border-bottom: 1px solid $window-color
nav.readonly
padding: 0 1em
line-height: 2.25em
.date, .score-container, .edit
.nickname, .date, .score-container, .edit
margin-right: 2em
.score-container, .link-container
display: inline-block
&:before
position: absolute
display: block
content: ' '
width: 0
height: 0
left: -1.5em
top: calc(50% - 0.75em)
border: 0.75em solid transparent
border-right: 0.75em solid darken($comment-border-color, 10%)
&:after
position: absolute
display: block
content: ' '
width: 0
height: 0
left: calc(-1.5em + 1px)
top: calc(50% - 0.75em)
border: 0.75em solid transparent
border-right: 0.75em solid $comment-header-background-color
.date, .score-container, .edit, .delete
font-size: 95%
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color)
color: mix($main-color, $inactive-tab-text-color)
.edit, .delete
font-size: 80%
i
margin-right: 0.3em
@ -98,49 +96,21 @@ $comment-border-color = #DDD
display: inline-block
width: 2em
.body
width: auto
margin: 1em
.keep-height
position: relative
textarea
position: absolute
width: 100%
height: 100%
.tab.edit
min-height: 150px
.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
ul
list-style-position: inside
margin: 1em 0
padding: 0 0 0 1.5em
padding: 0
.sjis
font-family: 'MS PGothic', ' ', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
background: #fbfbfb
color: #111
font-size: 1em
font-size: 12pt
line-height: 1
margin: 0
padding: 4px
@ -148,6 +118,9 @@ $comment-border-color = #DDD
white-space: pre
word-wrap: normal
p:first-child
margin-top: 0
.spoiler
background: #eee
color: #eee
@ -167,7 +140,5 @@ $comment-border-color = #DDD
background: #fafafa
color: #444
:first-child
margin-top: 0
:last-child
blockquote :last-child
margin-bottom: 0

View file

@ -1,9 +1,4 @@
.comments>ul
list-style-type: none
margin: 0
margin: 0 0 2em 0
padding: 0
>li
margin-bottom: 1em
&:last-child
margin-bottom: 0

View file

@ -1,25 +1,15 @@
@import colors
$comment-border-color = $top-navigation-color
$comment-border-color-darktheme = $top-navigation-color-darktheme
.global-comment-list
text-align: left
&>ul
list-style-type: none
margin: 1em 0 0
margin: 1em 0
padding: 0
&>li
margin-top: 2em
padding-top: 2em
border-top: 3px solid $comment-border-color
&:first-child
margin-top: 0
padding-top: 0
border-top: none
@media (max-width: 700px)
&>li
margin-bottom: 5em
padding: 1vw
.post-thumbnail
margin-bottom: 1em
.thumbnail
@ -28,17 +18,14 @@ $comment-border-color-darktheme = $top-navigation-color-darktheme
@media (min-width: 700px)
&>li
padding-left: 13em
display: flex
margin-bottom: 2em
.post-thumbnail
float: left
margin: 0 0 1em -13em
.thumbnail
width: 12em
height: 8em
&>li
clear: both
.post-thumbnail
vertical-align: top
margin-right: 1em
@ -47,8 +34,3 @@ $comment-border-color-darktheme = $top-navigation-color-darktheme
.comments-container
width: 100%
.darktheme .global-comment-list
&>ul
&>li
border-top: 3px solid $comment-border-color-darktheme

View file

@ -4,21 +4,21 @@ form
display: block
width: 20em
.input
ul
list-style-type: none
margin: 0 0 2em 0
margin: 0 0 1em 0
padding: 0
li
margin-top: 1.2em
label
display: block
padding: 0.3em 0
.input
margin-bottom: 2em
.input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
.input li:first-child
padding-top: 0
margin-top: 0
form:not(.horizontal)
.hint
margin-top: 0.2em
margin-bottom: 0
@ -26,31 +26,18 @@ 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
.input, .buttons, ul
display: inline-block
vertical-align: top
vertical-align: middle
margin: 0
padding: 0
input
vertical-align: top
vertical-align: middle
.buttons
margin-right: 0.5em
@media (max-width: 1000px)
display: block
.input, .buttons, ul
display: block
margin-top: 0.5em
&:first-child
margin-top: 0
.buttons
margin-right: 0
@ -141,48 +128,6 @@ input[type=checkbox]:focus + .checkbox:before
/*
* Date and time inputs
*/
input[type=date],
input[type=time]
vertical-align: top
font-family: 'Droid Sans', sans-serif
font-size: 100%
padding: 0.2em 0.3em
box-sizing: border-box
border: 2px solid $input-enabled-border-color
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
&:focus
border-color: $main-color
&[readonly]
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
.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%)
/*
* Regular inputs
*/
@ -218,21 +163,6 @@ input[type=number]
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
select,
textarea,
input[type=text],
input[type=email],
input[type=password],
input[type=number]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
input[readonly],
input[readonly]+.radio,
input[readonly]+.checkbox,
@ -242,25 +172,13 @@ input:disabled
cursor: not-allowed
label.color
white-space: nowrap
position: relative
display: flex
input[type=text]
margin-right: 0.25em
width: auto
.preview
display: inline-block
text-align: center
padding: 0 0.5em
border: 2px solid black
&:after
content: 'A'
.background-preview
border-right: 0
color: transparent
.text-preview
border-left: 0
pointer-events: none
input[type=color]
position: absolute
opacity: 0
form.show-validation .input
input:invalid
@ -271,9 +189,8 @@ form.show-validation .input
outline: 0
border: 2px solid $input-good-border-color
background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/*
* Buttons
@ -284,13 +201,10 @@ input[type=submit]
cursor: pointer
font-size: 100%
padding: 0.2em 0.7em
border-radius: 0
border: 2px solid $button-enabled-background-color
background: $button-enabled-background-color
color: $button-enabled-text-color
outline: 0 /* something on Chrome */
-moz-appearance: none
-webkit-appearance: none
&:disabled
cursor: default
@ -319,30 +233,25 @@ input::-moz-focus-inner
* File dropper
*/
.file-dropper-holder
display: flex
flex-wrap: wrap
.file-dropper
display: block
width: 100%
background: $window-color
border: 3px dashed #eee
padding: 0.3em 0.5em
line-height: 140%
text-align: center
cursor: pointer
overflow: hidden
word-wrap: break-word
.url-holder
display: flex
margin-top: 0.5em
input, button
min-width: 0 /* firefox being sassy */
width: auto !important /* don't inherit anything weird */
input
margin-top: 0.5em
width: auto
flex: 1
button
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
margin-top: 0.5em
width: 8em
input[type=file]:disabled+.file-dropper
cursor: default
@ -353,6 +262,8 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active
border-color: $main-color
.autocomplete
position: absolute
z-index: 10
@ -377,10 +288,6 @@ input[type=file]:focus+.file-dropper,
.disabled
color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete
display: none

View file

@ -1,17 +1,12 @@
@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';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
src: local('Open Sans'), local('OpenSans'), url(/fonts/open_sans.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
/* make <body> cover entire viewport */
@ -26,32 +21,19 @@ body
margin: 0
color: $text-color
font-family: 'Open Sans', sans-serif
font-size: 1em
line-height: 1.4
font-size: 12pt
line-height: 18pt
@media (max-width: 800px)
font-size: 0.875em
font-size: 10pt
line-height: 15pt
@media (max-width: 1200px)
font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
font-size: 11pt
line-height: 16.5pt
h1, h2, h3
font-weight: normal
margin-bottom: 1em
h1
font-size: 2em
h2
font-size: 1.5em
p,
ol,
ul
margin: 1em 0
th
font-weight: normal
@ -71,11 +53,6 @@ a
.vim-nav-hint
position: absolute
visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append
margin-left: 1em
@ -84,10 +61,8 @@ form .fa-question-circle-o
vertical-align: middle
#content-holder
padding: 1.5em
padding: 1.5vw
text-align: center
@media (max-width: 1000px)
padding: 1em
>.content-wrapper
box-sizing: border-box /* make max-width: 100% on this element include padding */
text-align: left
@ -95,30 +70,9 @@ form .fa-question-circle-o
margin: 0 auto
>*:first-child, form h1
margin-top: 0
nav.buttons
ul
display: block
max-width: 100%
white-space: nowrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
>.content-wrapper:not(.transparent)
background: $top-navigation-color
padding: 1.8em
@media (max-width: 1000px)
padding: 1.5em
.content,
.content .subcontent
>*:last-child
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
padding: 2vw
hr
border: 0
@ -126,9 +80,6 @@ hr
margin: 1em 0
padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav
ul
list-style-type: none
@ -174,39 +125,6 @@ nav
li
display: inline-block
float: left
a
padding: 0 1.5em
#mobile-navigation-toggle
display: none
width: 100%
padding: 0 1em
line-height: 2.3em
font-family: inherit
border: none
background: none
color: $active-tab-text-color
.site-name
display: block
float: left
max-width: 50vw
overflow: hidden
text-overflow: ellipsis
.toggle-icon
display: block
float: right
@media (max-width: 1000px)
text-align: left
li
display: none
float: none
a
display: block
padding: 0 1em
#mobile-navigation-toggle
display: block
&.opened
li
display: block
ul li[data-name=account],
ul li[data-name=register],
ul li[data-name=login],
@ -223,26 +141,6 @@ nav
margin-right: 0.6em
margin-left: calc(0.6em - 1.2em)
float: left
@media (max-width: 1000px)
display: none
.darktheme nav
&.buttons
ul
li:not(.active) a
color: $inactive-tab-text-color-darktheme
li:hover:not(.active) a
color: $active-tab-text-color-darktheme
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
@ -268,18 +166,6 @@ a .access-key
border: 1px solid $message-success-border-color
background: $message-success-background-color
.darktheme .messages
.message
&.info
border: 1px solid darken($message-info-border-color, 30%)
background: darken($message-info-background-color, 60%)
&.error
border: 1px solid darken($message-error-border-color, 30%)
background: darken($message-error-background-color, 60%)
&.success
border: 1px solid darken($message-success-border-color, 30%)
background: darken($message-success-background-color, 80%)
.thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle
@ -290,14 +176,9 @@ a .access-key
width: 20px
height: 20px
&.empty
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-image: url('/img/transparency_grid.png')
background-repeat: repeat
background-size: 20px 20px
background-size: initial
img
opacity: 0
width: 100%
@ -313,14 +194,6 @@ a .access-key
margin-top: 0 !important
margin-bottom: 0 !important
.table-wrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
/* hack to prevent text from being copied */
[data-pseudo-content]:before {
content: attr(data-pseudo-content)

View file

@ -16,17 +16,9 @@
color: mix($text-color, $inactive-link-color)
font-size: 120%
i
font-size: 1em
font-size: 12pt
color: $inactive-link-color
float: right
line-height: 2em
.expander-content
padding: 0.5em 0.5em 2em 0.5em
.darktheme .expander
header
background: $active-tab-background-color-darktheme
a
color: mix($text-color-darktheme, $inactive-link-color-darktheme)
i
color: $inactive-link-color-darktheme

View file

@ -16,10 +16,6 @@
font-size: 1.6em
&:first-child
margin-top: 0
@media (max-width: 1000px)
margin-top: 1.5em
&:first-child
margin-top: 0
nav
ul
margin: 0 auto

View file

@ -6,16 +6,13 @@
margin-bottom: 1em
h1
line-height: initial
font-size: 2.5em
font-size: 30pt
margin: 0
.messages
text-align: center
.message
margin: 0 auto 2em auto
margin-bottom: 2em
form
display: inline-block
width: auto
vertical-align: middle
margin: 0 0 2em 0
@ -34,8 +31,6 @@
display: flex
align-items: center
justify-content: center
&:empty
margin-bottom: 0
nav
a
@ -55,8 +50,6 @@
li
display: inline
white-space: nowrap
@media (max-width: 800px)
display: block
.sep
word-spacing: 1.1em
background-repeat: no-repeat

View file

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

View file

@ -1,2 +0,0 @@
#password-reset
max-width: 30em

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,6 @@
@import colors
.post-container
.post-content.transparency-grid img
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-size: 20px 20px
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background: url('/img/transparency_grid.png')
text-align: center
.post-content
@ -16,7 +8,16 @@
margin: 0 auto
position: relative
.resize-listener
img, object, video, .post-overlay
position: absolute
height: 100%
width: 100%
left: 0
right: 0
top: 0
bottom: 0
.post-overlay>*
position: absolute
left: 0
right: 0
@ -24,6 +25,3 @@
bottom: 0
width: 100%
height: 100%
img
image-orientation: from-image

View file

@ -14,6 +14,9 @@
.right-post-container
width: 47%
float: right
input[type=text]
width: 8em
margin-top: -2px
.post-mirror
margin-bottom: 1em
&:after
@ -28,10 +31,3 @@
margin-right: 0.35em
.target-post, .target-post-content
margin: 1em 0
header
margin-bottom: 1em
label
display: inline-block
margin-top: 2px
input[type=text]
width: 6em

View file

@ -54,12 +54,10 @@
.icon:not(:first-of-type)
margin-left: 1em
.edit-overlay
.masstag
position: absolute
top: 0.5em
left: 0.5em
.tag-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
@ -70,7 +68,7 @@
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
font-size: 20pt
&.tagged
background: rgba(0, 230, 0, 0.7)
&:after
@ -84,66 +82,11 @@
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
.safety-flipper a
display: inline-block
margin: 0.1em
box-sizing: border-box
border: 0
display: inline-block
width: 1.2em
height: 1.2em
text-align: center
line-height: 1em
font-size: 1.6em
border: 3px solid
&.safety-safe
background-color: darken($safety-safe, 5%)
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&.safety-sketchy
background-color: $safety-sketchy
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&.safety-unsafe
background-color: $safety-unsafe
border-color: @background-color
&:not(.active)
background-color: alpha(@background-color, 0.3)
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail
background-position: 50% 30%
width: 100%
height: 100%
outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags
.thumbnail
@ -158,102 +101,41 @@
.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
label
display: none !important
display: none
form
width: auto
margin-bottom: 0.75em
*
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
input[name=search-text]
width: 25em
@media (max-width: 1000px)
display: block
width: 100%
margin-bottom: 0.5em
input[name=masstag]
width: 12em
.masstag-hint, .open-masstag
margin-right: 1em
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.bulk-edit
&:not(.opened)
.close
display: none
&.opened
.open
display: none
&.hidden
display: none
.bulk-edit-tags
&.opened
.hint
@media (max-width: 1000px)
display: block
margin-bottom: 0.5em
&:not(.opened)
.masstag
&:not(.active)
[type=text],
.start
.start-tagging,
.stop-tagging
display: none
.hint
.masstag-hint
display: none
input[name=tag]
width: 24em
@media (max-width: 1000px)
display: block
width: 100%
margin-bottom: 0.5em
.append
&.open,
&.hint
@media (max-width: 1000px)
margin-left: 0
.hint
margin-right: 1em
.bulk-edit-safety
.append
@media (max-width: 1000px)
margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
&.active
.open-masstag
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety
margin-right: 0.25em
&.safety-safe

View file

@ -7,15 +7,15 @@
>.sidebar
margin-right: 1em
min-width: 21em
max-width: 21em
min-width: 20em
max-width: 20em
line-height: 160%
a:active
border: 0
outline: 0
>.sidebar>nav.buttons, >.content nav.buttons
nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
@ -27,46 +27,29 @@
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
transition: background 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
>.content
width: 100%
.post-container
margin-bottom: 0.6em
margin-bottom: 2em
.post-content
margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
background: unset
box-shadow: inset 0 0 0 0.3em $main-color
@media (max-width: 800px)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
max-width: 0
margin-right: 0
>.content
order: 1
@ -119,6 +102,7 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
li
@ -146,24 +130,10 @@
display: inline-block
.management
ul
list-style-type: none
margin: 0
padding: 0
li
margin: 0
padding: 0
.post-source
textarea
white-space: pre
overflow-wrap: normal
overflow-x: scroll
form
width: auto
label:not(.file-dropper)
label
margin-bottom: 0.3em
display: block

View file

@ -1,7 +1,5 @@
@import colors
$upload-header-background-color = $top-navigation-color
$upload-header-background-color-darktheme = $top-navigation-color-darktheme
$upload-border-color = #DDD
$cancel-button-color = tomato
#post-upload
@ -13,12 +11,8 @@ $cancel-button-color = tomato
&.inactive input[type=submit],
&.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit],
&.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel
display: none
@ -27,8 +21,6 @@ $cancel-button-color = tomato
.file-dropper
font-size: 150%
padding: 2em
small
font-size: 60%
input[type=submit]
margin-top: 1em
@ -43,81 +35,21 @@ $cancel-button-color = tomato
.skip-duplicates
margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
.messages
margin-top: 1em
.uploadables-container
list-style-type: none
margin: 0
padding: 0
.uploadable-container
clear: both
li
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
height: 8em
margin: 0 0 0 -13em
.thumbnail
width: 100%
height: 100%
.uploadable
border: 1px solid $upload-border-color
min-height: 8em
box-sizing: border-box
header
line-height: 1.5em
padding: 0.25em 1em
text-align: left
background: $upload-header-background-color
border-bottom: 1px solid $upload-border-color
nav
&:first-of-type
float: left
a
margin: 0 0.5em 0 0
&:last-of-type
float: right
a
margin: 0 0 0 0.5em
ul
list-style-type: none
ul, li
display: inline-block
margin: 0
padding: 0
span.filename
padding: 0 0.5em
display: block
.file
margin: 0.3em 0
overflow: hidden
white-space: nowrap
text-align: left
text-overflow: ellipsis
.body
margin: 1em
.anonymous
margin: 0.3em 0
@ -127,56 +59,18 @@ $cancel-button-color = tomato
display: inline-block
margin-right: 1em
.options div
display: inline-block
margin: 0 1em 0 0
.messages
margin-top: 1em
.message:last-child
margin-bottom: 0
.lookalikes
list-style-type: none
margin: 0
padding: 0
li
clear: both
margin: 1em 0 0 0
padding-left: 7em
font-size: 90%
.thumbnail-wrapper
float: left
width: 6em
height: 4em
margin: 0 0 0 -7em
width: 12.5em
height: 7em
margin: 0.2em 1em 0 0
.thumbnail
width: 100%
height: 100%
.description
margin-right: 0.5em
display: inline-block
.controls
float: right
display: inline-block
&:first-child .move-up
a
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
margin-left: 0.5em

View file

@ -8,16 +8,11 @@ $snapshot-merged-background-color = #FEC
ul
margin: 0 auto
padding: 0
width: 100%
max-width: 35em
list-style-type: none
li
margin-bottom: 1em
&:last-child
margin-bottom: 0
.time
float: right
@ -31,34 +26,19 @@ $snapshot-merged-background-color = #FEC
div.operation-created
background: $snapshot-created-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-created-background-color, 50%)
div.operation-modified
background: $snapshot-modified-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-modified-background-color, 50%)
div.operation-deleted
background: $snapshot-deleted-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-deleted-background-color, 50%)
div.operation-merged
background: $snapshot-merged-background-color
&+.details
background: alpha(@background, 50%)
background: lighten($snapshot-merged-background-color, 50%)
.darktheme .snapshot-list ul li
div.operation-created
background: darken($snapshot-created-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-modified
background: darken($snapshot-modified-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-deleted
background: darken($snapshot-deleted-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-merged
background: darken($snapshot-merged-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.details
margin-bottom: 2em

View file

@ -2,7 +2,7 @@
.content-wrapper.tag-categories
width: 100%
max-width: 45em
max-width: 40em
table
border-spacing: 0
width: 100%
@ -11,19 +11,13 @@
td, th
padding: .4em
&.color
input[type=text]
width: 8em
text-align: center
&.usages
text-align: center
&.remove, &.set-default
white-space: pre
th
white-space: nowrap
&:first-child
padding-left: 0
&:last-child
padding-right: 0
tfoot
display: none
form
width: auto

View file

@ -46,7 +46,7 @@ div.tag-input
.wrapper
margin-left: 0.5em
background: $window-color
background: $tag-suggestions-background-color
border: 1px solid $tag-suggestions-border-color
width: 15em
word-break: break-all
@ -55,14 +55,13 @@ div.tag-input
padding: 0.2em 1em
margin: 0
ul
list-style-type: none
margin: 0
overflow-y: auto
overflow-x: none
max-height: 20em
padding: 0.5em 1em 0 1em
li:last-child
border-bottom: 0.5em solid alpha($window-color, 0)
border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0)
li
margin: 0
font-size: 90%
@ -86,16 +85,9 @@ 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
padding: 0
margin-top: 0.5em
li
margin: 0
width: 100%
@ -109,30 +101,18 @@ ul.compact-tags
a:focus
outline: 0
box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication
background: $implied-tag-background-color
color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new
background: $new-tag-background-color
color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i
padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color
@ -143,19 +123,3 @@ div.tag-input, ul.compact-tags
margin-left: 0.7em
.remove-tag
margin-right: 0.5em
.darktheme
div.tag-input .tag-suggestions
.buttons a
color: $inactive-link-color-darktheme
.wrapper
background: $window-color-darktheme
ul li:last-child
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
p
background: darken($tag-suggestions-header-color, 80%)
.append
color: $inactive-link-color-darktheme
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color-darktheme

View file

@ -11,7 +11,6 @@
th, td
padding: 0.1em 0.5em
th
white-space: nowrap
background: $top-navigation-color
.names
width: 28%
@ -40,28 +39,14 @@
.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
display: none
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
max-width: 15em
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -21,31 +21,19 @@
.details
font-size: 90%
line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail
width: 3em
height: 3em
.darktheme .user-list
ul li
background: $top-navigation-color-darktheme
margin: 0.25em 0.6em 0 0
.user-list-header
label
display: none !important
display: none
text-align: left
form
width: auto
input[name=search-text]
width: 25em
@media (max-width: 1000px)
width: 100%
max-width: 15em
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .user-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -1,6 +1,3 @@
@import colors
$token-border-color = $active-tab-background-color
#user
width: 100%
max-width: 35em
@ -40,43 +37,7 @@ $token-border-color = $active-tab-background-color
height: 1px
clear: both
#user-tokens
.token-flex-container
width: 100%
display: flex;
flex-direction column;
padding-bottom: 0.5em;
.full-width
width: 100%
.token-flex-row
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.2em;
.no-wrap
white-space: nowrap;
.token-input
min-height: 2em;
line-height: 2em;
text-align: center;
.token-flex-column
display: flex;
flex-direction: column;
.token-flex-labels
padding-right: 0.5em
hr
border-top: 3px solid $token-border-color
form
width: 100%;
#user-delete form
width: 100%

View file

@ -1,11 +0,0 @@
#!/usr/bin/dumb-init /bin/sh
# Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/index.htm \
/var/www/manifest.json
# Start server
exec nginx

View file

@ -1,85 +1,57 @@
<div class='comment-container'>
<div class='comment'>
<div class='avatar'>
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
<a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>
<% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %>
<a href='/user/<%- encodeURIComponent(ctx.comment.user.name) %>'>
<% } %>
<%= ctx.makeThumbnail(ctx.user ? ctx.user.avatarUrl : null) %>
<%= ctx.makeThumbnail(ctx.comment.user ? ctx.comment.user.avatarUrl : null) %>
<% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
<% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %>
</a>
<% } %>
</div>
<div class='comment'>
<header>
<nav class='edit tabs'>
<ul>
<li class='edit'><a href>Write</a></li>
<li class='preview'><a href>Preview</a></li>
</ul>
</nav>
<nav class='readonly'><%
%><strong><span class='nickname'><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'><%
<div class='body'>
<header><%
%><span class='nickname'><%
%><% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %><%
%><a href='/user/<%- encodeURIComponent(ctx.comment.user.name) %>'><%
%><% } %><%
%><%- ctx.user ? ctx.user.name : 'Deleted user' %><%
%><%- ctx.comment.user ? ctx.comment.user.name : 'Deleted user' %><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%><% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %><%
%></a><%
%><% } %><%
%></span></strong>
%></span><%
<span class='date'><%
%>commented <%= ctx.makeRelativeTime(ctx.comment ? ctx.comment.creationTime : null) %><%
%><wbr><%
%><span class='date'><%
%><%= ctx.makeRelativeTime(ctx.comment.creationTime) %><%
%></span><%
%><wbr><%
%><span class='score-container'></span><%
%><% if (ctx.canEditComment || ctx.canDeleteComment) { %><%
%><span class='action-container'><%
%><wbr><%
%><% if (ctx.canEditComment) { %><%
%><a href class='edit'><%
%><i class='fa fa-pencil'></i>&nbsp;edit<%
%><i class='fa fa-pencil'></i> edit<%
%></a><%
%><% } %><%
%><wbr><%
%><% if (ctx.canDeleteComment) { %><%
%><a href class='delete'><%
%><i class='fa fa-remove'></i>&nbsp;delete<%
%><i class='fa fa-remove'></i> delete<%
%></a><%
%><% } %><%
%></span><%
%><% } %><%
%></nav><%
%></header>
<form class='body'>
<div class='keep-height'>
<div class='tab preview'>
<div class='comment-content'>
<%= ctx.makeMarkdown(ctx.comment ? ctx.comment.text : '') %>
</div>
</div>
<div class='tab edit'>
<textarea required minlength=1><%- ctx.comment ? ctx.comment.text : '' %></textarea>
</div>
</div>
<nav class='edit'>
<div class='messages'></div>
<input type='submit' class='save-changes' value='Save'/>
<% if (!ctx.onlyEditing) { %>
<input type='button' class='cancel-editing discourage' value='Cancel'/>
<% } %>
</div>
</form>
<div class='comment-form-container'></div>
</div>
</div>

View file

@ -0,0 +1,31 @@
<div class='tabs'>
<form>
<div class='tabs-wrapper'><%
%><div class='tab-wrapper'><%
%><div class='preview tab'><%
%><div class='comment-content'><%
%><%= ctx.makeMarkdown(ctx.comment.text) %><%
%></div><%
%></div><%
%><div class='edit tab'><%
%><textarea required minlength=1><%- ctx.comment.text %></textarea><%
%></div><%
%></div><%
%></div>
<nav class='buttons'>
<ul>
<li class='preview'><a href>Preview</a></li>
<li class='edit'><a href>Edit</a></li>
</ul>
</nav>
<nav class='actions'>
<input type='submit' class='save' value='Save'/>
<input type='button' class='cancel discourage' value='Cancel'/>
</nav>
</form>
<div class='messages'></div>
</div>

View file

@ -1,10 +1,10 @@
<div class='global-comment-list'>
<ul><!--
--><% for (let post of ctx.response.results) { %><!--
--><% for (let post of ctx.results) { %><!--
--><li><!--
--><div class='post-thumbnail'><!--
--><% if (ctx.canViewPosts) { %><!--
--><a href='<%- ctx.formatClientLink('post', post.id) %>'><!--
--><a href='/post/<%- encodeURIComponent(post.id) %>'><!--
--><% } %><!--
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
--><% if (ctx.canViewPosts) { %><!--

View file

@ -1,7 +1,5 @@
<div class='pager'>
<div class='page-header-holder'></div>
<div class='messages'></div>
<div class='page-guard top'></div>
<div class='pages-holder'></div>
<div class='page-guard bottom'></div>
</div>

View file

@ -8,19 +8,9 @@
<% } %>
<br/>
Or just click on this box.
<% if (ctx.extraText) { %>
<br/>
<small><%= ctx.extraText %></small>
<% } %>
</label>
<% if (ctx.allowUrls) { %>
<div class='url-holder'>
<input type='text' name='url' placeholder='<%- ctx.urlPlaceholder %>'/>
<% if (ctx.lock) { %>
<button>Confirm</button>
<% } else { %>
<input type='text' name='url' placeholder='Alternatively, paste an URL here.'/>
<button>Add URL</button>
<% } %>
</div>
<% } %>
</div>

View file

@ -1,11 +1,11 @@
<div class='content-wrapper' id='help'>
<nav class='buttons primary'><!--
--><ul><!--
--><li data-name='about'><a href='<%- ctx.formatClientLink('help', 'about') %>'>About</a></li><!--
--><li data-name='keyboard'><a href='<%- ctx.formatClientLink('help', 'keyboard') %>'>Keyboard</a></li><!--
--><li data-name='search'><a href='<%- ctx.formatClientLink('help', 'search') %>'>Search syntax</a></li><!--
--><li data-name='comments'><a href='<%- ctx.formatClientLink('help', 'comments') %>'>Comments</a></li><!--
--><li data-name='tos'><a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of service</a></li><!--
--><li data-name='about'><a href='/help/about'>About</a></li><!--
--><li data-name='keyboard'><a href='/help/keyboard'>Keyboard</a></li><!--
--><li data-name='search'><a href='/help/search'>Search syntax</a></li><!--
--><li data-name='comments'><a href='/help/comments'>Comments</a></li><!--
--><li data-name='tos'><a href='/help/tos'>Terms of service</a></li><!--
--></ul><!--
--></nav>

View file

@ -28,11 +28,3 @@
</tr>
</tbody>
</table>
<p>You can also specify the size of embedded images like this:</p>
<ul>
<li><code>![alt](href =WIDTHx "title")</code></li>
<li><code>![alt](href =xHEIGHT "title")</code></li>
<li><code>![alt](href =WIDTHxHEIGHT "title")</code></li>
</ul>

View file

@ -33,15 +33,10 @@ shortcuts:</p>
<td><kbd>P</kbd></td>
<td>Focus first post in post list</td>
</tr>
<tr>
<td><kbd>Delete</kbd></td>
<td>Delete post (while in edit mode)</td>
</tr>
</tbody>
</table>
<p>Additionally, each item in the top navigation can be accessed using a
feature called &ldquo;access keys&rdquo;. Pressing the underlined letter while
holding Shift or Alt+Shift (depending on your browser) will go to the desired
page (most browsers) or focus the link (IE).</p>
<p>Additionally, each item in top navigation can be accessed using feature
called &ldquo;access keys&rdquo;. Pressing underlined letter while holding
Shfit or Alt+Shift (depending on your browser) will go to the desired page
(most browsers) or focus the link (IE).</p>

View file

@ -1,10 +1,9 @@
<nav class='buttons secondary'><!--
--><ul><!--
--><li data-name='default'><a href='<%- ctx.formatClientLink('help', 'search') %>'>General</a></li><!--
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
--><li data-name='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</li><!--
--><li data-name='default'><a href='/help/search'>General</a></li><!--
--><li data-name='posts'><a href='/help/search/posts'>Posts</a></li><!--
--><li data-name='users'><a href='/help/search/users'>Users</a></li><!--
--><li data-name='tags'><a href='/help/search/tags'>Tags</a></li><!--
--></ul><!--
--></nav>

View file

@ -80,9 +80,6 @@ take following form:</p>
<code>,desc</code> to control the sort direction, which can be also controlled
by negating the whole token.</p>
<p>You can escape special characters such as <code>:</code> and <code>-</code>
by prepending them with a backslash: <code>\\</code>.</p>
<h1>Example</h1>
<p>Searching for posts with following query:</p>
@ -92,8 +89,3 @@ by prepending them with a backslash: <code>\\</code>.</p>
<p>will show flash files tagged as sea, that were liked by seven people at
most, uploaded by user Pirate.</p>
<p>Searching for posts with <code>re:zero</code> will show an error message
about unknown named token.</p>
<p>Searching for posts with <code>re\:zero</code> will show posts tagged with
<code>re:zero</code>.</p>

View file

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

View file

@ -12,7 +12,7 @@
</tr>
<tr>
<td><code>tag</code></td>
<td>having given tag (accepts wildcards)</td>
<td>having given tag</td>
</tr>
<tr>
<td><code>score</code></td>
@ -20,31 +20,23 @@
</tr>
<tr>
<td><code>uploader</code></td>
<td>uploaded by given user (accepts wildcards)</td>
<td>uploaded by given user</td>
</tr>
<tr>
<td><code>upload</code></td>
<td>alias of <code>uploader</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr>
<td><code>submit</code></td>
<td>alias of <code>uploader</code></td>
<td>alias of <code>upload</code></td>
</tr>
<tr>
<td><code>comment</code></td>
<td>commented by given user (accepts wildcards)</td>
<td>commented by given user</td>
</tr>
<tr>
<td><code>fav</code></td>
<td>favorited by given user (accepts wildcards)</td>
</tr>
<tr>
<td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td>
</tr>
<tr>
<td><code>pool</code></td>
<td>belonging to the pool with the given ID</td>
<td>favorited by given user</td>
</tr>
<tr>
<td><code>tag-count</code></td>
@ -62,10 +54,6 @@
<td><code>note-count</code></td>
<td>having given number of annotations</td>
</tr>
<tr>
<td><code>note-text</code></td>
<td>having given note text (accepts wildcards)</td>
</tr>
<tr>
<td><code>relation-count</code></td>
<td>having given number of relations</td>
@ -78,21 +66,9 @@
<td><code>type</code></td>
<td>given type of posts. <code>&lt;value&gt;</code> can be either <code>image</code>, <code>animation</code> (or <code>animated</code> or <code>anim</code>), <code>flash</code> (or <code>swf</code>) or <code>video</code> (or <code>webm</code>).</td>
</tr>
<tr>
<td><code>flag</code></td>
<td>having given flag. <code>&lt;value&gt;</code> can be either <code>loop</code> or <code>sound</code>.</td>
</tr>
<tr>
<td><code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>md5</code></td>
<td>having given MD5 checksum</td>
</tr>
<tr>
<td><code>content-checksum</code></td>
<td>alias of <code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>file-size</code></td>
@ -110,14 +86,6 @@
<td><code>image-area</code></td>
<td>having given number of pixels (image width * image height)</td>
</tr>
<tr>
<td><code>image-aspect-ratio</code></td>
<td>having given aspect ratio (image width / image height)</td>
</tr>
<tr>
<td><code>image-ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>alias of <code>image-width</code></td>
@ -130,14 +98,6 @@
<td><code>area</code></td>
<td>alias of <code>image-area</code></td>
</tr>
<tr>
<td><code>aspect-ratio</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>ar</code></td>
<td>alias of <code>image-aspect-ratio</code></td>
</tr>
<tr>
<td><code>creation-date</code></td>
<td>posted at given date</td>

View file

@ -12,7 +12,7 @@
</tr>
<tr>
<td><code>category</code></td>
<td>having given category (accepts wildcards)</td>
<td>having given category</td>
</tr>
<tr>
<td><code>creation-date</code></td>

View file

@ -8,7 +8,7 @@
<%= ctx.makeTextInput({name: 'search-text', placeholder: 'enter some tags'}) %>
<input type='submit' value='Search'/>
<span class=sep>or</span>
<a href='<%- ctx.formatClientLink('posts') %>'>browse all posts</a>
<a href='/posts'>browse all posts</a>
</form>
<% } %>
<div class='post-info-container'></div>

View file

@ -1,7 +1,7 @@
<ul>
<li><%- ctx.postCount %> posts</li><span class='sep'>
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a><%- ctx.isDevelopmentMode ? " (DEV MODE)" : "" %> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='<%- ctx.formatClientLink('history') %>'>History</a></li><span class='sep'>
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='/history'>History</a></li><span class='sep'>
</span><% } %>
</ul>

View file

@ -2,31 +2,16 @@
<html>
<head>
<meta charset='utf-8'/>
<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'/>
<meta name='msapplication-TileColor' content='#ffffff'/>
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
<title>Loading...</title>
<!-- Base HTML Placeholder -->
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
<link rel='apple-touch-icon' sizes='180x180' href='img/apple-touch-icon.png'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-640x1136.png' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-750x1294.png' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1242x2148.png' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1125x2436.png' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1536x2048.png' media='(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1668x2224.png' media='(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-2048x2732.png' media='(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='manifest' href='manifest.json'/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
<title><!-- configured in the config file --></title>
<link href='/css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/>
</head>
<body>
<div id='top-navigation-holder'></div>
<div id='content-holder'></div>
<script type='text/javascript' src='js/vendor.min.js'></script>
<script type='text/javascript' src='js/app.min.js'></script>
<script type='text/javascript' src='/js/vendor.min.js'></script>
<script type='text/javascript' src='/js/app.min.js'></script>
</body>
</html>

View file

@ -1,7 +1,8 @@
<div class='content-wrapper' id='login'>
<h1>Log in</h1>
<form>
<ul class='input'>
<div class='input'>
<ul>
<li>
<%= ctx.makeTextInput({
text: 'User name',
@ -25,12 +26,13 @@
}) %>
</li>
</ul>
</div>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Log in'/>
<a class='append' href='<%- ctx.formatClientLink('password-reset') %>'>Forgot the password?</a>
<% if (ctx.canSendMails) { %>
<a class='append' href='/password-reset'>Forgot the password?</a>
<% } %>
</div>
</form>
</div>

View file

@ -1,17 +1,17 @@
<nav class='buttons'>
<ul>
<li>
<% if (ctx.prevPage !== ctx.currentPage) { %>
<a rel='prev' class='prev' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.prevPage).offset, ctx.pages.get(ctx.prevPage).limit) %>'>
<% if (ctx.prevLinkActive) { %>
<a class='prev' href='<%- ctx.prevLink %>'>
<% } else { %>
<a rel='prev' class='prev disabled'>
<a class='prev disabled'>
<% } %>
<i class='fa fa-chevron-left'></i>
<span class='vim-nav-hint'>&lt; Previous page</span>
</a>
</li>
<% for (let page of ctx.pages.values()) { %>
<% for (let page of ctx.pages) { %>
<% if (page.ellipsis) { %>
<li>&hellip;</li>
<% } else { %>
@ -20,16 +20,16 @@
<% } else { %>
<li>
<% } %>
<a href='<%- ctx.getClientUrlForPage(page.offset, page.limit) %>'><%- page.number %></a>
<a href='<%- page.link %>'><%- page.number %></a>
</li>
<% } %>
<% } %>
<li>
<% if (ctx.nextPage !== ctx.currentPage) { %>
<a rel='next' class='next' href='<%- ctx.getClientUrlForPage(ctx.pages.get(ctx.nextPage).offset, ctx.pages.get(ctx.nextPage).limit) %>'>
<% if (ctx.nextLinkActive) { %>
<a class='next' href='<%- ctx.nextLink %>'>
<% } else { %>
<a rel='next' class='next disabled'>
<a class='next disabled'>
<% } %>
<i class='fa fa-chevron-right'></i>
<span class='vim-nav-hint'>Next page &gt;</span>

View file

@ -1,5 +1,5 @@
<div class='not-found'>
<h1>Not found</h1>
<p><%- ctx.path %> is not a valid URL.</p>
<p><a href='<%- ctx.formatClientLink() %>'>Back to main page</a></p>
<p><a href='/'>Back to main page</a></p>
</div>

View file

@ -1,8 +1,8 @@
<div class='content-wrapper' id='password-reset'>
<h1>Password reset</h1>
<% if (ctx.canSendMails) { %>
<form autocomplete='off'>
<ul class='input'>
<div class='input'>
<ul>
<li>
<%= ctx.makeTextInput({
text: 'User name or e-mail address',
@ -11,20 +11,13 @@
}) %>
</li>
</ul>
</div>
<p><small>Proceeding will send an e-mail that contains a password reset
link. Clicking it is going to generate a new password for your account.
It is recommended to change that password to something else.</small></p>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Proceed'/>
</div>
</form>
<% } else { %>
<p>We do not support automatic password resetting.</p>
<% if (ctx.contactEmail) { %>
<p>Please send an e-mail to <a href='mailto:<%- ctx.contactEmail %>'><%- ctx.contactEmail %></a> to go through a manual procedure.</p>
<% } %>
<% } %>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,34 +1,30 @@
<div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(ctx.post.type)) { %>
<img class='resize-listener' alt='' src='<%- ctx.post.contentUrl %>'/>
<img alt='' src='<%- ctx.post.contentUrl %>'/>
<% } else if (ctx.post.type === 'flash') { %>
<object class='resize-listener' width='<%- ctx.post.canvasWidth %>' height='<%- ctx.post.canvasHeight %>' data='<%- ctx.post.contentUrl %>'>
<object width='<%- ctx.post.canvasWidth %>' height='<%- ctx.post.canvasHeight %>' data='<%- ctx.post.contentUrl %>'>
<param name='wmode' value='opaque'/>
<param name='movie' value='<%- ctx.post.contentUrl %>'/>
</object>
<% } else if (ctx.post.type === 'video') { %>
<%= ctx.makeElement(
'video', {
class: 'resize-listener',
controls: true,
loop: (ctx.post.flags || []).includes('loop'),
playsinline: true,
autoplay: ctx.autoplay,
},
ctx.makeElement('source', {
type: ctx.post.mimeType,
src: ctx.post.contentUrl,
}),
'Your browser doesn\'t support HTML5 videos.')
%>
<% if ((ctx.post.flags || []).includes('loop')) { %>
<video id='video' controls loop='loop'>
<% } else { %>
<video id='video' controls>
<% } %>
<source type='<%- ctx.post.mimeType %>' src='<%- ctx.post.contentUrl %>'/>
Your browser doesn't support HTML5 videos.
</video>
<% } else { console.log(new Error('Unknown post type')); } %>
<div class='post-overlay resize-listener'>
<div class='post-overlay'>
</div>
</div>

View file

@ -2,9 +2,9 @@
<h1>Post #<%- ctx.post.id %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li><a href='<%- ctx.formatClientLink('post', ctx.post.id) %>'><i class='fa fa-reply'></i> Main view</a></li><!--
--><li><a href='/post/<%- ctx.post.id %>'><i class='fa fa-reply'></i> Main view</a></li><!--
--><% if (ctx.canMerge) { %><!--
--><li data-name='merge'><a href='<%- ctx.formatClientLink('post', ctx.post.id, 'merge') %>'>Merge with&hellip;</a></li><!--
--><li data-name='merge'><a href='/post/<%- ctx.post.id %>/merge'>Merge with&hellip;</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>

View file

@ -4,7 +4,7 @@
<div class='messages'></div>
<% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
<% if (ctx.canEditPostSafety) { %>
<section class='safety'>
<label>Safety</label>
<div class='radio-wrapper'>
@ -50,32 +50,14 @@
name: 'loop',
checked: ctx.post.flags.includes('loop'),
}) %>
<%= ctx.makeCheckbox({
text: 'Sound',
name: 'sound',
checked: ctx.post.flags.includes('sound'),
}) %>
</section>
<% } %>
<% if (ctx.canEditPostSource) { %>
<section class='post-source'>
<%= ctx.makeTextarea({
text: 'Source',
value: ctx.post.source,
}) %>
</section>
<% } %>
<% if (ctx.canEditPostTags) { %>
<section class='tags'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
<%= ctx.makeTextInput({
value: ctx.post.tags.join(' '),
}) %>
</section>
<% } %>
@ -84,12 +66,6 @@
<a href class='add'>Add a note</a>
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
<a href class='delete inactive'>Delete selected note</a>
<% if (ctx.hasClipboard) { %>
<br/>
<a href class='copy'>Export notes to clipboard</a>
<br/>
<a href class='paste'>Import notes from clipboard</a>
<% } %>
</section>
<% } %>

View file

@ -4,12 +4,12 @@
<article class='previous-post'>
<% if (ctx.prevPostId) { %>
<% if (ctx.editMode) { %>
<a rel='prev' href='<%= ctx.getPostEditUrl(ctx.prevPostId, ctx.parameters) %>'>
<a href='<%= ctx.getPostEditUrl(ctx.prevPostId, ctx.parameters) %>'>
<% } else { %>
<a rel='prev' href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
<a href='<%= ctx.getPostUrl(ctx.prevPostId, ctx.parameters) %>'>
<% } %>
<% } else { %>
<a rel='prev' class='inactive'>
<a class='inactive'>
<% } %>
<i class='fa fa-chevron-left'></i>
<span class='vim-nav-hint'>&lt; Previous post</span>
@ -18,18 +18,17 @@
<article class='next-post'>
<% if (ctx.nextPostId) { %>
<% if (ctx.editMode) { %>
<a rel='next' href='<%= ctx.getPostEditUrl(ctx.nextPostId, ctx.parameters) %>'>
<a href='<%= ctx.getPostEditUrl(ctx.nextPostId, ctx.parameters) %>'>
<% } else { %>
<a rel='next' href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
<a href='<%= ctx.getPostUrl(ctx.nextPostId, ctx.parameters) %>'>
<% } %>
<% } else { %>
<a rel='next' class='inactive'>
<a class='inactive'>
<% } %>
<i class='fa fa-chevron-right'></i>
<span class='vim-nav-hint'>Next post &gt;</span>
</a>
</article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'>
<% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
@ -37,13 +36,16 @@
<span class='vim-nav-hint'>Back to view mode</span>
</a>
<% } else { %>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<% } else { %>
<a class='inactive'>
<% } %>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a>
<% } %>
</article>
<% } %>
</nav>
<div class='sidebar-container'></div>
@ -52,16 +54,13 @@
<div class='content'>
<div class='post-container'></div>
<div class='after-mobile-controls'>
<div class='description'></div>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
</div>
</div>
</div>

View file

@ -1,6 +1,6 @@
<div class='post-merge'>
<form>
<ul class='input'>
<ul>
<li class='post-mirror'>
<div class='left-post-container'></div>
<div class='right-post-container'></div>

View file

@ -1,12 +1,8 @@
<header>
<label for='merge-id-<%- ctx.name %>'>Post #</label>
<% if (ctx.editable) { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/>
<input type='button' value='Search'/>
<% } else { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/>
<% } %>
</header>
<% if (ctx.editable) { %>
<p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/></p>
<% } else { %>
<p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/></p>
<% } %>
<% if (ctx.post) { %>
<div class='post-thumbnail'>
@ -35,14 +31,7 @@
'image/gif': 'GIF',
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] +
' (' +

View file

@ -8,22 +8,11 @@
'image/gif': 'GIF',
'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><% } %><!--
--><% if (ctx.post.flags.includes('sound')) { %><i class='fa fa-volume-up'></i><% } %>
<% } %>
</section>
<section class='upload-info'>
@ -31,12 +20,10 @@
<%= ctx.makeRelativeTime(ctx.post.creationTime) %>
</section>
<% if (ctx.enableSafety) { %>
<section class='safety'>
<i class='fa fa-circle safety-<%- ctx.post.safety %>'></i><!--
--><%- ctx.post.safety[0].toUpperCase() + ctx.post.safety.slice(1) %>
</section>
<% } %>
<section class='zoom'>
<a href class='fit-original'>Original zoom</a> &middot;
@ -45,20 +32,10 @@
<a href class='fit-both'>both</a>
</section>
<% if (ctx.post.source) { %>
<section class='source'>
Source: <% for (let i = 0; i < ctx.post.sourceSplit.length; i++) { %>
<% if (i != 0) { %>&middot;<% } %>
<a href='<%- ctx.post.sourceSplit[i] %>' title='<%- ctx.post.sourceSplit[i] %>'><%- ctx.extractRootDomain(ctx.post.sourceSplit[i]) %></a>
<% } %>
</section>
<% } %>
<section class='search'>
Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>IQDB</a> &middot;
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -90,20 +67,20 @@
--><% for (let tag of ctx.post.tags) { %><!--
--><li><!--
--><% if (ctx.canViewTags) { %><!--
--><a href='<%- ctx.formatClientLink('tag', tag.names[0]) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><a href='/tag/<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><i class='fa fa-tag'></i><!--
--><% } %><!--
--><% if (ctx.canViewTags) { %><!--
--></a><!--
--><% } %><!--
--><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><a href='/posts/query=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><% } %><!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><%- tag %>&#32;<!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- ctx.getTagUsages(tag) %>'></span><!--
--></li><!--
--><% } %><!--
--></ul>

View file

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

View file

@ -1,4 +1,10 @@
<li class='uploadable-container'>
<li class='uploadable'>
<div class='controls'>
<a href class='move-up'><i class='fa fa-chevron-up'></i></a>
<a href class='move-down'><i class='fa fa-chevron-down'></i></a>
<a href class='remove'><i class='fa fa-remove'></i></a>
</div>
<div class='thumbnail-wrapper'>
<% if (['image'].includes(ctx.uploadable.type)) { %>
@ -23,25 +29,10 @@
<% } %>
</div>
<div class='uploadable'>
<header>
<nav>
<ul>
<li><a href class='move-up'><i class='fa fa-chevron-up'></i></a></li>
<li><a href class='move-down'><i class='fa fa-chevron-down'></i></a></li>
</ul>
</nav>
<nav>
<ul>
<li><a href class='remove'><i class='fa fa-remove'></i></a></li>
</ul>
</nav>
<div class='file'>
<strong><%= ctx.uploadable.name %></strong>
</div>
<span class='filename'><%= ctx.uploadable.name %></span>
</header>
<div class='body'>
<% if (ctx.enableSafety) { %>
<div class='safety'>
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
<%= ctx.makeRadio({
@ -52,45 +43,14 @@
}) %>
<% } %>
</div>
<% } %>
<div class='options'>
<% if (ctx.canUploadAnonymously) { %>
<div class='anonymous'>
<%= ctx.makeCheckbox({
text: 'Upload anonymously',
name: 'anonymous',
checked: ctx.uploadable.anonymous,
readonly: ctx.uploadable.forceAnonymous,
}) %>
</div>
<% } %>
</div>
<div class='messages'></div>
<% if (ctx.uploadable.lookalikes.length) { %>
<ul class='lookalikes'>
<% for (let lookalike of ctx.uploadable.lookalikes) { %>
<li>
<a class='thumbnail-wrapper' title='@<%- lookalike.post.id %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(lookalike.post.id) : "" %>'>
<%= ctx.makeThumbnail(lookalike.post.thumbnailUrl) %>
</a>
<div class='description'>
Similar post: <%= ctx.makePostLink(lookalike.post.id, true) %>
<br/>
<%- Math.round((1-lookalike.distance) * 100) %>% match
</div>
<div class='controls'>
<%= ctx.makeCheckbox({text: 'Copy tags', name: 'copy-tags'}) %>
<br/>
<%= ctx.makeCheckbox({text: 'Add relation', name: 'add-relation'}) %>
</div>
</li>
<% } %>
</ul>
<% } %>
</div>
</div>
</li>

View file

@ -1,37 +1,24 @@
<div class='post-list-header'><%
%><form class='horizontal search'><%
%><form class='horizontal'><%
%><%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %><%
%><wbr/><%
%><input class='mousetrap' type='submit' value='Search'/><%
%><wbr/><%
%><% if (ctx.enableSafety) { %><%
%><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><%
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
%><% } %><%
%><wbr/><%
%><a class='mousetrap button append' href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Syntax help</a><%
%></form><%
%><% if (ctx.canBulkEditTags) { %><%
%><form class='horizontal bulk-edit bulk-edit-tags'><%
%><span class='append hint'>Tagging with:</span><%
%><a href class='mousetrap button append open'>Mass tag</a><%
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append close'>Stop tagging</a><%
%></form><%
%><a class='mousetrap button append' href='/help/search/posts'>Syntax help</a><%
%><% if (ctx.canMassTag) { %><%
%><wbr/><%
%><span class='masstag'><%
%><span class='append masstag-hint'>Tagging with:</span><%
%><a href class='mousetrap button append open-masstag'>Mass tag</a><%
%><wbr/><%
%><%= ctx.makeTextInput({name: 'masstag', value: ctx.parameters.tag}) %><%
%><input class='mousetrap start-tagging' type='submit' value='Start tagging'/><%
%><a href class='mousetrap button append stop-tagging'>Stop tagging</a><%
%></span><%
%><% } %><%
%><% if (ctx.enableSafety && ctx.canBulkEditSafety) { %><%
%><form class='horizontal bulk-edit bulk-edit-safety'><%
%><a href class='mousetrap button append open'>Mass edit safety</a><%
%><a href class='mousetrap button append close'>Stop editing safety</a><%
%></form><%
%><% } %><%
%><% if (ctx.canBulkDelete) { %><%
%><form class='horizontal bulk-edit bulk-edit-delete'><%
%><a href class='mousetrap button append open'>Mass delete</a><%
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
%><a href class='mousetrap button append close'>Stop deleting</a><%
%></form><%
%><% } %><%
%></div>

View file

@ -1,18 +1,14 @@
<% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %>
<% if (ctx.response.results.length) { %>
<div class='post-list'>
<% if (ctx.results.length) { %>
<ul>
<% for (let post of ctx.response.results) { %>
<li data-post-id='<%= post.id %>'>
<% for (let post of ctx.results) { %>
<li>
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>'
title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag.names[0]).join(' ') || 'none' %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
title='@<%- post.id %> (<%- post.type %>)&#10;&#10;Tags: <%- post.tags.map(tag => '#' + tag).join(' ') || 'none' %>'
href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : "" %>'>
<%= ctx.makeThumbnail(post.thumbnailUrl) %>
<span class='type' data-type='<%- post.type %>'>
<% if (post.type == 'video' || post.type == 'flash' || post.type == 'animation') { %>
<span class='icon'><i class='fa fa-film'></i></span>
<% } else { %>
<%- post.type %>
<% } %>
</span>
<% if (post.score || post.favoriteCount || post.commentCount) { %>
<span class='stats'>
@ -37,24 +33,10 @@
</span>
<% } %>
</a>
<span class='edit-overlay'>
<% if (ctx.canBulkEditTags && ctx.parameters && ctx.parameters.tag) { %>
<a href class='tag-flipper'>
<% if (ctx.canMassTag && ctx.parameters && ctx.parameters.tag) { %>
<a href data-post-id='<%= post.id %>' class='masstag'>
</a>
<% } %>
<% if (ctx.canBulkEditSafety && ctx.parameters && ctx.parameters.safety) { %>
<span class='safety-flipper'>
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
<a href data-safety='<%- safety %>' class='safety-<%- safety %><%- post.safety === safety ? ' active' : '' %>'>
</a>
<% } %>
</span>
<% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span>
</li>
<% } %>
<%= ctx.makeFlexboxAlign() %>

View file

@ -5,10 +5,11 @@
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
text: "Enable keyboard shortcuts <a class='append icon' href='" + ctx.formatClientLink('help', 'keyboard') + "'><i class='fa fa-question-circle-o'></i></a>",
text: 'Enable keyboard shortcuts',
name: 'keyboard-shortcuts',
checked: ctx.browsingSettings.keyboardShortcuts,
}) %>
<a class='append icon' href='/help/keyboard'><i class='fa fa-question-circle-o'></i></a>
</li>
<li>
@ -22,15 +23,6 @@
}) %>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Use dark theme',
name: 'dark-theme',
checked: ctx.browsingSettings.darkTheme,
}) %>
<p class='hint'>Changing this setting will require you to refresh the page for it to apply.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Upscale small posts',
@ -47,15 +39,6 @@
<p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Use post flow',
name: 'post-flow',
checked: ctx.browsingSettings.postFlow,
}) %>
<p class='hint'>Use a content-aware flow for thumbnails on the post search page.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Enable transparency grid',
@ -73,23 +56,6 @@
}) %>
<p class='hint'>Shows a popup with suggested tags in edit forms.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Automatically play video posts',
name: 'autoplay-videos',
checked: ctx.browsingSettings.autoplayVideos,
}) %>
</li>
<li>
<%= ctx.makeCheckbox({
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>
</li>
</ul>
<div class='messages'></div>

View file

@ -1,7 +1,7 @@
<div class='snapshot-list'>
<% if (ctx.response.results.length) { %>
<% if (ctx.results.length) { %>
<ul>
<% for (let item of ctx.response.results) { %>
<% for (let item of ctx.results) { %>
<li>
<div class='header operation-<%= item.operation %>'>
<span class='time'>

View file

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

View file

@ -1,20 +1,17 @@
<div class='content-wrapper tag-categories'>
<form>
<h1>Tag categories</h1>
<div class="table-wrap">
<table>
<thead>
<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>
<tbody>
</tbody>
</table>
</div>
<% if (ctx.canCreate) { %>
<p><a href class='add'>Add new category</a></p>
@ -22,7 +19,7 @@
<div class='messages'></div>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>

View file

@ -17,16 +17,9 @@
<%- 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}) %>'>
<a href='/tags/query=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
<%- ctx.tagCategory.tagCount %>
</a>
<% } else { %>

View file

@ -1,8 +1,8 @@
<div class='tag-delete'>
<form>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<ul class='input'>
<ul>
<li>
<%= ctx.makeCheckbox({
name: 'confirm-deletion',

View file

@ -1,6 +1,6 @@
<div class='content-wrapper tag-edit'>
<form>
<ul class='input'>
<ul>
<li class='names'>
<% if (ctx.canEditNames) { %>
<%= ctx.makeTextInput({
@ -22,12 +22,18 @@
</li>
<li class='implications'>
<% if (ctx.canEditImplications) { %>
<%= ctx.makeTextInput({text: 'Implications'}) %>
<%= ctx.makeTextInput({
text: 'Implications',
value: ctx.tag.implications.join(' '),
}) %>
<% } %>
</li>
<li class='suggestions'>
<% if (ctx.canEditSuggestions) { %>
<%= ctx.makeTextInput({text: 'Suggestions'}) %>
<%= ctx.makeTextInput({
text: 'Suggestions',
value: ctx.tag.suggestions.join(' '),
}) %>
<% } %>
</li>
<li class='description'>

View file

@ -1,15 +1,13 @@
<div class='tag-merge'>
<form>
<ul class='input'>
<ul>
<li class='target'>
<%= ctx.makeTextInput({name: 'target-tag', required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
<%= ctx.makeTextInput({required: true, text: 'Target tag', pattern: ctx.tagNamePattern}) %>
</li>
<li>
<p>Usages in posts, suggestions and implications will be
merged. Category needs to be handled manually.</p>
<%= ctx.makeCheckbox({name: 'alias', text: 'Make this tag an alias of the target tag.'}) %>
merged. Category and aliases need to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
</li>

View file

@ -9,7 +9,7 @@
Aliases:<br/>
<ul><!--
--><% for (let name of ctx.tag.names.slice(1)) { %><!--
--><li><%= ctx.makeTagLink(name, false, false, ctx.tag) %></li><!--
--><li><%= ctx.makeTagLink(name) %></li><!--
--><% } %><!--
--></ul>
</section>
@ -18,7 +18,7 @@
Implications:<br/>
<ul><!--
--><% for (let tag of ctx.tag.implications) { %><!--
--><li><%= ctx.makeTagLink(tag.names[0], false, false, tag) %></li><!--
--><li><%= ctx.makeTagLink(tag) %></li><!--
--><% } %><!--
--></ul>
</section>
@ -27,7 +27,7 @@
Suggestions:<br/>
<ul><!--
--><% for (let tag of ctx.tag.suggestions) { %><!--
--><li><%= ctx.makeTagLink(tag.names[0], false, false, tag) %></li><!--
--><li><%= ctx.makeTagLink(tag) %></li><!--
--><% } %><!--
--></ul>
</section>
@ -36,6 +36,6 @@
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(ctx.tag.names[0]) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
</section>
</div>

View file

@ -1,16 +1,17 @@
<div class='tag-list-header'>
<form class='horizontal'>
<ul class='input'>
<div class='input'>
<ul>
<li>
<%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %>
</li>
</ul>
</div>
<div class='buttons'>
<input type='submit' value='Search'/>
<a class='button append' href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Syntax help</a>
<a class='button append' href='/help/search/tags'>Syntax help</a>
<% if (ctx.canEditTagCategories) { %>
<a class='append' href='<%- ctx.formatClientLink('tag-categories') %>'>Tag categories</a>
<a class='append' href='/tag-categories'>Tag categories</a>
<% } %>
</div>
</form>

View file

@ -1,58 +1,58 @@
<div class='tag-list table-wrap'>
<% if (ctx.response.results.length) { %>
<div class='tag-list'>
<% if (ctx.results.length) { %>
<table>
<thead>
<th class='names'>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
<a href='/tags/query=-sort:name'>Tag name(s)</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:name'}) %>'>Tag name(s)</a>
<a href='/tags/query=sort:name'>Tag name(s)</a>
<% } %>
</th>
<th class='implications'>
<% if (ctx.parameters.query == 'sort:implication-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
<% if (ctx.query == 'sort:implication-count') { %>
<a href='/tags/query=-sort:implication-count'>Implications</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:implication-count'}) %>'>Implications</a>
<a href='/tags/query=sort:implication-count'>Implications</a>
<% } %>
</th>
<th class='suggestions'>
<% if (ctx.parameters.query == 'sort:suggestion-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
<% if (ctx.query == 'sort:suggestion-count') { %>
<a href='/tags/query=-sort:suggestion-count'>Suggestions</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:suggestion-count'}) %>'>Suggestions</a>
<a href='/tags/query=sort:suggestion-count'>Suggestions</a>
<% } %>
</th>
<th class='usages'>
<% if (ctx.parameters.query == 'sort:usages') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
<% if (ctx.query == 'sort:usages') { %>
<a href='/tags/query=-sort:usages'>Usages</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:usages'}) %>'>Usages</a>
<a href='/tags/query=sort:usages'>Usages</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
<% if (ctx.query == 'sort:creation-time') { %>
<a href='/tags/query=-sort:creation-time'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>
<a href='/tags/query=sort:creation-time'>Created on</a>
<% } %>
</th>
</thead>
<tbody>
<% for (let tag of ctx.response.results) { %>
<% for (let tag of ctx.results) { %>
<tr>
<td class='names'>
<ul>
<% for (let name of tag.names) { %>
<li><%= ctx.makeTagLink(name, false, false, tag) %></li>
<li><%= ctx.makeTagLink(name) %></li>
<% } %>
</ul>
</td>
<td class='implications'>
<% if (tag.implications.length) { %>
<ul>
<% for (let relation of tag.implications) { %>
<li><%= ctx.makeTagLink(relation.names[0], false, false, relation) %></li>
<% for (let name of tag.implications) { %>
<li><%= ctx.makeTagLink(name) %></li>
<% } %>
</ul>
<% } else { %>
@ -62,8 +62,8 @@
<td class='suggestions'>
<% if (tag.suggestions.length) { %>
<ul>
<% for (let relation of tag.suggestions) { %>
<li><%= ctx.makeTagLink(relation.names[0], false, false, relation) %></li>
<% for (let name of tag.suggestions) { %>
<li><%= ctx.makeTagLink(name) %></li>
<% } %>
</ul>
<% } else { %>

View file

@ -1,9 +1,5 @@
<nav id='top-navigation' class='buttons'><!--
--><ul><!--
--><button id="mobile-navigation-toggle"><!--
--><span class="site-name"><%- ctx.name %></span><!--
--><span class="toggle-icon"><i class="fa fa-bars"></i></span><!--
--></button><!--
--><% for (let item of ctx.items) { %><!--
--><% if (item.available) { %><!--
--><li data-name='<%- item.key %>'><!--

View file

@ -2,15 +2,12 @@
<h1><%- ctx.user.name %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('user', ctx.user.name) %>'>Summary</a></li><!--
--><li data-name='summary'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>'>Summary</a></li><!--
--><% if (ctx.canEditAnything) { %><!--
--><li data-name='edit'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'edit') %>'>Settings</a></li><!--
--><% } %><!--
--><% if (ctx.canListTokens) { %><!--
--><li data-name='list-tokens'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'list-tokens') %>'>Login tokens</a></li><!--
--><li data-name='edit'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/edit'>Account settings</a></li><!--
--><% } %><!--
--><% if (ctx.canDelete) { %><!--
--><li data-name='delete'><a href='<%- ctx.formatClientLink('user', ctx.user.name, 'delete') %>'>Delete</a></li><!--
--><li data-name='delete'><a href='/user/<%- encodeURIComponent(ctx.user.name) %>/delete'>Account deletion</a></li><!--
--><% } %><!--
--></ul><!--
--></nav>

View file

@ -1,6 +1,7 @@
<div id='user-delete'>
<form>
<ul class='input'>
<div class='input'>
<ul>
<li>
<%= ctx.makeCheckbox({
name: 'confirm-deletion',
@ -9,7 +10,7 @@
}) %>
</li>
</ul>
</div>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Delete account'/>

View file

@ -4,7 +4,8 @@
<input class='anticomplete' type='text' name='fakeuser'/>
<input class='anticomplete' type='password' name='fakepass'/>
<ul class='input'>
<div class='input'>
<ul>
<li>
<%= ctx.makeTextInput({
text: 'User name',
@ -35,13 +36,12 @@
</p>
</li>
</ul>
</div>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Create an account'/>
</div>
</form>
<div class='info'>
<p>Registered users can:</p>
<ul>
@ -51,6 +51,6 @@
<li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li>
</ul>
<hr/>
<p>By creating an account, you are agreeing to the <a href='<%- ctx.formatClientLink('help', 'tos') %>'>Terms of Service</a>.</p>
<p>By creating an account, you are agreeing to the <a href='/help/tos'>Terms of Service</a>.</p>
</div>
</div>

View file

@ -10,9 +10,9 @@
<nav>
<p><strong>Quick links</strong></p>
<ul>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'submit:' + ctx.user.name}) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'fav:' + ctx.user.name}) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'comment:' + ctx.user.name}) %>'><%- ctx.user.commentCount %> comments</a></li>
<li><a href='/posts/query=submit:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.uploadedPostCount %> uploads</a></li>
<li><a href='/posts/query=fav:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.favoritePostCount %> favorites</a></li>
<li><a href='/posts/query=comment:<%- encodeURIComponent(ctx.user.name) %>'><%- ctx.user.commentCount %> comments</a></li>
</ul>
</nav>
@ -20,8 +20,8 @@
<nav>
<p><strong>Only visible to you</strong></p>
<ul>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:liked'}) %>'><%- ctx.user.likedPostCount %> liked posts</a></li>
<li><a href='<%- ctx.formatClientLink('posts', {query: 'special:disliked'}) %>'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
<li><a href='/posts/query=special:liked'><%- ctx.user.likedPostCount %> liked posts</a></li>
<li><a href='/posts/query=special:disliked'><%- ctx.user.dislikedPostCount %> disliked posts</a></li>
</ul>
</nav>
<% } %>

View file

@ -1,74 +0,0 @@
<div id='user-tokens'>
<div class='messages'></div>
<% if (ctx.tokens.length > 0) { %>
<div class='token-flex-container'>
<% _.each(ctx.tokens, function(token, index) { %>
<div class='token-flex-row'>
<div class='token-flex-column token-flex-labels'>
<div class='token-flex-row'>Token:</div>
<div class='token-flex-row'>Note:</div>
<div class='token-flex-row'>Created:</div>
<div class='token-flex-row'>Expires:</div>
<div class='token-flex-row no-wrap'>Last used:</div>
</div>
<div class='token-flex-column full-width'>
<div class='token-flex-row'><%= token.token %></div>
<div class='token-flex-row'>
<% if (token.note !== null) { %>
<%= token.note %>
<% } else { %>
No note
<% } %>
<a class='token-change-note' data-token-id='<%= index %>' href='#'>(change)</a>
</div>
<div class='token-flex-row'><%= ctx.makeRelativeTime(token.creationTime) %></div>
<div class='token-flex-row'>
<% if (token.expirationTime) { %>
<%= ctx.makeRelativeTime(token.expirationTime) %>
<% } else { %>
No expiration
<% } %>
</div>
<div class='token-flex-row'><%= ctx.makeRelativeTime(token.lastUsageTime) %></div>
</div>
</div>
<div class='token-flex-row'>
<div class='token-flex-column full-width'>
<div class='token-flex-row'>
<form class='token' data-token-id='<%= index %>'>
<% if (token.isCurrentAuthToken) { %>
<input type='submit' value='Delete and logout'
title='This token is used to authenticate this client, deleting it will force a logout.'/>
<% } else { %>
<input type='submit' value='Delete'/>
<% } %>
</form>
</div>
</div>
</div>
<hr/>
<% }); %>
</div>
<% } else { %>
<h2>No Registered Tokens</h2>
<% } %>
<form id='create-token-form'>
<ul class='input'>
<li class='note'>
<%= ctx.makeTextInput({
text: 'Note',
id: 'note',
}) %>
</li>
<li class='expirationTime'>
<%= ctx.makeDateInput({
text: 'Expires',
id: 'expirationTime',
}) %>
</li>
</ul>
<div class='buttons'>
<input type='submit' value='Create token'/>
</div>
</form>
</div>

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