mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
136 Commits
2.5
...
9e4e5a08d6
Author | SHA1 | Date | |
---|---|---|---|
9e4e5a08d6 | |||
ee7e9ef2a3 | |||
aafcfc33bb | |||
d7ffdb0997 | |||
0971745615 | |||
27f94be56d | |||
98227e562e | |||
5244718e0f | |||
ac64c1ca50 | |||
ff788a5e30 | |||
0acc522bfc | |||
a5cf49a94a | |||
374f4a5fa9 | |||
3875ec173f | |||
7708b4e5a3 | |||
64b7b6d0bc | |||
0601c32598 | |||
4792f01362 | |||
cf0a64d832 | |||
2e0dd251b2 | |||
0ff359d613 | |||
769b4f0e22 | |||
7823682b39 | |||
5034121be6 | |||
2ff79a6aa2 | |||
ef48b07966 | |||
1507e6bf2b | |||
d59eac948b | |||
745186062d | |||
63dbff36a0 | |||
c3705f6ee2 | |||
871ebe4083 | |||
650d9784c0 | |||
4f663293c0 | |||
86320fe227 | |||
ae899853d2 | |||
e4f7e9e8e0 | |||
7c12abeffa | |||
c3a4cb6cd1 | |||
75138525e8 | |||
f8dfde9a61 | |||
7586d92db5 | |||
10be19050d | |||
377998fdbc | |||
9b2e1c3064 | |||
b30db8caf8 | |||
c0504692e1 | |||
376f687c38 | |||
4fd848abf2 | |||
61b9f81e39 | |||
b721865931 | |||
46e3295003 | |||
031131506e | |||
d102578b54 | |||
6edb25d87b | |||
93fc15f2a4 | |||
4f9d46e1c2 | |||
b72e81850d | |||
c1c695f082 | |||
4b6b231fc8 | |||
6b0c3cfc7f | |||
4ec8cb3ba2 | |||
8d971234a2 | |||
a16bb198ab | |||
3f182a66ad | |||
b52363e82d | |||
3bf45e4c0a | |||
5596f53744 | |||
da425afc49 | |||
d7394d672f | |||
190d795426 | |||
7c92ceaf6a | |||
9e00f37464 | |||
59c497e168 | |||
c292b96f06 | |||
7a82e9d581 | |||
4806bbe0ed | |||
c2fdc2d070 | |||
ffdf115714 | |||
782f069031 | |||
81f7ae8034 | |||
648121d7c3 | |||
42524503b9 | |||
8a03015349 | |||
2165b59158 | |||
244a0f0b6c | |||
da3b4790ad | |||
196f92593c | |||
d7d2a151a8 | |||
75635bbc43 | |||
e3062b1c77 | |||
e950fe7ea5 | |||
86f50ec742 | |||
8088ff3bbe | |||
da71c672dd | |||
42bb364dd0 | |||
5b43c5bebd | |||
6c3b50d287 | |||
6075ae9326 | |||
70f2164dc6 | |||
1b9ce79f4e | |||
7e5d48b6e8 | |||
e746f09911 | |||
6088e89ea1 | |||
79d0efc25b | |||
929071ea1a | |||
514b846781 | |||
b2582b7b0f | |||
82541536af | |||
8ad9457b24 | |||
6de0a74257 | |||
a22485afda | |||
e2419a30ba | |||
d5a6609f75 | |||
106dcc4135 | |||
a14ead1842 | |||
780b7dc6fd | |||
9f95e9eb90 | |||
9b3123a815 | |||
f3aa0eb801 | |||
98c0941c97 | |||
a5fbaae4b3 | |||
d699979d35 | |||
d083084407 | |||
ad9d3599bc | |||
c3b81371d8 | |||
c64983002e | |||
4f57f49ebe | |||
f58079e12e | |||
be0c867d25 | |||
f5338ca508 | |||
e4a253fd25 | |||
414106a477 | |||
fa4997fbb9 | |||
516b3a51a7 | |||
f4ca435657 |
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts require LF
|
||||
*.sh text eol=lf
|
108
.github/workflows/build-containers.yml
vendored
Normal file
108
.github/workflows/build-containers.yml
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
name: Build Docker containers
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build-client:
|
||||
name: Build and push client/ Docker container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine metadata
|
||||
run: |
|
||||
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
|
||||
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
|
||||
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
|
||||
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
|
||||
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
|
||||
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
|
||||
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
|
||||
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
|
||||
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
|
||||
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
echo "Build Info: ${BUILD_INFO}"
|
||||
echo "Build Date: ${BUILD_DATE}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build container
|
||||
run: >
|
||||
docker buildx build --push
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
--build-arg BUILD_INFO=${{ env.build_info }}
|
||||
--build-arg BUILD_DATE=${{ env.build_date }}
|
||||
--build-arg SOURCE_COMMIT=$GITHUB_SHA
|
||||
--build-arg DOCKER_REPO=szurubooru/client
|
||||
-t "szurubooru/client:latest"
|
||||
-t "szurubooru/client:${{ env.major_tag }}"
|
||||
-t "szurubooru/client:${{ env.minor_tag }}"
|
||||
./client
|
||||
|
||||
build-server:
|
||||
name: Build and push server/ Docker container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine metadata
|
||||
run: |
|
||||
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
|
||||
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
|
||||
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
|
||||
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
|
||||
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
|
||||
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
|
||||
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
|
||||
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
|
||||
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
|
||||
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
|
||||
echo "Build Info: ${BUILD_INFO}"
|
||||
echo "Build Date: ${BUILD_DATE}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build container
|
||||
run: >
|
||||
docker buildx build --push
|
||||
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||
--build-arg BUILD_DATE=${{ env.build_date }}
|
||||
--build-arg SOURCE_COMMIT=$GITHUB_SHA
|
||||
--build-arg DOCKER_REPO=szurubooru/server
|
||||
-t "szurubooru/server:latest"
|
||||
-t "szurubooru/server:${{ env.major_tag }}"
|
||||
-t "szurubooru/server:${{ env.minor_tag }}"
|
||||
./server
|
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Run unit tests
|
||||
on: [push, pull_request]
|
||||
jobs:
|
||||
test-server:
|
||||
name: Run pytest for server/
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Build test container
|
||||
run: >
|
||||
docker buildx build --load
|
||||
--platform linux/amd64 --target testing
|
||||
-t test_container
|
||||
./server
|
||||
|
||||
- name: Run unit tests
|
||||
run: >
|
||||
docker run --rm -t test_container
|
||||
--color=no
|
||||
--cov-report=term-missing:skip-covered
|
||||
--cov=szurubooru
|
||||
szurubooru/
|
@ -1,28 +1,29 @@
|
||||
repos:
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
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.1.9
|
||||
rev: v1.4.2
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: '23.1.0'
|
||||
hooks:
|
||||
- id: black
|
||||
files: 'server/'
|
||||
types: [python]
|
||||
language_version: python3.8
|
||||
language_version: python3.9
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: '5.4.2'
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: '5.12.0'
|
||||
hooks:
|
||||
- id: isort
|
||||
files: 'server/'
|
||||
@ -31,8 +32,8 @@ repos:
|
||||
additional_dependencies:
|
||||
- toml
|
||||
|
||||
- repo: https://github.com/prettier/prettier
|
||||
rev: '2.1.1'
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: client/js/
|
||||
@ -40,7 +41,7 @@ repos:
|
||||
args: ['--config', 'client/.prettierrc.yml']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v7.8.0
|
||||
rev: v8.33.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: client/js/
|
||||
@ -48,8 +49,8 @@ repos:
|
||||
additional_dependencies:
|
||||
- eslint-config-prettier
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: '3.8.3'
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: '6.0.0'
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: server/szurubooru/
|
||||
@ -57,44 +58,5 @@ repos:
|
||||
- flake8-print
|
||||
args: ['--config=server/.flake8']
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: docker-build-client
|
||||
name: Docker - build client
|
||||
entry: bash -c 'docker build client/'
|
||||
language: system
|
||||
types: [file]
|
||||
files: client/
|
||||
pass_filenames: false
|
||||
|
||||
- id: docker-build-server
|
||||
name: Docker - build server
|
||||
entry: bash -c 'docker build server/'
|
||||
language: system
|
||||
types: [file]
|
||||
files: server/
|
||||
pass_filenames: false
|
||||
|
||||
- id: pytest
|
||||
name: pytest
|
||||
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) szurubooru/'
|
||||
language: system
|
||||
types: [python]
|
||||
files: server/szurubooru/
|
||||
exclude: server/szurubooru/migrations/
|
||||
pass_filenames: false
|
||||
stages: [push]
|
||||
|
||||
- id: pytest-cov
|
||||
name: pytest
|
||||
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) --cov-report=term-missing:skip-covered --cov=szurubooru szurubooru/'
|
||||
language: system
|
||||
types: [python]
|
||||
files: server/szurubooru/
|
||||
exclude: server/szurubooru/migrations/
|
||||
pass_filenames: false
|
||||
verbose: true
|
||||
stages: [manual]
|
||||
|
||||
fail_fast: true
|
||||
exclude: LICENSE.md
|
||||
|
@ -3,12 +3,12 @@
|
||||
Szurubooru is an image board engine inspired by services such as Danbooru,
|
||||
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
|
||||
its roots in Polish language and has onomatopeic meaning of scraping or
|
||||
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
|
||||
## Features
|
||||
|
||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
||||
- Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
- 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))
|
||||
|
@ -1,4 +1,5 @@
|
||||
node_modules/*
|
||||
public/
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
**/.gitignore
|
||||
|
@ -1,8 +1,30 @@
|
||||
FROM node:lts as builder
|
||||
FROM --platform=$BUILDPLATFORM node:lts-alpine as development
|
||||
WORKDIR /opt/app
|
||||
|
||||
RUN apk --no-cache add \
|
||||
dumb-init \
|
||||
nginx \
|
||||
git
|
||||
|
||||
RUN ln -sf /opt/app/nginx.conf.docker /etc/nginx/nginx.conf
|
||||
RUN rm -rf /var/www
|
||||
RUN ln -sf /opt/app/public/ /var/www
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install
|
||||
|
||||
ARG BUILD_INFO="docker-development"
|
||||
ENV BUILD_INFO=${BUILD_INFO}
|
||||
ENV BACKEND_HOST="server"
|
||||
|
||||
CMD ["/opt/app/docker-start-dev.sh"]
|
||||
VOLUME ["/data"]
|
||||
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:lts as builder
|
||||
WORKDIR /opt/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install -g npm@lts
|
||||
RUN npm install
|
||||
|
||||
COPY . ./
|
||||
@ -12,7 +34,7 @@ ARG CLIENT_BUILD_ARGS=""
|
||||
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
|
||||
|
||||
|
||||
FROM scratch as approot
|
||||
FROM --platform=$BUILDPLATFORM scratch as approot
|
||||
|
||||
COPY docker-start.sh /
|
||||
|
||||
|
@ -315,7 +315,7 @@ function makeOutputDirs() {
|
||||
}
|
||||
|
||||
function watch() {
|
||||
let wss = new WebSocket.Server({ port: 8080 });
|
||||
let wss = new WebSocket.Server({ port: environment === "development" ? 8081 : 8080 });
|
||||
const liveReload = !process.argv.includes('--no-live-reload');
|
||||
|
||||
function emitReload() {
|
||||
|
@ -36,6 +36,7 @@ $button-disabled-background-color = #CCC
|
||||
$post-thumbnail-border-color = $main-color
|
||||
$post-thumbnail-no-tags-border-color = #F44
|
||||
$default-tag-category-background-color = $active-tab-background-color
|
||||
$default-pool-category-background-color = $active-tab-background-color
|
||||
$new-tag-background-color = #DFC
|
||||
$new-tag-text-color = black
|
||||
$implied-tag-background-color = #FFC
|
||||
|
@ -127,6 +127,10 @@ $comment-border-color = #DDD
|
||||
color: mix($main-color, $inactive-link-color-darktheme)
|
||||
|
||||
.comment-content
|
||||
p
|
||||
word-wrap: normal
|
||||
word-break: break-word
|
||||
|
||||
ul, ol
|
||||
list-style-position: inside
|
||||
margin: 1em 0
|
||||
|
@ -240,6 +240,9 @@ nav
|
||||
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
|
||||
@ -275,7 +278,7 @@ a .access-key
|
||||
background: darken($message-error-background-color, 60%)
|
||||
&.success
|
||||
border: 1px solid darken($message-success-border-color, 30%)
|
||||
background: darken($message-success-background-color, 60%)
|
||||
background: darken($message-success-background-color, 80%)
|
||||
|
||||
.thumbnail
|
||||
/*background-image: attr(data-src url)*/ /* not available yet */
|
||||
|
@ -22,7 +22,7 @@
|
||||
z-index: 1
|
||||
span
|
||||
position: relative
|
||||
background: white
|
||||
background: $window-color
|
||||
padding: 0 1em
|
||||
z-index: 2
|
||||
|
||||
@ -31,3 +31,5 @@
|
||||
.page-header
|
||||
&:before
|
||||
background: $top-navigation-color-darktheme
|
||||
span
|
||||
background: $window-color-darktheme
|
||||
|
@ -1,47 +1,100 @@
|
||||
@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
|
||||
ul
|
||||
list-style-type: none
|
||||
padding: 0
|
||||
display: flex
|
||||
align-content: flex-end
|
||||
flex-wrap: wrap
|
||||
margin: 0 -0.25em
|
||||
|
||||
.darktheme .pool-list
|
||||
table
|
||||
tr:hover td
|
||||
background: $top-navigation-color-darktheme
|
||||
th
|
||||
background: $top-navigation-color-darktheme
|
||||
li
|
||||
position: relative
|
||||
flex-grow: 1
|
||||
margin: 2em 1.5em 2em 1.2em
|
||||
display: inline-block
|
||||
text-align: left
|
||||
min-width: 10em
|
||||
width: 12vw
|
||||
&:not(.flexbox-dummy)
|
||||
min-height: 7.5em
|
||||
height: 9vw
|
||||
|
||||
.thumbnail-wrapper
|
||||
display: inline-block
|
||||
width: 100%
|
||||
height: 100%
|
||||
line-height: 80%
|
||||
font-size: 80%
|
||||
color: white
|
||||
outline: none
|
||||
border-right: 20px solid transparent
|
||||
&:before
|
||||
content: ' '
|
||||
display: block
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 20px
|
||||
bottom: 20px
|
||||
|
||||
.thumbnail
|
||||
width: 100%
|
||||
height: 100%
|
||||
outline-offset: -2px
|
||||
background-size: cover
|
||||
transition: top .1s ease-in-out, right .1s ease-in-out
|
||||
background-position: 50% 30%
|
||||
position: absolute
|
||||
display: inline-block
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.2)
|
||||
|
||||
.thumbnail-1, .thumbnail.empty
|
||||
right: -4px
|
||||
top: -4px
|
||||
z-index: 30
|
||||
|
||||
.thumbnail-2
|
||||
right: -10px
|
||||
top: -10px
|
||||
z-index: 20
|
||||
|
||||
.thumbnail-3
|
||||
right: -16px
|
||||
top: -16px
|
||||
z-index: 10
|
||||
|
||||
.pool-name
|
||||
color: black
|
||||
font-size: 1em
|
||||
text-align: center
|
||||
a
|
||||
width: 100%
|
||||
display: inline-block
|
||||
|
||||
a:active, a:focus
|
||||
.thumbnail
|
||||
outline: 2px solid $main-color !important
|
||||
|
||||
.pool-list ul li:hover
|
||||
.thumbnail-wrapper
|
||||
.thumbnail-1
|
||||
right: -0px
|
||||
top: -0px
|
||||
|
||||
.thumbnail-3
|
||||
right: -20px
|
||||
top: -20px
|
||||
|
||||
.pool-list ul li:has(a:focus), .pool-list ul li:has(a:active)
|
||||
.thumbnail-wrapper
|
||||
.thumbnail-1
|
||||
right: -0px
|
||||
top: -0px
|
||||
|
||||
.thumbnail-3
|
||||
right: -20px
|
||||
top: -20px
|
||||
|
||||
.pool-list-header
|
||||
label
|
||||
@ -61,3 +114,21 @@
|
||||
.darktheme .pool-list-header
|
||||
.append
|
||||
color: $inactive-link-color-darktheme
|
||||
|
||||
.post-flow
|
||||
ul
|
||||
li
|
||||
min-width: inherit
|
||||
width: inherit
|
||||
margin: 0 0.25em 0.5em 0.25em
|
||||
&:not(.flexbox-dummy)
|
||||
height: 14vw
|
||||
.thumbnail
|
||||
position: static
|
||||
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
|
||||
|
39
client/css/pool-navigator-control.styl
Normal file
39
client/css/pool-navigator-control.styl
Normal file
@ -0,0 +1,39 @@
|
||||
@import colors
|
||||
|
||||
.pool-navigator-container
|
||||
padding: 0
|
||||
margin: 0 auto
|
||||
|
||||
.pool-info-wrapper
|
||||
box-sizing: border-box
|
||||
width: 100%
|
||||
margin: 0 0 1em 0
|
||||
display: flex
|
||||
padding: 0.5em 1em
|
||||
border: 1px solid $line-color
|
||||
background: $top-navigation-color
|
||||
|
||||
.pool-name
|
||||
flex: 1 1
|
||||
text-align: center
|
||||
overflow: hidden
|
||||
white-space: nowrap
|
||||
-o-text-overflow: ellipsis
|
||||
text-overflow: ellipsis
|
||||
|
||||
.first, .last
|
||||
flex-basis: 1em
|
||||
|
||||
.first, .prev, .next, .last
|
||||
flex: 0 1
|
||||
white-space: nowrap
|
||||
|
||||
>span
|
||||
padding-top: 2px
|
||||
padding-bottom: 2px
|
||||
margin: 0 .25em
|
||||
|
||||
|
||||
.darktheme .pool-navigator-container .pool-info-wrapper
|
||||
border: 1px solid $top-navigation-color-darktheme
|
||||
background: $window-color-darktheme
|
9
client/css/pool-navigator-list.styl
Normal file
9
client/css/pool-navigator-list.styl
Normal file
@ -0,0 +1,9 @@
|
||||
.pool-navigators>ul
|
||||
list-style-type: none
|
||||
margin: 0
|
||||
padding: 0
|
||||
|
||||
>li
|
||||
margin-bottom: 1em
|
||||
&:last-child
|
||||
margin-bottom: 0
|
@ -114,6 +114,29 @@
|
||||
&[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
|
||||
width: 100%
|
||||
@ -164,6 +187,9 @@
|
||||
vertical-align: top
|
||||
@media (max-width: 1000px)
|
||||
display: block
|
||||
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
|
||||
float: left
|
||||
margin-right: 1em
|
||||
input
|
||||
margin-bottom: 0.25em
|
||||
margin-right: 0.25em
|
||||
@ -215,7 +241,19 @@
|
||||
.append
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
|
||||
.bulk-edit-delete
|
||||
&.opened
|
||||
.start
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
&:not(.opened)
|
||||
.start
|
||||
display: none
|
||||
.append.open
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
.start
|
||||
margin-left: 1em
|
||||
.safety
|
||||
margin-right: 0.25em
|
||||
&.safety-safe
|
||||
|
@ -15,39 +15,53 @@
|
||||
border: 0
|
||||
outline: 0
|
||||
|
||||
nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear
|
||||
&:not(.inactive):hover
|
||||
background: lighten($main-color, 90%)
|
||||
i
|
||||
font-size: 140%
|
||||
>.sidebar>nav.buttons, >.content nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
article
|
||||
flex: 1 0 33%
|
||||
a
|
||||
display: inline-block
|
||||
width: 100%
|
||||
padding: 0.3em 0
|
||||
text-align: center
|
||||
@media (max-width: 800px)
|
||||
margin-top: 2em
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear, box-shadow 0.2s linear
|
||||
&:not(.inactive):hover
|
||||
background: lighten($main-color, 90%)
|
||||
i
|
||||
font-size: 140%
|
||||
text-align: center
|
||||
@media (max-width: 800px)
|
||||
margin-top: 0.6em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
>.content
|
||||
width: 100%
|
||||
|
||||
.post-container
|
||||
margin-bottom: 2em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
.post-content
|
||||
margin: 0
|
||||
|
||||
.after-mobile-controls
|
||||
width: 100%
|
||||
|
||||
.darktheme .post-view
|
||||
>.sidebar, >.content
|
||||
nav.buttons
|
||||
article
|
||||
a:not(.inactive):hover
|
||||
background: unset
|
||||
box-shadow: inset 0 0 0 0.3em $main-color
|
||||
|
||||
@media (max-width: 800px)
|
||||
.post-view
|
||||
flex-wrap: wrap
|
||||
>.after-mobile-controls
|
||||
order: 3
|
||||
>.sidebar
|
||||
order: 2
|
||||
min-width: 100%
|
||||
@ -105,7 +119,6 @@
|
||||
h1
|
||||
margin-bottom: 0.5em
|
||||
.thumbnail
|
||||
background-position: 50% 30%
|
||||
width: 4em
|
||||
height: 3em
|
||||
li
|
||||
|
@ -13,8 +13,12 @@ $cancel-button-color = tomato
|
||||
|
||||
&.inactive input[type=submit],
|
||||
&.inactive .skip-duplicates
|
||||
&.inactive .always-upload-similar
|
||||
&.inactive .pause-remain-on-error
|
||||
&.uploading input[type=submit],
|
||||
&.uploading .skip-duplicates,
|
||||
&.uploading .always-upload-similar
|
||||
&.uploading .pause-remain-on-error
|
||||
&:not(.uploading) .cancel
|
||||
display: none
|
||||
|
||||
@ -39,6 +43,12 @@ $cancel-button-color = tomato
|
||||
.skip-duplicates
|
||||
margin-left: 1em
|
||||
|
||||
.always-upload-similar
|
||||
margin-left: 1em
|
||||
|
||||
.pause-remain-on-error
|
||||
margin-left: 1em
|
||||
|
||||
form>.messages
|
||||
margin-top: 1em
|
||||
|
||||
@ -52,6 +62,14 @@ $cancel-button-color = tomato
|
||||
margin: 0 0 1.2em 0
|
||||
padding-left: 13em
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
video
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
&>.thumbnail-wrapper
|
||||
float: left
|
||||
width: 12em
|
||||
|
@ -86,6 +86,12 @@ div.tag-input
|
||||
font-size: 90%
|
||||
unselectable()
|
||||
|
||||
@keyframes tag-added-to-post
|
||||
from
|
||||
max-height: 0
|
||||
to
|
||||
max-height: 5em
|
||||
|
||||
ul.compact-tags
|
||||
width: 100%
|
||||
margin: 0.5em 0 0 0
|
||||
@ -103,18 +109,30 @@ ul.compact-tags
|
||||
a:focus
|
||||
outline: 0
|
||||
box-shadow: inset 0 0 0 2px $main-color
|
||||
// these 3 added when tag is added to ul
|
||||
&.added, &.new, &.implication
|
||||
animation: tag-added-to-post 1s ease forwards
|
||||
&.implication
|
||||
background: $implied-tag-background-color
|
||||
color: $implied-tag-text-color
|
||||
background-color: $implied-tag-background-color
|
||||
&.new
|
||||
background: $new-tag-background-color
|
||||
color: $new-tag-text-color
|
||||
background-color: $new-tag-background-color
|
||||
&.duplicate
|
||||
background: $duplicate-tag-background-color
|
||||
color: $duplicate-tag-text-color
|
||||
background-color: $duplicate-tag-background-color
|
||||
i
|
||||
padding-right: 0.4em
|
||||
|
||||
.darktheme ul.compact-tags
|
||||
li
|
||||
&.new
|
||||
background-color: darken($new-tag-background-color, 80%)
|
||||
&.implication
|
||||
background-color: darken($implied-tag-background-color, 85%)
|
||||
&.duplicate
|
||||
background-color: darken($duplicate-tag-background-color, 80%)
|
||||
|
||||
div.tag-input, ul.compact-tags
|
||||
.tag-usages, .tag-weight, .remove-tag
|
||||
color: $inactive-link-color
|
||||
@ -134,6 +152,8 @@ div.tag-input, ul.compact-tags
|
||||
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
|
||||
|
@ -21,10 +21,11 @@
|
||||
.details
|
||||
font-size: 90%
|
||||
line-height: 130%
|
||||
.image
|
||||
margin: 0.25em 0.6em 0.25em 0
|
||||
.thumbnail
|
||||
width: 3em
|
||||
height: 3em
|
||||
margin: 0.25em 0.6em 0 0
|
||||
|
||||
.darktheme .user-list
|
||||
ul li
|
||||
|
17
client/docker-start-dev.sh
Executable file
17
client/docker-start-dev.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/dumb-init /bin/sh
|
||||
|
||||
# Integrate environment variables
|
||||
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||
/etc/nginx/nginx.conf
|
||||
|
||||
# Start server
|
||||
nginx &
|
||||
|
||||
# Watch source for changes and build app
|
||||
# FIXME: It's not ergonomic to run `npm i` outside of the build step.
|
||||
# However, the mounting of different directories into the
|
||||
# client container's /opt/app causes node_modules to disappear
|
||||
# (the mounting causes client/Dockerfile's RUN npm install
|
||||
# to silently clobber).
|
||||
# Find a way to move `npm i` into client/Dockerfile.
|
||||
npm i && npm run watch -- --polling
|
@ -2,10 +2,10 @@
|
||||
|
||||
# Integrate environment variables
|
||||
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
|
||||
/etc/nginx/nginx.conf
|
||||
/etc/nginx/nginx.conf
|
||||
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
|
||||
/var/www/index.htm \
|
||||
/var/www/manifest.json
|
||||
/var/www/index.htm \
|
||||
/var/www/manifest.json
|
||||
|
||||
# Start server
|
||||
exec nginx
|
||||
|
@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
CLOSEST_VER=$(git describe --tags --abbrev=0 ${SOURCE_COMMIT})
|
||||
if git describe --exact-match --abbrev=0 ${SOURCE_COMMIT} 2> /dev/null; then
|
||||
BUILD_INFO="v${CLOSEST_VER}"
|
||||
else
|
||||
BUILD_INFO="v${CLOSEST_VER}-edge-$(git rev-parse --short ${SOURCE_COMMIT})"
|
||||
fi
|
||||
|
||||
echo "Using BUILD_INFO=${BUILD_INFO}"
|
||||
docker build \
|
||||
--build-arg BUILD_INFO=${BUILD_INFO} \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg SOURCE_COMMIT \
|
||||
--build-arg DOCKER_REPO \
|
||||
-f $DOCKERFILE_PATH -t $IMAGE_NAME .
|
@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
add_tag() {
|
||||
echo "Also tagging image as ${DOCKER_REPO}:${1}"
|
||||
docker tag $IMAGE_NAME $DOCKER_REPO:$1
|
||||
docker push $DOCKER_REPO:$1
|
||||
}
|
||||
|
||||
CLOSEST_VER=$(git describe --tags --abbrev=0)
|
||||
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
|
||||
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
|
||||
|
||||
add_tag "${CLOSEST_MAJOR_VER}-edge"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
|
||||
|
||||
if git describe --exact-match --abbrev=0 2> /dev/null; then
|
||||
add_tag "${CLOSEST_MAJOR_VER}"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
|
||||
fi
|
@ -329,6 +329,10 @@
|
||||
<td><code>feature-time</code></td>
|
||||
<td>alias of <code>feature-time</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pool</code></td>
|
||||
<td>pool order, requires pool named token</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<meta name='theme-color' content='#24aadd'/>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<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>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
|
||||
<ul class='input'>
|
||||
<li>
|
||||
|
49
client/html/pool_navigator.tpl
Normal file
49
client/html/pool_navigator.tpl
Normal file
@ -0,0 +1,49 @@
|
||||
<div class='pool-navigator-container'>
|
||||
<div class='pool-info-wrapper'>
|
||||
<span class='first'>
|
||||
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.firstPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
«
|
||||
<% if (ctx.canViewPosts && ctx.previousPost && ctx.firstPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='prev'>
|
||||
<% if (ctx.canViewPosts && ctx.previousPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.previousPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
‹ prev
|
||||
<% if (ctx.canViewPosts && ctx.previousPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='pool-name'>
|
||||
<% if (ctx.canViewPools) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.formatClientLink("pool", ctx.pool.id) %>'>
|
||||
<% } %>
|
||||
Pool: <%- ctx.getPrettyName(ctx.pool.names[0]) %>
|
||||
<% if (ctx.canViewPools) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='next'>
|
||||
<% if (ctx.canViewPosts && ctx.nextPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.nextPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
next ›
|
||||
<% if (ctx.canViewPosts && ctx.nextPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
<span class='last'>
|
||||
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
|
||||
<a class='<%- ctx.linkClass %>' href='<%= ctx.getPostUrl(ctx.lastPost.id, ctx.parameters) %>'>
|
||||
<% } %>
|
||||
»
|
||||
<% if (ctx.canViewPosts && ctx.nextPost && ctx.lastPost) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
4
client/html/pool_navigator_list.tpl
Normal file
4
client/html/pool_navigator_list.tpl
Normal file
@ -0,0 +1,4 @@
|
||||
<div class='pool-navigators'>
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
@ -18,6 +18,6 @@
|
||||
<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>
|
||||
<p>This pool has <a href='<%- ctx.formatClientLink('posts', {query: 'pool:' + ctx.pool.id + ' -sort:pool'}) %>'><%- ctx.pool.postCount %> post(s)</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -1,48 +1,19 @@
|
||||
<div class='pool-list table-wrap'>
|
||||
<% if (ctx.postFlow) { %><div class='pool-list post-flow'><% } else { %><div class='pool-list'><% } %>
|
||||
<% 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>
|
||||
<ul>
|
||||
<% for (let pool of ctx.response.results) { %>
|
||||
<li data-pool-id='<%= pool.id %>'>
|
||||
<a class='thumbnail-wrapper' href='<%= ctx.canViewPools ? ctx.formatClientLink("pool", pool.id) : "" %>'>
|
||||
<% if (ctx.canViewPosts) { %>
|
||||
<%= ctx.makePoolThumbnails(pool.posts, ctx.postFlow) %>
|
||||
<% } %>
|
||||
</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>
|
||||
</a>
|
||||
<div class='pool-name'>
|
||||
<%= ctx.makePoolLink(pool.id, false, false, pool, name) %>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
<%= ctx.makeFlexboxAlign() %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
|
@ -29,6 +29,7 @@
|
||||
<span class='vim-nav-hint'>Next post ></span>
|
||||
</a>
|
||||
</article>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<article class='edit-post'>
|
||||
<% if (ctx.editMode) { %>
|
||||
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
@ -36,16 +37,13 @@
|
||||
<span class='vim-nav-hint'>Back to view mode</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<% } else { %>
|
||||
<a class='inactive'>
|
||||
<% } %>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</article>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<div class='sidebar-container'></div>
|
||||
@ -54,13 +52,19 @@
|
||||
<div class='content'>
|
||||
<div class='post-container'></div>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% if (ctx.canListPools && ctx.canViewPools) { %>
|
||||
<div class='pool-navigators-container'></div>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-container'></div>
|
||||
<% } %>
|
||||
<div class='after-mobile-controls'>
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-container'></div>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,6 +42,7 @@
|
||||
'image/heic': 'HEIC',
|
||||
'video/webm': 'WEBM',
|
||||
'video/mp4': 'MPEG-4',
|
||||
'video/quicktime': 'MOV',
|
||||
'application/x-shockwave-flash': 'SWF',
|
||||
}[ctx.post.mimeType] +
|
||||
' (' +
|
||||
|
@ -15,9 +15,10 @@
|
||||
'image/heic': 'HEIC',
|
||||
'video/webm': 'WEBM',
|
||||
'video/mp4': 'MPEG-4',
|
||||
'video/quicktime': 'MOV',
|
||||
'application/x-shockwave-flash': 'SWF',
|
||||
}[ctx.post.mimeType] %>
|
||||
</a>
|
||||
}[ctx.post.mimeType] %><!--
|
||||
--></a>
|
||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||
<% if (ctx.post.flags.length) { %><!--
|
||||
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||
@ -57,7 +58,7 @@
|
||||
Search on
|
||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
||||
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
</section>
|
||||
|
||||
<section class='social'>
|
||||
@ -98,10 +99,10 @@
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||
--><% } %><!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %> <!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %><!--
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--></a><!--
|
||||
--><% } %><!--
|
||||
--><% } %> <!--
|
||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||
--></li><!--
|
||||
--><% } %><!--
|
||||
|
@ -7,12 +7,28 @@
|
||||
|
||||
<span class='skip-duplicates'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Skip duplicates',
|
||||
text: 'Skip duplicate',
|
||||
name: 'skip-duplicates',
|
||||
checked: false,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<span class='always-upload-similar'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Force upload similar',
|
||||
name: 'always-upload-similar',
|
||||
checked: false,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<span class='pause-remain-on-error'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Pause on error',
|
||||
name: 'pause-remain-on-error',
|
||||
checked: true,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<input type='button' value='Cancel' class='cancel'/>
|
||||
</div>
|
||||
|
||||
|
@ -61,6 +61,7 @@
|
||||
text: 'Upload anonymously',
|
||||
name: 'anonymous',
|
||||
checked: ctx.uploadable.anonymous,
|
||||
readonly: ctx.uploadable.forceAnonymous,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
@ -16,7 +16,6 @@
|
||||
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||
%><span class='append hint'>Tagging with:</span><%
|
||||
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||
%><wbr/><%
|
||||
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||
@ -28,4 +27,11 @@
|
||||
%><a href class='mousetrap button append close'>Stop editing safety</a><%
|
||||
%></form><%
|
||||
%><% } %><%
|
||||
%><% if (ctx.canBulkDelete) { %><%
|
||||
%><form class='horizontal bulk-edit bulk-edit-delete'><%
|
||||
%><a href class='mousetrap button append open'>Mass delete</a><%
|
||||
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
|
||||
%><a href class='mousetrap button append close'>Stop deleting</a><%
|
||||
%></form><%
|
||||
%><% } %><%
|
||||
%></div>
|
||||
|
@ -50,6 +50,10 @@
|
||||
<% } %>
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
|
||||
<a href class='delete-flipper'>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
|
@ -17,7 +17,7 @@ class HomeController {
|
||||
buildDate: config.meta.buildDate,
|
||||
canListSnapshots: api.hasPrivilege("snapshots:list"),
|
||||
canListPosts: api.hasPrivilege("posts:list"),
|
||||
isDevelopmentMode: config.environment == "development"
|
||||
isDevelopmentMode: config.environment == "development",
|
||||
});
|
||||
|
||||
Info.get().then(
|
||||
|
@ -91,16 +91,16 @@ class PoolController {
|
||||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.pool.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.pool.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.pool.description = e.detail.description;
|
||||
}
|
||||
if (e.detail.posts !== undefined) {
|
||||
if (e.detail.posts !== undefined && e.detail.posts !== null) {
|
||||
e.detail.pool.posts.clear();
|
||||
for (let postId of e.detail.posts) {
|
||||
e.detail.pool.posts.add(
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const router = require("../router.js");
|
||||
const api = require("../api.js");
|
||||
const settings = require("../models/settings.js");
|
||||
const uri = require("../util/uri.js");
|
||||
const PoolList = require("../models/pool_list.js");
|
||||
const topNavigation = require("../models/top_navigation.js");
|
||||
@ -13,7 +14,6 @@ const EmptyView = require("../views/empty_view.js");
|
||||
const fields = [
|
||||
"id",
|
||||
"names",
|
||||
"posts",
|
||||
"creationTime",
|
||||
"postCount",
|
||||
"category",
|
||||
@ -43,6 +43,8 @@ class PoolListController {
|
||||
this._headerView.addEventListener(
|
||||
"submit",
|
||||
(e) => this._evtSubmit(e),
|
||||
);
|
||||
this._headerView.addEventListener(
|
||||
"navigate",
|
||||
(e) => this._evtNavigate(e)
|
||||
);
|
||||
@ -98,14 +100,21 @@ class PoolListController {
|
||||
return uri.formatClientLink("pools", parameters);
|
||||
},
|
||||
requestPage: (offset, limit) => {
|
||||
const canEditPosts = api.hasPrivilege("pools:edit") || api.hasPrivilege("pools:edit:posts");
|
||||
const effectiveFields = fields.concat([canEditPosts ? "posts": "postsMicro"]);
|
||||
return PoolList.search(
|
||||
this._ctx.parameters.query,
|
||||
offset,
|
||||
limit,
|
||||
fields
|
||||
effectiveFields
|
||||
);
|
||||
},
|
||||
pageRenderer: (pageCtx) => {
|
||||
Object.assign(pageCtx, {
|
||||
canViewPosts: api.hasPrivilege("posts:view"),
|
||||
canViewPools: api.hasPrivilege("pools:view"),
|
||||
postFlow: settings.get().postFlow,
|
||||
});
|
||||
return new PoolsPageView(pageCtx);
|
||||
},
|
||||
});
|
||||
|
@ -44,6 +44,7 @@ class PostListController {
|
||||
enableSafety: api.safetyEnabled(),
|
||||
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
|
||||
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
|
||||
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||
bulkEdit: {
|
||||
tags: this._bulkEditTags,
|
||||
},
|
||||
@ -52,6 +53,16 @@ class PostListController {
|
||||
this._evtNavigate(e)
|
||||
);
|
||||
|
||||
if (this._headerView._bulkDeleteEditor) {
|
||||
this._headerView._bulkDeleteEditor.addEventListener(
|
||||
"deleteSelectedPosts",
|
||||
(e) => {
|
||||
this._evtDeleteSelectedPosts(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this._postsMarkedForDeletion = [];
|
||||
this._syncPageController();
|
||||
}
|
||||
|
||||
@ -91,6 +102,38 @@ class PostListController {
|
||||
e.detail.post.save().catch((error) => window.alert(error.message));
|
||||
}
|
||||
|
||||
_evtMarkForDeletion(e) {
|
||||
const postId = e.detail;
|
||||
|
||||
// Add or remove post from delete list
|
||||
if (e.detail.delete) {
|
||||
this._postsMarkedForDeletion.push(e.detail.post);
|
||||
} else {
|
||||
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
|
||||
(x) => x.id != e.detail.post.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_evtDeleteSelectedPosts(e) {
|
||||
if (this._postsMarkedForDeletion.length == 0) return;
|
||||
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
|
||||
)
|
||||
) {
|
||||
Promise.all(
|
||||
this._postsMarkedForDeletion.map((post) => post.delete())
|
||||
)
|
||||
.catch((error) => window.alert(error.message))
|
||||
.then(() => {
|
||||
this._postsMarkedForDeletion = [];
|
||||
this._headerView._navigate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_syncPageController() {
|
||||
this._pageController.run({
|
||||
parameters: this._ctx.parameters,
|
||||
@ -117,8 +160,10 @@ class PostListController {
|
||||
canBulkEditSafety: api.hasPrivilege(
|
||||
"posts:bulk-edit:safety"
|
||||
),
|
||||
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||
bulkEdit: {
|
||||
tags: this._bulkEditTags,
|
||||
markedForDeletion: this._postsMarkedForDeletion,
|
||||
},
|
||||
postFlow: settings.get().postFlow,
|
||||
});
|
||||
@ -128,6 +173,9 @@ class PostListController {
|
||||
view.addEventListener("changeSafety", (e) =>
|
||||
this._evtChangeSafety(e)
|
||||
);
|
||||
view.addEventListener("markForDeletion", (e) =>
|
||||
this._evtMarkForDeletion(e)
|
||||
);
|
||||
return view;
|
||||
},
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ const PostList = require("../models/post_list.js");
|
||||
const PostMainView = require("../views/post_main_view.js");
|
||||
const BasePostController = require("./base_post_controller.js");
|
||||
const EmptyView = require("../views/empty_view.js");
|
||||
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
|
||||
|
||||
class PostMainController extends BasePostController {
|
||||
constructor(ctx, editMode) {
|
||||
@ -26,6 +27,7 @@ class PostMainController extends BasePostController {
|
||||
]).then(
|
||||
(responses) => {
|
||||
const [post, aroundResponse] = responses;
|
||||
let aroundPool = null;
|
||||
|
||||
// remove junk from query, but save it into history so that it can
|
||||
// be still accessed after history navigation / page refresh
|
||||
@ -39,23 +41,36 @@ class PostMainController extends BasePostController {
|
||||
)
|
||||
: uri.formatClientLink("post", ctx.parameters.id);
|
||||
router.replace(url, ctx.state, false);
|
||||
misc.splitByWhitespace(parameters.query).forEach((item) => {
|
||||
const found = item.match(/^pool:([0-9]+)/i);
|
||||
if (found) {
|
||||
const activePool = parseInt(found[1]);
|
||||
post.pools.map((pool) => {
|
||||
if (pool.id == activePool) {
|
||||
aroundPool = pool;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this._post = post;
|
||||
this._view = new PostMainView({
|
||||
post: post,
|
||||
editMode: editMode,
|
||||
prevPostId: aroundResponse.prev
|
||||
? aroundResponse.prev.id
|
||||
: null,
|
||||
nextPostId: aroundResponse.next
|
||||
? aroundResponse.next.id
|
||||
: null,
|
||||
prevPostId: aroundPool
|
||||
? (aroundPool.previousPost ? aroundPool.previousPost.id : null)
|
||||
: (aroundResponse.prev ? aroundResponse.prev.id : null),
|
||||
nextPostId: aroundPool
|
||||
? (aroundPool.nextPost ? aroundPool.nextPost.id : null)
|
||||
: (aroundResponse.next ? aroundResponse.next.id : null),
|
||||
canEditPosts: api.hasPrivilege("posts:edit"),
|
||||
canDeletePosts: api.hasPrivilege("posts:delete"),
|
||||
canFeaturePosts: api.hasPrivilege("posts:feature"),
|
||||
canListComments: api.hasPrivilege("comments:list"),
|
||||
canCreateComments: api.hasPrivilege("comments:create"),
|
||||
canListPools: api.hasPrivilege("pools:list"),
|
||||
canViewPools: api.hasPrivilege("pools:view"),
|
||||
parameters: parameters,
|
||||
});
|
||||
|
||||
@ -169,22 +184,22 @@ class PostMainController extends BasePostController {
|
||||
this._view.sidebarControl.disableForm();
|
||||
this._view.sidebarControl.clearMessages();
|
||||
const post = e.detail.post;
|
||||
if (e.detail.safety !== undefined) {
|
||||
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||
post.safety = e.detail.safety;
|
||||
}
|
||||
if (e.detail.flags !== undefined) {
|
||||
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||
post.flags = e.detail.flags;
|
||||
}
|
||||
if (e.detail.relations !== undefined) {
|
||||
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||
post.relations = e.detail.relations;
|
||||
}
|
||||
if (e.detail.content !== undefined) {
|
||||
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||
post.newContent = e.detail.content;
|
||||
}
|
||||
if (e.detail.thumbnail !== undefined) {
|
||||
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||
post.newThumbnail = e.detail.thumbnail;
|
||||
}
|
||||
if (e.detail.source !== undefined) {
|
||||
if (e.detail.source !== undefined && e.detail.source !== null) {
|
||||
post.source = e.detail.source;
|
||||
}
|
||||
post.save().then(
|
||||
|
@ -12,7 +12,7 @@ const PostUploadView = require("../views/post_upload_view.js");
|
||||
const EmptyView = require("../views/empty_view.js");
|
||||
|
||||
const genericErrorMessage =
|
||||
"One of the posts needs your attention; " +
|
||||
"One or more posts needs your attention; " +
|
||||
'click "resume upload" when you\'re ready.';
|
||||
|
||||
class PostUploadController {
|
||||
@ -55,6 +55,7 @@ class PostUploadController {
|
||||
_evtSubmit(e) {
|
||||
this._view.disableForm();
|
||||
this._view.clearMessages();
|
||||
let anyFailures = false;
|
||||
|
||||
e.detail.uploadables
|
||||
.reduce(
|
||||
@ -62,11 +63,45 @@ class PostUploadController {
|
||||
promise.then(() =>
|
||||
this._uploadSinglePost(
|
||||
uploadable,
|
||||
e.detail.skipDuplicates
|
||||
)
|
||||
e.detail.skipDuplicates,
|
||||
e.detail.alwaysUploadSimilar
|
||||
).catch((error) => {
|
||||
anyFailures = true;
|
||||
if (error.uploadable) {
|
||||
if (error.similarPosts) {
|
||||
error.uploadable.lookalikes =
|
||||
error.similarPosts;
|
||||
this._view.updateUploadable(
|
||||
error.uploadable
|
||||
);
|
||||
this._view.showInfo(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
} else {
|
||||
this._view.showError(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._view.showError(
|
||||
error.message,
|
||||
uploadable
|
||||
);
|
||||
}
|
||||
if (e.detail.pauseRemainOnError) {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
),
|
||||
Promise.resolve()
|
||||
)
|
||||
.then(() => {
|
||||
if (anyFailures) {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
this._view.clearMessages();
|
||||
@ -75,31 +110,13 @@ class PostUploadController {
|
||||
ctx.controller.showSuccess("Posts uploaded.");
|
||||
},
|
||||
(error) => {
|
||||
if (error.uploadable) {
|
||||
if (error.similarPosts) {
|
||||
error.uploadable.lookalikes = error.similarPosts;
|
||||
this._view.updateUploadable(error.uploadable);
|
||||
this._view.showInfo(genericErrorMessage);
|
||||
this._view.showInfo(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
} else {
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.showError(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.enableForm();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_uploadSinglePost(uploadable, skipDuplicates) {
|
||||
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
|
||||
progress.start();
|
||||
let reverseSearchPromise = Promise.resolve();
|
||||
if (!uploadable.lookalikesConfirmed) {
|
||||
@ -128,7 +145,10 @@ class PostUploadController {
|
||||
}
|
||||
|
||||
// notify about similar posts
|
||||
if (searchResult.similarPosts.length) {
|
||||
if (
|
||||
searchResult.similarPosts.length &&
|
||||
!alwaysUploadSimilar
|
||||
) {
|
||||
let error = new Error(
|
||||
`Found ${searchResult.similarPosts.length} similar ` +
|
||||
"posts.\nYou can resume or discard this upload."
|
||||
|
@ -95,13 +95,13 @@ class TagController {
|
||||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.tag.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.tag.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.tag.description = e.detail.description;
|
||||
}
|
||||
e.detail.tag.save().then(
|
||||
|
@ -31,9 +31,8 @@ class UserController {
|
||||
userTokenPromise = UserToken.get(userName).then(
|
||||
(userTokens) => {
|
||||
return userTokens.map((token) => {
|
||||
token.isCurrentAuthToken = api.isCurrentAuthToken(
|
||||
token
|
||||
);
|
||||
token.isCurrentAuthToken =
|
||||
api.isCurrentAuthToken(token);
|
||||
return token;
|
||||
});
|
||||
},
|
||||
@ -176,21 +175,21 @@ class UserController {
|
||||
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
||||
const infix = isLoggedIn ? "self" : "any";
|
||||
|
||||
if (e.detail.name !== undefined) {
|
||||
if (e.detail.name !== undefined && e.detail.name !== null) {
|
||||
e.detail.user.name = e.detail.name;
|
||||
}
|
||||
if (e.detail.email !== undefined) {
|
||||
if (e.detail.email !== undefined && e.detail.email !== null) {
|
||||
e.detail.user.email = e.detail.email;
|
||||
}
|
||||
if (e.detail.rank !== undefined) {
|
||||
if (e.detail.rank !== undefined && e.detail.rank !== null) {
|
||||
e.detail.user.rank = e.detail.rank;
|
||||
}
|
||||
|
||||
if (e.detail.password !== undefined) {
|
||||
if (e.detail.password !== undefined && e.detail.password !== null) {
|
||||
e.detail.user.password = e.detail.password;
|
||||
}
|
||||
|
||||
if (e.detail.avatarStyle !== undefined) {
|
||||
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
|
||||
e.detail.user.avatarStyle = e.detail.avatarStyle;
|
||||
if (e.detail.avatarContent) {
|
||||
e.detail.user.avatarContent = e.detail.avatarContent;
|
||||
@ -303,7 +302,7 @@ class UserController {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
|
||||
if (e.detail.note !== undefined) {
|
||||
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||
e.detail.userToken.note = e.detail.note;
|
||||
}
|
||||
|
||||
|
@ -45,9 +45,8 @@ class ExpanderControl {
|
||||
// eslint-disable-next-line accessor-pairs
|
||||
set title(newTitle) {
|
||||
if (this._expanderNode) {
|
||||
this._expanderNode.querySelector(
|
||||
"header span"
|
||||
).textContent = newTitle;
|
||||
this._expanderNode.querySelector("header span").textContent =
|
||||
newTitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +48,12 @@ class FileDropperControl extends events.EventTarget {
|
||||
this._urlInputNode.addEventListener("keydown", (e) =>
|
||||
this._evtUrlInputKeyDown(e)
|
||||
);
|
||||
this._urlInputNode.addEventListener("paste", (e) => {
|
||||
// document.onpaste is used on the post-upload page.
|
||||
// And this event is used on the post edit page.
|
||||
if (document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
});
|
||||
}
|
||||
if (this._urlConfirmButtonNode) {
|
||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
||||
@ -55,6 +61,11 @@ class FileDropperControl extends events.EventTarget {
|
||||
);
|
||||
}
|
||||
|
||||
document.onpaste = (e) => {
|
||||
if (!document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
}
|
||||
|
||||
this._originalHtml = this._dropperNode.innerHTML;
|
||||
views.replaceContent(target, source);
|
||||
}
|
||||
@ -129,6 +140,17 @@ class FileDropperControl extends events.EventTarget {
|
||||
this._emitFiles(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
_evtPaste(e) {
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
|
||||
|
||||
if (!this._options.allowMultiple && fileList.length > 1) {
|
||||
window.alert("Cannot select multiple files.");
|
||||
} else if (fileList.length > 0) {
|
||||
this._emitFiles(fileList);
|
||||
}
|
||||
}
|
||||
|
||||
_evtUrlInputKeyDown(e) {
|
||||
if (e.which !== KEY_RETURN) {
|
||||
return;
|
||||
|
34
client/js/controls/pool_navigator_control.js
Normal file
34
client/js/controls/pool_navigator_control.js
Normal file
@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
|
||||
const api = require("../api.js");
|
||||
const misc = require("../util/misc.js");
|
||||
const events = require("../events.js");
|
||||
const views = require("../util/views.js");
|
||||
|
||||
const template = views.getTemplate("pool-navigator");
|
||||
|
||||
class PoolNavigatorControl extends events.EventTarget {
|
||||
constructor(hostNode, poolPostNearby) {
|
||||
super();
|
||||
this._hostNode = hostNode;
|
||||
this._poolPostNearby = poolPostNearby;
|
||||
|
||||
views.replaceContent(
|
||||
this._hostNode,
|
||||
template({
|
||||
pool: poolPostNearby,
|
||||
parameters: { query: `pool:${poolPostNearby.id}` },
|
||||
linkClass: misc.makeCssName(poolPostNearby.category, "pool"),
|
||||
canViewPosts: api.hasPrivilege("posts:view"),
|
||||
canViewPools: api.hasPrivilege("pools:view"),
|
||||
firstPost: poolPostNearby.firstPost,
|
||||
previousPost: poolPostNearby.previousPost,
|
||||
nextPost: poolPostNearby.nextPost,
|
||||
lastPost: poolPostNearby.lastPost,
|
||||
getPrettyName: misc.getPrettyName,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolNavigatorControl;
|
49
client/js/controls/pool_navigator_list_control.js
Normal file
49
client/js/controls/pool_navigator_list_control.js
Normal file
@ -0,0 +1,49 @@
|
||||
"use strict";
|
||||
|
||||
const events = require("../events.js");
|
||||
const views = require("../util/views.js");
|
||||
const PoolNavigatorControl = require("../controls/pool_navigator_control.js");
|
||||
|
||||
const template = views.getTemplate("pool-navigator-list");
|
||||
|
||||
class PoolNavigatorListControl extends events.EventTarget {
|
||||
constructor(hostNode, poolPostNearby) {
|
||||
super();
|
||||
this._hostNode = hostNode;
|
||||
this._poolPostNearby = poolPostNearby;
|
||||
this._indexToNode = {};
|
||||
|
||||
for (const entry of this._poolPostNearby) {
|
||||
this._installPoolNavigatorNode(entry);
|
||||
}
|
||||
}
|
||||
|
||||
get _poolNavigatorListNode() {
|
||||
return this._hostNode;
|
||||
}
|
||||
|
||||
_installPoolNavigatorNode(poolPostNearby) {
|
||||
const poolListItemNode = document.createElement("div");
|
||||
const poolControl = new PoolNavigatorControl(
|
||||
poolListItemNode,
|
||||
poolPostNearby,
|
||||
);
|
||||
this._indexToNode[poolPostNearby.id] = poolListItemNode;
|
||||
this._poolNavigatorListNode.appendChild(poolListItemNode);
|
||||
}
|
||||
|
||||
_uninstallPoolNavigatorNode(index) {
|
||||
const poolListItemNode = this._indexToNode[index];
|
||||
poolListItemNode.parentNode.removeChild(poolListItemNode);
|
||||
}
|
||||
|
||||
_evtAdd(e) {
|
||||
this._installPoolNavigatorNode(e.detail.index);
|
||||
}
|
||||
|
||||
_evtRemove(e) {
|
||||
this._uninstallPoolNavigatorNode(e.detail.index);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PoolNavigatorListControl;
|
@ -103,6 +103,30 @@ class PostContentControl {
|
||||
}
|
||||
|
||||
_refreshSize() {
|
||||
if (window.innerWidth <= 800) {
|
||||
const buttons = document.querySelector(".sidebar > .buttons");
|
||||
if (buttons) {
|
||||
const content = document.querySelector(".content");
|
||||
content.insertBefore(buttons, content.querySelector(".post-container + *"));
|
||||
|
||||
const afterControls = document.querySelector(".content > .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
afterControls.parentElement.parentElement.appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const buttons = document.querySelector(".content > .buttons");
|
||||
if (buttons) {
|
||||
const sidebar = document.querySelector(".sidebar");
|
||||
sidebar.insertBefore(buttons, sidebar.firstElementChild);
|
||||
}
|
||||
|
||||
const afterControls = document.querySelector(".content + .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
document.querySelector(".content").appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentFitFunction();
|
||||
}
|
||||
|
||||
|
@ -203,9 +203,8 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||
);
|
||||
|
||||
if (this._formNode) {
|
||||
const inputNodes = this._formNode.querySelectorAll(
|
||||
"input, textarea"
|
||||
);
|
||||
const inputNodes =
|
||||
this._formNode.querySelectorAll("input, textarea");
|
||||
for (let node of inputNodes) {
|
||||
node.addEventListener("change", (e) =>
|
||||
this.dispatchEvent(new CustomEvent("change"))
|
||||
@ -428,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||
: undefined,
|
||||
|
||||
thumbnail:
|
||||
this._newPostThumbnail !== undefined
|
||||
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
|
||||
? this._newPostThumbnail
|
||||
: undefined,
|
||||
|
||||
|
@ -727,9 +727,8 @@ class PostNotesOverlayControl extends events.EventTarget {
|
||||
}
|
||||
|
||||
_showNoteText(note) {
|
||||
this._textNode.querySelector(
|
||||
".wrapper"
|
||||
).innerHTML = misc.formatMarkdown(note.text);
|
||||
this._textNode.querySelector(".wrapper").innerHTML =
|
||||
misc.formatMarkdown(note.text);
|
||||
this._textNode.style.display = "block";
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const noteRect = this._textNode.getBoundingClientRect();
|
||||
|
@ -196,9 +196,10 @@ class TagInputControl extends events.EventTarget {
|
||||
const listItemNode = this._createListItemNode(tag);
|
||||
if (!tag.category) {
|
||||
listItemNode.classList.add("new");
|
||||
}
|
||||
if (source === SOURCE_IMPLICATION) {
|
||||
} else if (source === SOURCE_IMPLICATION) {
|
||||
listItemNode.classList.add("implication");
|
||||
} else {
|
||||
listItemNode.classList.add("added");
|
||||
}
|
||||
this._tagListNode.prependChild(listItemNode);
|
||||
_fadeOutListItemNodeStatus(listItemNode);
|
||||
|
@ -3,13 +3,13 @@
|
||||
const config = require("./config.js");
|
||||
|
||||
if (config.environment == "development") {
|
||||
var ws = new WebSocket("ws://" + location.hostname + ":8080");
|
||||
ws.addEventListener('open', function (event) {
|
||||
var ws = new WebSocket("ws://" + location.hostname + ":8081");
|
||||
ws.addEventListener("open", function (event) {
|
||||
console.log("Live-reloading websocket connected.");
|
||||
});
|
||||
ws.addEventListener('message', (event) => {
|
||||
ws.addEventListener("message", (event) => {
|
||||
console.log(event);
|
||||
if (event.data == 'reload'){
|
||||
if (event.data == "reload") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
|
@ -36,7 +36,7 @@ class Pool extends events.EventTarget {
|
||||
}
|
||||
|
||||
get posts() {
|
||||
return this._posts;
|
||||
return this._postsMicro || this._posts;
|
||||
}
|
||||
|
||||
get postCount() {
|
||||
@ -51,6 +51,22 @@ class Pool extends events.EventTarget {
|
||||
return this._lastEditTime;
|
||||
}
|
||||
|
||||
get firstPost() {
|
||||
return this._firstPost;
|
||||
}
|
||||
|
||||
get lastPost() {
|
||||
return this._lastPost;
|
||||
}
|
||||
|
||||
get previousPost() {
|
||||
return this._previousPost;
|
||||
}
|
||||
|
||||
get nextPost() {
|
||||
return this._nextPost;
|
||||
}
|
||||
|
||||
set names(value) {
|
||||
this._names = value;
|
||||
}
|
||||
@ -169,10 +185,15 @@ class Pool extends events.EventTarget {
|
||||
_creationTime: response.creationTime,
|
||||
_lastEditTime: response.lastEditTime,
|
||||
_postCount: response.postCount || 0,
|
||||
_postsMicro: response.postsMicro,
|
||||
_firstPost: response.firstPost || null,
|
||||
_lastPost: response.lastPost || null,
|
||||
_previousPost: response.previousPost || null,
|
||||
_nextPost: response.nextPost || null,
|
||||
};
|
||||
|
||||
for (let obj of [this, this._orig]) {
|
||||
obj._posts.sync(response.posts);
|
||||
obj._posts.sync(response.posts || []);
|
||||
}
|
||||
|
||||
Object.assign(this, map);
|
||||
|
@ -271,7 +271,7 @@ class Post extends events.EventTarget {
|
||||
if (this._newContent) {
|
||||
files.content = this._newContent;
|
||||
}
|
||||
if (this._newThumbnail !== undefined) {
|
||||
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
|
||||
files.thumbnail = this._newThumbnail;
|
||||
}
|
||||
if (this._source !== this._orig._source) {
|
||||
|
@ -65,17 +65,6 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
|
||||
// post, user and tags permalinks
|
||||
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
||||
preprocess(text) {
|
||||
// URL-based permalinks
|
||||
text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1");
|
||||
text = text.replace(
|
||||
new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"#$1"
|
||||
);
|
||||
text = text.replace(
|
||||
new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"+$1"
|
||||
);
|
||||
|
||||
text = text.replace(
|
||||
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
||||
"$1[$2]($2)"
|
||||
@ -136,12 +125,8 @@ function createRenderer() {
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.image = (href, title, alt) => {
|
||||
let [
|
||||
_,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let [_, url, width, height] =
|
||||
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
|
||||
if (width) {
|
||||
res += '" width="' + width;
|
||||
@ -174,7 +159,7 @@ function formatMarkdown(text) {
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked(text, options);
|
||||
text = marked.parse(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
@ -200,7 +185,7 @@ function formatInlineMarkdown(text) {
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked.inlineLexer(text, [], options);
|
||||
text = marked.parseInline(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
|
@ -40,19 +40,36 @@ function makeRelativeTime(time) {
|
||||
);
|
||||
}
|
||||
|
||||
function makeThumbnail(url) {
|
||||
function makeThumbnail(url, klass, extraProperties) {
|
||||
return makeElement(
|
||||
"span",
|
||||
url
|
||||
? {
|
||||
class: "thumbnail",
|
||||
class: klass || "thumbnail",
|
||||
style: `background-image: url(\'${url}\')`,
|
||||
}
|
||||
: { class: "thumbnail empty" },
|
||||
makeElement("img", { alt: "thumbnail", src: url })
|
||||
makeElement("img", Object.assign({ alt: "thumbnail", src: url }, extraProperties || {}))
|
||||
);
|
||||
}
|
||||
|
||||
function makePoolThumbnails(posts, postFlow) {
|
||||
if (posts.length == 0) {
|
||||
return makeThumbnail(null);
|
||||
}
|
||||
if (postFlow) {
|
||||
return makeThumbnail(posts.at(0).thumbnailUrl);
|
||||
}
|
||||
|
||||
let s = "";
|
||||
|
||||
for (let i = 0; i < Math.min(3, posts.length); i++) {
|
||||
s += makeThumbnail(posts.at(i).thumbnailUrl, "thumbnail thumbnail-" + (i+1), i === 0 ? {fetchPriority: "high"} : {});
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function makeRadio(options) {
|
||||
_imbueId(options);
|
||||
return makeElement(
|
||||
@ -209,13 +226,13 @@ function makePostLink(id, includeHash) {
|
||||
}
|
||||
|
||||
function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
const category = tag ? tag.category : "unknown";
|
||||
const category = tag && tag.category ? tag.category : "unknown";
|
||||
let text = misc.getPrettyName(name);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (tag ? tag.postCount : 0) + ")";
|
||||
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("tags:view")
|
||||
? makeElement(
|
||||
@ -234,15 +251,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
}
|
||||
|
||||
function makePoolLink(id, includeHash, includeCount, pool, name) {
|
||||
const category = pool ? pool.category : "unknown";
|
||||
const category = pool && pool.category ? pool.category : "unknown";
|
||||
let text = misc.getPrettyName(
|
||||
name ? name : pool ? pool.names[0] : "unknown"
|
||||
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
|
||||
);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (pool ? pool.postCount : 0) + ")";
|
||||
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("pools:view")
|
||||
? makeElement(
|
||||
@ -254,7 +271,7 @@ function makePoolLink(id, includeHash, includeCount, pool, name) {
|
||||
misc.escapeHtml(text)
|
||||
)
|
||||
: makeElement(
|
||||
"span",
|
||||
"div",
|
||||
{ class: misc.makeCssName(category, "pool") },
|
||||
misc.escapeHtml(text)
|
||||
);
|
||||
@ -264,7 +281,7 @@ function makeUserLink(user) {
|
||||
let text = makeThumbnail(user ? user.avatarUrl : null);
|
||||
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
||||
const link =
|
||||
user && api.hasPrivilege("users:view")
|
||||
user && user.name && api.hasPrivilege("users:view")
|
||||
? makeElement(
|
||||
"a",
|
||||
{ href: uri.formatClientLink("user", user.name) },
|
||||
@ -436,6 +453,7 @@ function getTemplate(templatePath) {
|
||||
makeFileSize: makeFileSize,
|
||||
makeMarkdown: makeMarkdown,
|
||||
makeThumbnail: makeThumbnail,
|
||||
makePoolThumbnails: makePoolThumbnails,
|
||||
makeRadio: makeRadio,
|
||||
makeCheckbox: makeCheckbox,
|
||||
makeSelect: makeSelect,
|
||||
|
@ -30,7 +30,7 @@ class PoolCreateView extends events.EventTarget {
|
||||
}
|
||||
|
||||
for (let node of this._formNode.querySelectorAll(
|
||||
"input, select, textarea, posts"
|
||||
"input, select, textarea"
|
||||
)) {
|
||||
node.addEventListener("change", (e) => {
|
||||
this.dispatchEvent(new CustomEvent("change"));
|
||||
|
@ -10,6 +10,7 @@ const PostContentControl = require("../controls/post_content_control.js");
|
||||
const PostNotesOverlayControl = require("../controls/post_notes_overlay_control.js");
|
||||
const PostReadonlySidebarControl = require("../controls/post_readonly_sidebar_control.js");
|
||||
const PostEditSidebarControl = require("../controls/post_edit_sidebar_control.js");
|
||||
const PoolNavigatorListControl = require("../controls/pool_navigator_list_control.js");
|
||||
const CommentControl = require("../controls/comment_control.js");
|
||||
const CommentListControl = require("../controls/comment_list_control.js");
|
||||
|
||||
@ -25,9 +26,8 @@ class PostMainView {
|
||||
views.replaceContent(this._hostNode, sourceNode);
|
||||
views.syncScrollPosition();
|
||||
|
||||
const topNavigationNode = document.body.querySelector(
|
||||
"#top-navigation"
|
||||
);
|
||||
const topNavigationNode =
|
||||
document.body.querySelector("#top-navigation");
|
||||
|
||||
this._postContentControl = new PostContentControl(
|
||||
postContainerNode,
|
||||
@ -58,6 +58,7 @@ class PostMainView {
|
||||
this._installSidebar(ctx);
|
||||
this._installCommentForm();
|
||||
this._installComments(ctx.post.comments);
|
||||
this._installPoolNavigators(ctx);
|
||||
|
||||
const showPreviousImage = () => {
|
||||
if (ctx.prevPostId) {
|
||||
@ -138,6 +139,20 @@ class PostMainView {
|
||||
}
|
||||
}
|
||||
|
||||
_installPoolNavigators(ctx) {
|
||||
const poolNavigatorsContainerNode = document.querySelector(
|
||||
"#content-holder .pool-navigators-container"
|
||||
);
|
||||
if (!poolNavigatorsContainerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.poolNavigatorsControl = new PoolNavigatorListControl(
|
||||
poolNavigatorsContainerNode,
|
||||
ctx.post.pools,
|
||||
);
|
||||
}
|
||||
|
||||
_installCommentForm() {
|
||||
const commentFormContainer = document.querySelector(
|
||||
"#content-holder .comment-form-container"
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
const events = require("../events.js");
|
||||
const api = require("../api.js");
|
||||
const views = require("../util/views.js");
|
||||
const FileDropperControl = require("../controls/file_dropper_control.js");
|
||||
|
||||
@ -21,6 +22,7 @@ function _mimeTypeToPostType(mimeType) {
|
||||
"image/heic": "image",
|
||||
"video/mp4": "video",
|
||||
"video/webm": "video",
|
||||
"video/quicktime": "video",
|
||||
}[mimeType] || "unknown"
|
||||
);
|
||||
}
|
||||
@ -34,7 +36,8 @@ class Uploadable extends events.EventTarget {
|
||||
this.flags = [];
|
||||
this.tags = [];
|
||||
this.relations = [];
|
||||
this.anonymous = false;
|
||||
this.anonymous = !api.isLoggedIn();
|
||||
this.forceAnonymous = !api.isLoggedIn();
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
@ -118,6 +121,7 @@ class Url extends Uploadable {
|
||||
heif: "image/heif",
|
||||
heic: "image/heic",
|
||||
mp4: "video/mp4",
|
||||
mov: "video/quicktime",
|
||||
webm: "video/webm",
|
||||
};
|
||||
for (let extension of Object.keys(mime)) {
|
||||
@ -283,7 +287,7 @@ class PostUploadView extends events.EventTarget {
|
||||
for (let uploadable of this._uploadables) {
|
||||
this._updateUploadableFromDom(uploadable);
|
||||
}
|
||||
this._submitButtonNode.value = "Resume upload";
|
||||
this._submitButtonNode.value = "Resume";
|
||||
this._emit("submit");
|
||||
}
|
||||
|
||||
@ -358,6 +362,10 @@ class PostUploadView extends events.EventTarget {
|
||||
detail: {
|
||||
uploadables: this._uploadables,
|
||||
skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
|
||||
alwaysUploadSimilar:
|
||||
this._alwaysUploadSimilarCheckboxNode.checked,
|
||||
pauseRemainOnError:
|
||||
this._pauseRemainOnErrorCheckboxNode.checked,
|
||||
},
|
||||
})
|
||||
);
|
||||
@ -421,6 +429,18 @@ class PostUploadView extends events.EventTarget {
|
||||
return this._hostNode.querySelector("form [name=skip-duplicates]");
|
||||
}
|
||||
|
||||
get _alwaysUploadSimilarCheckboxNode() {
|
||||
return this._hostNode.querySelector(
|
||||
"form [name=always-upload-similar]"
|
||||
);
|
||||
}
|
||||
|
||||
get _pauseRemainOnErrorCheckboxNode() {
|
||||
return this._hostNode.querySelector(
|
||||
"form [name=pause-remain-on-error]"
|
||||
);
|
||||
}
|
||||
|
||||
get _submitButtonNode() {
|
||||
return this._hostNode.querySelector("form [type=submit]");
|
||||
}
|
||||
|
@ -141,6 +141,34 @@ class BulkTagEditor extends BulkEditor {
|
||||
}
|
||||
}
|
||||
|
||||
class BulkDeleteEditor extends BulkEditor {
|
||||
constructor(hostNode) {
|
||||
super(hostNode);
|
||||
this._hostNode.addEventListener("submit", (e) =>
|
||||
this._evtFormSubmit(e)
|
||||
);
|
||||
}
|
||||
|
||||
_evtFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deleteSelectedPosts", { detail: {} })
|
||||
);
|
||||
}
|
||||
|
||||
_evtOpenLinkClick(e) {
|
||||
e.preventDefault();
|
||||
this.toggleOpen(true);
|
||||
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
|
||||
}
|
||||
|
||||
_evtCloseLinkClick(e) {
|
||||
e.preventDefault();
|
||||
this.toggleOpen(false);
|
||||
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
|
||||
}
|
||||
}
|
||||
|
||||
class PostsHeaderView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
@ -186,6 +214,13 @@ class PostsHeaderView extends events.EventTarget {
|
||||
this._bulkEditors.push(this._bulkSafetyEditor);
|
||||
}
|
||||
|
||||
if (this._bulkEditDeleteNode) {
|
||||
this._bulkDeleteEditor = new BulkDeleteEditor(
|
||||
this._bulkEditDeleteNode
|
||||
);
|
||||
this._bulkEditors.push(this._bulkDeleteEditor);
|
||||
}
|
||||
|
||||
for (let editor of this._bulkEditors) {
|
||||
editor.addEventListener("submit", (e) => {
|
||||
this._navigate();
|
||||
@ -204,6 +239,8 @@ class PostsHeaderView extends events.EventTarget {
|
||||
this._openBulkEditor(this._bulkTagEditor);
|
||||
} else if (ctx.parameters.safety && this._bulkSafetyEditor) {
|
||||
this._openBulkEditor(this._bulkSafetyEditor);
|
||||
} else if (ctx.parameters.delete && this._bulkDeleteEditor) {
|
||||
this._openBulkEditor(this._bulkDeleteEditor);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +264,10 @@ class PostsHeaderView extends events.EventTarget {
|
||||
return this._hostNode.querySelector(".bulk-edit-safety");
|
||||
}
|
||||
|
||||
get _bulkEditDeleteNode() {
|
||||
return this._hostNode.querySelector(".bulk-edit-delete");
|
||||
}
|
||||
|
||||
_openBulkEditor(editor) {
|
||||
editor.toggleOpen(true);
|
||||
this._hideBulkEditorsExcept(editor);
|
||||
@ -253,9 +294,8 @@ class PostsHeaderView extends events.EventTarget {
|
||||
e.target.classList.toggle("disabled");
|
||||
const safety = e.target.getAttribute("data-safety");
|
||||
let browsingSettings = settings.get();
|
||||
browsingSettings.listPosts[safety] = !browsingSettings.listPosts[
|
||||
safety
|
||||
];
|
||||
browsingSettings.listPosts[safety] =
|
||||
!browsingSettings.listPosts[safety];
|
||||
settings.save(browsingSettings, true);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", {
|
||||
@ -294,6 +334,10 @@ class PostsHeaderView extends events.EventTarget {
|
||||
this._bulkSafetyEditor && this._bulkSafetyEditor.opened
|
||||
? "1"
|
||||
: null;
|
||||
parameters.delete =
|
||||
this._bulkDeleteEditor && this._bulkDeleteEditor.opened
|
||||
? "1"
|
||||
: null;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", { detail: { parameters: parameters } })
|
||||
);
|
||||
|
@ -39,6 +39,13 @@ class PostsPageView extends events.EventTarget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
|
||||
if (deleteFlipperNode) {
|
||||
deleteFlipperNode.addEventListener("click", (e) =>
|
||||
this._evtBulkToggleDeleteClick(e, post)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._syncBulkEditorsHighlights();
|
||||
@ -56,6 +63,10 @@ class PostsPageView extends events.EventTarget {
|
||||
return listItemNode.querySelector(".safety-flipper");
|
||||
}
|
||||
|
||||
_getDeleteFlipperNode(listItemNode) {
|
||||
return listItemNode.querySelector(".delete-flipper");
|
||||
}
|
||||
|
||||
_evtPostChange(e) {
|
||||
const listItemNode = this._postIdToListItemNode[e.detail.post.id];
|
||||
for (let node of listItemNode.querySelectorAll("[data-disabled]")) {
|
||||
@ -99,6 +110,20 @@ class PostsPageView extends events.EventTarget {
|
||||
);
|
||||
}
|
||||
|
||||
_evtBulkToggleDeleteClick(e, post) {
|
||||
e.preventDefault();
|
||||
const linkNode = e.target;
|
||||
linkNode.classList.toggle("delete");
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("markForDeletion", {
|
||||
detail: {
|
||||
post,
|
||||
delete: linkNode.classList.contains("delete"),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_syncBulkEditorsHighlights() {
|
||||
for (let listItemNode of this._listItemNodes) {
|
||||
const postId = listItemNode.getAttribute("data-post-id");
|
||||
@ -123,6 +148,16 @@ class PostsPageView extends events.EventTarget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
|
||||
if (deleteFlipperNode) {
|
||||
deleteFlipperNode.classList.toggle(
|
||||
"delete",
|
||||
this._ctx.bulkEdit.markedForDeletion.some(
|
||||
(x) => x.id == postId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,9 +72,8 @@ class UserTokenView extends events.EventTarget {
|
||||
|
||||
_evtDelete(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("delete", {
|
||||
detail: {
|
||||
@ -110,9 +109,8 @@ class UserTokenView extends events.EventTarget {
|
||||
|
||||
_evtChangeNoteClick(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
const text = window.prompt(
|
||||
"Please enter the new name:",
|
||||
userToken.note !== null ? userToken.note : undefined
|
||||
|
526
client/package-lock.json
generated
526
client/package-lock.json
generated
@ -10,7 +10,7 @@
|
||||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
@ -27,13 +27,19 @@
|
||||
"html-minifier": "^3.5.18",
|
||||
"jimp": "^0.13.0",
|
||||
"pretty-error": "^3.0.3",
|
||||
"stylus": "^0.54.8",
|
||||
"terser": "^3.7.7",
|
||||
"stylus": "^0.59.0",
|
||||
"terser": "^4.8.1",
|
||||
"underscore": "^1.12.1",
|
||||
"watchify": "^4.0.0",
|
||||
"ws": "^7.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@adobe/css-tools": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
|
||||
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
|
||||
@ -476,24 +482,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-filter": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
|
||||
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-map": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-reduce": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
@ -534,18 +522,6 @@
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"atob": "bin/atob.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/available-typed-arrays": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
|
||||
@ -1506,16 +1482,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@ -1682,9 +1657,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "2.5.7",
|
||||
@ -1756,27 +1731,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/css": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
|
||||
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.5.2",
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-parse": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
|
||||
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"css": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
|
||||
@ -1814,15 +1768,6 @@
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csso": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-3.5.1.tgz",
|
||||
@ -1849,15 +1794,6 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@ -2254,8 +2190,7 @@
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/get-assigned-identifiers": {
|
||||
"version": "1.2.0",
|
||||
@ -2267,7 +2202,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@ -2351,7 +2285,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
@ -2384,7 +2317,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
@ -2859,9 +2791,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
@ -2997,14 +2929,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
@ -3108,9 +3040,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
@ -3224,7 +3156,6 @@
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -3385,9 +3316,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-platform": {
|
||||
@ -3507,11 +3438,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/querystring": {
|
||||
@ -3694,13 +3631,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-url": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
|
||||
"deprecated": "https://github.com/lydell/resolve-url#deprecated",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ripemd160": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||
@ -3716,12 +3646,6 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@ -3770,15 +3694,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
||||
"dev": true,
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dependencies": {
|
||||
"array-filter": "~0.0.0",
|
||||
"array-map": "~0.0.0",
|
||||
"array-reduce": "~0.0.0",
|
||||
"jsonify": "~0.0.0"
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
@ -3805,19 +3736,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-resolve": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
|
||||
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"resolve-url": "^0.2.1",
|
||||
"source-map-url": "^0.4.0",
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.4.18",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
|
||||
@ -3827,12 +3745,6 @@
|
||||
"source-map": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stream-browserify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
||||
@ -3923,18 +3835,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/stylus": {
|
||||
"version": "0.54.8",
|
||||
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
|
||||
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
|
||||
"version": "0.59.0",
|
||||
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
|
||||
"integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"css-parse": "~2.0.0",
|
||||
"debug": "~3.1.0",
|
||||
"@adobe/css-tools": "^4.0.1",
|
||||
"debug": "^4.3.2",
|
||||
"glob": "^7.1.6",
|
||||
"mkdirp": "~1.0.4",
|
||||
"safer-buffer": "^2.1.2",
|
||||
"sax": "~1.2.4",
|
||||
"semver": "^6.3.0",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"bin": {
|
||||
@ -3942,28 +3851,33 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/stylus"
|
||||
}
|
||||
},
|
||||
"node_modules/stylus/node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"node_modules/stylus/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/stylus/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
"node_modules/stylus/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/stylus/node_modules/source-map": {
|
||||
"version": "0.7.3",
|
||||
@ -3983,12 +3897,6 @@
|
||||
"minimist": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/subarg/node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
@ -4028,26 +3936,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "~2.14.1",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.6"
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/uglifyjs"
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map": {
|
||||
@ -4060,9 +3968,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map-support": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
@ -4236,13 +4144,6 @@
|
||||
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/urix": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
|
||||
"deprecated": "Please see https://github.com/lydell/urix#deprecated",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/url": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
|
||||
@ -4619,6 +4520,12 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz",
|
||||
"integrity": "sha512-E09FiIft46CmH5Qnjb0wsW54/YQd69LsxeKUOWawmws1XWvyFGURnAChH0mlr7YPFR1ofwvUQfcL0J3lMxXqPA==",
|
||||
"dev": true
|
||||
},
|
||||
"@babel/runtime": {
|
||||
"version": "7.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz",
|
||||
@ -5047,24 +4954,6 @@
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"array-filter": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
|
||||
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
|
||||
"dev": true
|
||||
},
|
||||
"array-map": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
||||
"dev": true
|
||||
},
|
||||
"array-reduce": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||
"dev": true
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
@ -5107,12 +4996,6 @@
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
|
||||
},
|
||||
"atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
|
||||
"dev": true
|
||||
},
|
||||
"available-typed-arrays": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz",
|
||||
@ -6053,16 +5936,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
@ -6211,9 +6093,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.5.7",
|
||||
@ -6282,35 +6164,6 @@
|
||||
"randomfill": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz",
|
||||
"integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.5.2",
|
||||
"urix": "^0.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-parse": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz",
|
||||
"integrity": "sha1-pGjuZnwW2BzPBcWMONKpfHgNv9Q=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"css-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz",
|
||||
@ -6362,12 +6215,6 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
@ -6697,8 +6544,7 @@
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"get-assigned-identifiers": {
|
||||
"version": "1.2.0",
|
||||
@ -6710,7 +6556,6 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
@ -6778,7 +6623,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
@ -6801,8 +6645,7 @@
|
||||
"has-symbols": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "3.0.4",
|
||||
@ -7158,9 +7001,9 @@
|
||||
}
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"dev": true
|
||||
},
|
||||
"js-cookie": {
|
||||
@ -7280,9 +7123,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.4",
|
||||
@ -7364,9 +7207,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
@ -7466,8 +7309,7 @@
|
||||
"object-inspect": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw=="
|
||||
},
|
||||
"object-keys": {
|
||||
"version": "1.1.1",
|
||||
@ -7607,9 +7449,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-platform": {
|
||||
@ -7705,9 +7547,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
@ -7867,12 +7712,6 @@
|
||||
"path-parse": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"resolve-url": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
|
||||
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
|
||||
"dev": true
|
||||
},
|
||||
"ripemd160": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
|
||||
@ -7888,12 +7727,6 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"dev": true
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
@ -7936,15 +7769,19 @@
|
||||
}
|
||||
},
|
||||
"shell-quote": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
||||
"dev": true,
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||
"dev": true
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"requires": {
|
||||
"array-filter": "~0.0.0",
|
||||
"array-map": "~0.0.0",
|
||||
"array-reduce": "~0.0.0",
|
||||
"jsonify": "~0.0.0"
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"simple-concat": {
|
||||
@ -7965,19 +7802,6 @@
|
||||
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz",
|
||||
"integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0",
|
||||
"resolve-url": "^0.2.1",
|
||||
"source-map-url": "^0.4.0",
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.4.18",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz",
|
||||
@ -7987,12 +7811,6 @@
|
||||
"source-map": "^0.5.6"
|
||||
}
|
||||
},
|
||||
"source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
|
||||
"dev": true
|
||||
},
|
||||
"stream-browserify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
|
||||
@ -8074,31 +7892,31 @@
|
||||
}
|
||||
},
|
||||
"stylus": {
|
||||
"version": "0.54.8",
|
||||
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz",
|
||||
"integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==",
|
||||
"version": "0.59.0",
|
||||
"resolved": "https://registry.npmjs.org/stylus/-/stylus-0.59.0.tgz",
|
||||
"integrity": "sha512-lQ9w/XIOH5ZHVNuNbWW8D822r+/wBSO/d6XvtyHLF7LW4KaCIDeVbvn5DF8fGCJAUCwVhVi/h6J0NUcnylUEjg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"css-parse": "~2.0.0",
|
||||
"debug": "~3.1.0",
|
||||
"@adobe/css-tools": "^4.0.1",
|
||||
"debug": "^4.3.2",
|
||||
"glob": "^7.1.6",
|
||||
"mkdirp": "~1.0.4",
|
||||
"safer-buffer": "^2.1.2",
|
||||
"sax": "~1.2.4",
|
||||
"semver": "^6.3.0",
|
||||
"source-map": "^0.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"dev": true
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
@ -8116,14 +7934,6 @@
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
@ -8159,20 +7969,20 @@
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "~2.14.1",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.6"
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
@ -8182,9 +7992,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
@ -8329,12 +8139,6 @@
|
||||
"integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=",
|
||||
"dev": true
|
||||
},
|
||||
"urix": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
|
||||
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=",
|
||||
"dev": true
|
||||
},
|
||||
"url": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
|
||||
|
@ -3,14 +3,15 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"watch": "node build.js --watch"
|
||||
"watch": "node build.js --watch",
|
||||
"build-container": "docker build -t szurubooru/client:dev ."
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^2.0.17",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
@ -27,8 +28,8 @@
|
||||
"html-minifier": "^3.5.18",
|
||||
"jimp": "^0.13.0",
|
||||
"pretty-error": "^3.0.3",
|
||||
"stylus": "^0.54.8",
|
||||
"terser": "^3.7.7",
|
||||
"stylus": "^0.59.0",
|
||||
"terser": "^4.8.1",
|
||||
"underscore": "^1.12.1",
|
||||
"watchify": "^4.0.0",
|
||||
"ws": "^7.4.6"
|
||||
|
77
doc/API.md
77
doc/API.md
@ -37,6 +37,7 @@
|
||||
- [Creating post](#creating-post)
|
||||
- [Updating post](#updating-post)
|
||||
- [Getting post](#getting-post)
|
||||
- [Getting around post](#getting-around-post)
|
||||
- [Deleting post](#deleting-post)
|
||||
- [Merging posts](#merging-posts)
|
||||
- [Rating post](#rating-post)
|
||||
@ -53,7 +54,7 @@
|
||||
- [Deleting pool category](#deleting-pool-category)
|
||||
- [Setting default pool category](#setting-default-pool-category)
|
||||
- Pools
|
||||
- [Listing pools](#listing-pool)
|
||||
- [Listing pools](#listing-pools)
|
||||
- [Creating pool](#creating-pool)
|
||||
- [Updating pool](#updating-pool)
|
||||
- [Getting pool](#getting-pool)
|
||||
@ -164,9 +165,9 @@ way. The files, however, should be passed as regular fields appended with a
|
||||
accepts a file named `content`, the client should pass
|
||||
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
|
||||
body. When creating or updating post content using this method, the server can
|
||||
also be configured to employ [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
to download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
youtube-dl can be configured with the `'uploads:use_downloader'` permission
|
||||
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
|
||||
download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
yt-dlp can be configured with the `'uploads:use_downloader'` permission
|
||||
|
||||
Finally, in some cases the user might want to reuse one file between the
|
||||
requests to save the bandwidth (for example, reverse search + consecutive
|
||||
@ -322,7 +323,7 @@ data.
|
||||
{
|
||||
"name": <name>,
|
||||
"color": <color>,
|
||||
"order": <order> // optional
|
||||
"order": <order>
|
||||
}
|
||||
```
|
||||
|
||||
@ -788,7 +789,7 @@ data.
|
||||
| `fav-time` | alias of `fav-date` |
|
||||
| `feature-date` | featured at given date |
|
||||
| `feature-time` | alias of `feature-time` |
|
||||
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` (or `questionable`) or `unsafe`. |
|
||||
| `safety` | having given safety. `<value>` can be either `safe`, `sketchy` or `unsafe`. |
|
||||
| `rating` | alias of `safety` |
|
||||
|
||||
**Sort style tokens**
|
||||
@ -951,6 +952,66 @@ data.
|
||||
|
||||
Retrieves information about an existing post.
|
||||
|
||||
## Getting around post
|
||||
- **Request**
|
||||
|
||||
`GET /post/<id>/around`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"prev": <post-resource>,
|
||||
"next": <post-resource>
|
||||
}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves information about posts that are before or after an existing post.
|
||||
|
||||
## Getting pools around post
|
||||
- **Request**
|
||||
|
||||
`GET /post/<id>/pools-nearby`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
[
|
||||
{
|
||||
"pool": <pool>,
|
||||
"firstPost": <first-post>,
|
||||
"lastPost": <last-post>,
|
||||
"nextPost": <next-post>,
|
||||
"previousPost": <previous-post>
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
- **Field meaning**
|
||||
|
||||
- `<pool>`: The associated [micro pool resource](#micro-pool).
|
||||
- `<first-post>`: A [micro post resource](#micro-post) that displays the first post in the pool.
|
||||
- `<last-post>`: A [micro post resource](#micro-post) that displays the last post in the pool.
|
||||
- `<next-post>`: A [micro post resource](#micro-post) that displays the next post in the pool.
|
||||
- `<previous-post>`: A [micro post resource](#micro-post) that displays the previous post in the pool.
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves extra information about any pools that the post is in.
|
||||
|
||||
## Deleting post
|
||||
- **Request**
|
||||
|
||||
@ -1365,7 +1426,7 @@ data.
|
||||
## Creating pool
|
||||
- **Request**
|
||||
|
||||
`POST /pools/create`
|
||||
`POST /pool`
|
||||
|
||||
- **Input**
|
||||
|
||||
@ -2467,7 +2528,7 @@ One file together with its metadata posted to the site.
|
||||
## Micro post
|
||||
**Description**
|
||||
|
||||
A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
|
||||
A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields.
|
||||
|
||||
## Note
|
||||
**Description**
|
||||
|
@ -1,5 +1,5 @@
|
||||
This assumes that you have Docker (version 17.05 or greater)
|
||||
and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
This assumes that you have Docker (version 19.03 or greater)
|
||||
and the Docker Compose CLI (version 1.27.0 or greater) already installed.
|
||||
|
||||
### Prepare things
|
||||
|
||||
@ -34,33 +34,79 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
||||
Read the comments to guide you. Note that `.env` should be in the root
|
||||
directory of this repository.
|
||||
|
||||
### Running the Application
|
||||
4. Pull the containers:
|
||||
|
||||
Download containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose pull
|
||||
```
|
||||
This pulls the latest containers from docker.io:
|
||||
```console
|
||||
user@host:szuru$ docker compose pull
|
||||
```
|
||||
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d sql
|
||||
```
|
||||
If you have modified the application's source and would like to manually
|
||||
build it, follow the instructions in [**Building**](#Building) instead,
|
||||
then read here once you're done.
|
||||
|
||||
To start all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d
|
||||
```
|
||||
5. Run it!
|
||||
|
||||
To view/monitor the application logs:
|
||||
```console
|
||||
user@host:szuru$ docker-compose logs -f
|
||||
# (CTRL+C to exit)
|
||||
```
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker compose up -d sql
|
||||
```
|
||||
|
||||
To start all containers:
|
||||
```console
|
||||
user@host:szuru$ docker compose up -d
|
||||
```
|
||||
|
||||
To view/monitor the application logs:
|
||||
```console
|
||||
user@host:szuru$ docker compose logs -f
|
||||
# (CTRL+C to exit)
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
1. Edit `docker-compose.yml` to tell Docker to build instead of pull containers:
|
||||
|
||||
```diff yaml
|
||||
...
|
||||
server:
|
||||
- image: szurubooru/server:latest
|
||||
+ build: server
|
||||
...
|
||||
client:
|
||||
- image: szurubooru/client:latest
|
||||
+ build: client
|
||||
...
|
||||
```
|
||||
|
||||
You can choose to build either one from source.
|
||||
|
||||
2. Build the containers:
|
||||
|
||||
```console
|
||||
user@host:szuru$ docker compose build
|
||||
```
|
||||
|
||||
That will attempt to build both containers, but you can specify `client`
|
||||
or `server` to make it build only one.
|
||||
|
||||
If `docker compose build` spits out:
|
||||
|
||||
```
|
||||
ERROR: Service 'server' failed to build: failed to parse platform : "" is an invalid component of "": platform specifier component must match "^[A-Za-z0-9_-]+$": invalid argument
|
||||
```
|
||||
|
||||
...you will need to export Docker BuildKit flags:
|
||||
|
||||
```console
|
||||
user@host:szuru$ export DOCKER_BUILDKIT=1; export COMPOSE_DOCKER_CLI_BUILD=1
|
||||
```
|
||||
|
||||
...and run `docker compose build` again.
|
||||
|
||||
*Note: If your changes are not taking effect in your builds, consider building
|
||||
with `--no-cache`.*
|
||||
|
||||
To stop all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose down
|
||||
```
|
||||
|
||||
### Additional Features
|
||||
|
||||
@ -71,7 +117,7 @@ user@host:szuru$ docker-compose down
|
||||
run from docker:
|
||||
|
||||
```console
|
||||
user@host:szuru$ docker-compose run server ./szuru-admin --help
|
||||
user@host:szuru$ docker compose run server ./szuru-admin --help
|
||||
```
|
||||
|
||||
will give you a breakdown on all available commands.
|
||||
|
@ -10,6 +10,12 @@ BUILD_INFO=latest
|
||||
# otherwise the port specified here will be publicly accessible
|
||||
PORT=8080
|
||||
|
||||
# How many waitress threads to start
|
||||
# 4 is the default amount of threads. If you experience performance
|
||||
# degradation with a large number of posts, increasing this may
|
||||
# improve performance, since waitress is most likely clogging up with Tasks.
|
||||
THREADS=4
|
||||
|
||||
# URL base to run szurubooru under
|
||||
# See "Additional Features" section in INSTALL.md
|
||||
BASE_URL=/
|
||||
|
54
docker-compose.dev.yml
Normal file
54
docker-compose.dev.yml
Normal file
@ -0,0 +1,54 @@
|
||||
## Docker Compose configuration for dev iteration
|
||||
##
|
||||
## Data is transient by using named vols.
|
||||
## Run: docker-compose -f ./docker-compose.dev.yml up
|
||||
services:
|
||||
|
||||
server:
|
||||
build:
|
||||
context: ./server
|
||||
target: development
|
||||
depends_on:
|
||||
- sql
|
||||
environment:
|
||||
## These should be the names of the dependent containers listed below,
|
||||
## or FQDNs/IP addresses if these services are running outside of Docker
|
||||
POSTGRES_HOST: sql
|
||||
## Credentials for database:
|
||||
POSTGRES_USER:
|
||||
POSTGRES_PASSWORD:
|
||||
## Commented Values are Default:
|
||||
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
||||
#POSTGRES_PORT: 5432
|
||||
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||
THREADS:
|
||||
volumes:
|
||||
- "data:/data"
|
||||
- "./server/:/opt/app/"
|
||||
|
||||
client:
|
||||
build:
|
||||
context: ./client
|
||||
target: development
|
||||
depends_on:
|
||||
- server
|
||||
volumes:
|
||||
- "data:/data:ro"
|
||||
- "./client/:/opt/app/"
|
||||
- "/opt/app/public/"
|
||||
ports:
|
||||
- "${PORT}:80"
|
||||
- "8081:8081"
|
||||
|
||||
sql:
|
||||
image: postgres:11-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER:
|
||||
POSTGRES_PASSWORD:
|
||||
volumes:
|
||||
- "sql:/var/lib/postgresql/data"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
sql:
|
@ -1,9 +1,7 @@
|
||||
## Example Docker Compose configuration
|
||||
##
|
||||
## Use this as a template to set up docker-compose, or as guide to set up other
|
||||
## Use this as a template to set up docker compose, or as guide to set up other
|
||||
## orchestration services
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
server:
|
||||
@ -21,6 +19,7 @@ services:
|
||||
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
||||
#POSTGRES_PORT: 5432
|
||||
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||
THREADS:
|
||||
volumes:
|
||||
- "${MOUNT_DATA}:/data"
|
||||
- "./server/config.yaml:/opt/app/config.yaml"
|
||||
|
@ -7,8 +7,13 @@ WORKDIR /opt/app
|
||||
RUN apk --no-cache add \
|
||||
python3 \
|
||||
python3-dev \
|
||||
ffmpeg \
|
||||
py3-pip \
|
||||
build-base \
|
||||
libheif \
|
||||
libheif-dev \
|
||||
libavif \
|
||||
libavif-dev \
|
||||
ffmpeg \
|
||||
# from requirements.txt:
|
||||
py3-yaml \
|
||||
py3-psycopg2 \
|
||||
@ -18,26 +23,21 @@ RUN apk --no-cache add \
|
||||
py3-pillow \
|
||||
py3-pynacl \
|
||||
py3-tz \
|
||||
py3-pyrfc3339 \
|
||||
build-base \
|
||||
&& apk --no-cache add \
|
||||
libheif \
|
||||
libavif \
|
||||
libheif-dev \
|
||||
libavif-dev \
|
||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
alembic \
|
||||
py3-pyrfc3339
|
||||
RUN pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
"alembic>=0.8.5" \
|
||||
"coloredlogs==5.0" \
|
||||
youtube_dl \
|
||||
pillow-avif-plugin \
|
||||
pyheif-pillow-opener \
|
||||
&& apk --no-cache del py3-pip
|
||||
"pyheif==0.6.1" \
|
||||
"heif-image-plugin>=0.3.2" \
|
||||
yt-dlp \
|
||||
"pillow-avif-plugin~=1.1.0"
|
||||
RUN apk --no-cache del py3-pip
|
||||
|
||||
COPY ./ /opt/app/
|
||||
RUN rm -rf /opt/app/szurubooru/tests
|
||||
|
||||
|
||||
FROM prereqs as testing
|
||||
FROM --platform=$BUILDPLATFORM prereqs as testing
|
||||
WORKDIR /opt/app
|
||||
|
||||
RUN apk --no-cache add \
|
||||
@ -61,7 +61,42 @@ ENTRYPOINT ["pytest", "--tb=short"]
|
||||
CMD ["szurubooru/"]
|
||||
|
||||
|
||||
FROM prereqs as development
|
||||
WORKDIR /opt/app
|
||||
|
||||
ARG PUID=1000
|
||||
ARG PGID=1000
|
||||
|
||||
RUN apk --no-cache add \
|
||||
dumb-init \
|
||||
py3-pip \
|
||||
py3-setuptools \
|
||||
py3-waitress \
|
||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
hupper \
|
||||
&& mkdir -p /opt/app /data \
|
||||
&& addgroup -g ${PGID} app \
|
||||
&& adduser -SDH -h /opt/app -g '' -G app -u ${PUID} app \
|
||||
&& chown -R app:app /opt/app /data
|
||||
|
||||
USER app
|
||||
CMD ["/opt/app/docker-start-dev.sh"]
|
||||
|
||||
ARG PORT=6666
|
||||
ENV PORT=${PORT}
|
||||
EXPOSE ${PORT}
|
||||
|
||||
ARG THREADS=4
|
||||
ENV THREADS=${THREADS}
|
||||
|
||||
VOLUME ["/data/"]
|
||||
|
||||
|
||||
FROM prereqs as release
|
||||
|
||||
COPY ./ /opt/app/
|
||||
RUN rm -rf /opt/app/szurubooru/tests
|
||||
|
||||
WORKDIR /opt/app
|
||||
|
||||
ARG PUID=1000
|
||||
@ -83,6 +118,9 @@ ARG PORT=6666
|
||||
ENV PORT=${PORT}
|
||||
EXPOSE ${PORT}
|
||||
|
||||
ARG THREADS=4
|
||||
ENV THREADS=${THREADS}
|
||||
|
||||
VOLUME ["/data/"]
|
||||
|
||||
ARG DOCKER_REPO
|
||||
|
@ -115,6 +115,7 @@ privileges:
|
||||
'posts:favorite': regular
|
||||
'posts:bulk-edit:tags': power
|
||||
'posts:bulk-edit:safety': power
|
||||
'posts:bulk-edit:delete': power
|
||||
|
||||
'tags:create': regular
|
||||
'tags:edit:names': power
|
||||
|
8
server/docker-start-dev.sh
Executable file
8
server/docker-start-dev.sh
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/dumb-init /bin/sh
|
||||
set -e
|
||||
cd /opt/app
|
||||
|
||||
alembic upgrade head
|
||||
|
||||
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||
exec hupper -m waitress --port ${PORT} --threads ${THREADS} szurubooru.facade:app
|
@ -4,5 +4,5 @@ cd /opt/app
|
||||
|
||||
alembic upgrade head
|
||||
|
||||
echo "Starting szurubooru API on port ${PORT}"
|
||||
exec waitress-serve-3 --port ${PORT} szurubooru.facade:app
|
||||
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||
exec waitress-serve-3 --port ${PORT} --threads ${THREADS} szurubooru.facade:app
|
||||
|
@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
docker build \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg SOURCE_COMMIT \
|
||||
--build-arg DOCKER_REPO \
|
||||
-f $DOCKERFILE_PATH -t $IMAGE_NAME .
|
@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
add_tag() {
|
||||
echo "Also tagging image as ${DOCKER_REPO}:${1}"
|
||||
docker tag $IMAGE_NAME $DOCKER_REPO:$1
|
||||
docker push $DOCKER_REPO:$1
|
||||
}
|
||||
|
||||
CLOSEST_VER=$(git describe --tags --abbrev=0)
|
||||
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
|
||||
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
|
||||
|
||||
add_tag "${CLOSEST_MAJOR_VER}-edge"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
|
||||
|
||||
if git describe --exact-match --abbrev=0 2> /dev/null; then
|
||||
add_tag "${CLOSEST_MAJOR_VER}"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
|
||||
fi
|
@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
docker run --rm \
|
||||
-t $(docker build --target testing -q .) \
|
||||
--color=no szurubooru/
|
||||
|
||||
exit $?
|
@ -1,14 +1,15 @@
|
||||
alembic>=0.8.5
|
||||
pyyaml>=3.11
|
||||
psycopg2-binary>=2.6.1
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
coloredlogs==5.0
|
||||
certifi>=2017.11.5
|
||||
coloredlogs==5.0
|
||||
heif-image-plugin==0.3.2
|
||||
numpy>=1.8.2
|
||||
pillow-avif-plugin~=1.1.0
|
||||
pillow>=4.3.0
|
||||
psycopg2-binary>=2.6.1
|
||||
pyheif==0.6.1
|
||||
pynacl>=1.2.1
|
||||
pytz>=2018.3
|
||||
pyRFC3339>=1.0
|
||||
pillow-avif-plugin>=1.1.0
|
||||
pyheif-pillow-opener>=0.1.0
|
||||
youtube_dl
|
||||
pytz>=2018.3
|
||||
pyyaml>=3.11
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
yt-dlp
|
||||
|
@ -91,6 +91,15 @@ def reset_filenames() -> None:
|
||||
rename_in_dir("posts/custom-thumbnails/")
|
||||
|
||||
|
||||
def regenerate_thumbnails() -> None:
|
||||
for post in db.session.query(model.Post).all():
|
||||
print("Generating tumbnail for post %d ..." % post.post_id, end="\r")
|
||||
try:
|
||||
postfuncs.generate_post_thumbnail(post)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser_top = ArgumentParser(
|
||||
description="Collection of CLI commands for an administrator to use",
|
||||
@ -114,6 +123,12 @@ def main() -> None:
|
||||
help="reset and rename the content and thumbnail "
|
||||
"filenames in case of a lost/changed secret key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--regenerate-thumbnails",
|
||||
action="store_true",
|
||||
help="regenerate the thumbnails for posts if the "
|
||||
"thumbnail files are missing",
|
||||
)
|
||||
command = parser_top.parse_args()
|
||||
|
||||
try:
|
||||
@ -123,6 +138,8 @@ def main() -> None:
|
||||
check_audio()
|
||||
elif command.reset_filenames:
|
||||
reset_filenames()
|
||||
elif command.regenerate_thumbnails:
|
||||
regenerate_thumbnails()
|
||||
except errors.BaseError as e:
|
||||
print(e, file=stderr)
|
||||
|
||||
|
@ -5,12 +5,10 @@ from szurubooru import db, errors, model, rest, search
|
||||
from szurubooru.func import (
|
||||
auth,
|
||||
favorites,
|
||||
mime,
|
||||
posts,
|
||||
scores,
|
||||
serialization,
|
||||
snapshots,
|
||||
tags,
|
||||
versions,
|
||||
)
|
||||
|
||||
|
@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
|
||||
return left
|
||||
|
||||
|
||||
def _docker_config() -> Dict:
|
||||
def _container_config() -> Dict:
|
||||
if "TEST_ENVIRONMENT" not in os.environ:
|
||||
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
||||
if key not in os.environ:
|
||||
@ -33,7 +33,7 @@ def _docker_config() -> Dict:
|
||||
"show_sql": int(os.getenv("LOG_SQL", 0)),
|
||||
"data_url": os.getenv("DATA_URL", "data/"),
|
||||
"data_dir": "/data/",
|
||||
"database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
"database": "postgresql://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
% {
|
||||
"user": os.getenv("POSTGRES_USER"),
|
||||
"pass": os.getenv("POSTGRES_PASSWORD"),
|
||||
@ -49,6 +49,15 @@ def _file_config(filename: str) -> Dict:
|
||||
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
|
||||
|
||||
|
||||
def _running_inside_container() -> bool:
|
||||
env = os.environ.keys()
|
||||
return (
|
||||
os.path.exists("/.dockerenv")
|
||||
or "KUBERNETES_SERVICE_HOST" in env
|
||||
or "container" in env # set by lxc/podman
|
||||
)
|
||||
|
||||
|
||||
def _read_config() -> Dict:
|
||||
ret = _file_config("config.yaml.dist")
|
||||
if os.path.isfile("config.yaml"):
|
||||
@ -57,8 +66,8 @@ def _read_config() -> Dict:
|
||||
logger.warning(
|
||||
"'config.yaml' should be a file, not a directory, skipping"
|
||||
)
|
||||
if os.path.exists("/.dockerenv"):
|
||||
ret = _merge(ret, _docker_config())
|
||||
if _running_inside_container():
|
||||
ret = _merge(ret, _container_config())
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -135,7 +135,7 @@ _live_migrations = (
|
||||
|
||||
|
||||
def create_app() -> Callable[[Any, Any], Any]:
|
||||
""" Create a WSGI compatible App object. """
|
||||
"""Create a WSGI compatible App object."""
|
||||
validate_config()
|
||||
coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s")
|
||||
if config.config["debug"]:
|
||||
|
@ -25,7 +25,7 @@ RANK_MAP = OrderedDict(
|
||||
|
||||
|
||||
def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||
""" Retrieve argon2id password hash. """
|
||||
"""Retrieve argon2id password hash."""
|
||||
return (
|
||||
pwhash.argon2id.str(
|
||||
(config.config["secret"] + salt + password).encode("utf8")
|
||||
@ -37,7 +37,7 @@ def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||
def get_sha256_legacy_password_hash(
|
||||
salt: str, password: str
|
||||
) -> Tuple[str, int]:
|
||||
""" Retrieve old-style sha256 password hash. """
|
||||
"""Retrieve old-style sha256 password hash."""
|
||||
digest = hashlib.sha256()
|
||||
digest.update(config.config["secret"].encode("utf8"))
|
||||
digest.update(salt.encode("utf8"))
|
||||
@ -46,7 +46,7 @@ def get_sha256_legacy_password_hash(
|
||||
|
||||
|
||||
def get_sha1_legacy_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||
""" Retrieve old-style sha1 password hash. """
|
||||
"""Retrieve old-style sha1 password hash."""
|
||||
digest = hashlib.sha1()
|
||||
digest.update(b"1A2/$_4xVa")
|
||||
digest.update(salt.encode("utf8"))
|
||||
@ -125,7 +125,7 @@ def verify_privilege(user: model.User, privilege_name: str) -> None:
|
||||
|
||||
|
||||
def generate_authentication_token(user: model.User) -> str:
|
||||
""" Generate nonguessable challenge (e.g. links in password reminder). """
|
||||
"""Generate nonguessable challenge (e.g. links in password reminder)."""
|
||||
assert user
|
||||
digest = hashlib.md5()
|
||||
digest.update(config.config["secret"].encode("utf8"))
|
||||
|
@ -4,12 +4,10 @@ from datetime import datetime
|
||||
from io import BytesIO
|
||||
from typing import Any, Callable, List, Optional, Set, Tuple
|
||||
|
||||
import HeifImagePlugin
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import pillow_avif
|
||||
import pyheif
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
register_heif_opener()
|
||||
from PIL import Image
|
||||
|
||||
from szurubooru import config, errors
|
||||
|
||||
@ -44,7 +42,7 @@ def _preprocess_image(content: bytes) -> NpMatrix:
|
||||
try:
|
||||
img = Image.open(BytesIO(content))
|
||||
return np.asarray(img.convert("L"), dtype=np.uint8)
|
||||
except IOError:
|
||||
except (IOError, ValueError):
|
||||
raise errors.ProcessingError(
|
||||
"Unable to generate a signature hash " "for this image."
|
||||
)
|
||||
|
@ -6,6 +6,9 @@ import shlex
|
||||
import subprocess
|
||||
from io import BytesIO
|
||||
from typing import List
|
||||
|
||||
import HeifImagePlugin
|
||||
import pillow_avif
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from szurubooru import errors
|
||||
@ -17,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||
def convert_heif_to_png(content: bytes) -> bytes:
|
||||
img = PILImage.open(BytesIO(content))
|
||||
img_byte_arr = BytesIO()
|
||||
img.save(img_byte_arr, format='PNG')
|
||||
img.save(img_byte_arr, format="PNG")
|
||||
return img_byte_arr.getvalue()
|
||||
|
||||
|
||||
@ -276,10 +279,10 @@ class Image:
|
||||
proc = subprocess.Popen(
|
||||
cli,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(input=self.content)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
"Failed to execute ffmpeg command (cli=%r, err=%r)",
|
||||
|
@ -36,9 +36,12 @@ def get_mime_type(content: bytes) -> str:
|
||||
if content[0:4] == b"\x1A\x45\xDF\xA3":
|
||||
return "video/webm"
|
||||
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42", b"ftypM4V "):
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypiso6", b"ftypmp42", b"ftypM4V "):
|
||||
return "video/mp4"
|
||||
|
||||
if content[4:12] == b"ftypqt ":
|
||||
return "video/quicktime"
|
||||
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
@ -54,6 +57,7 @@ def get_extension(mime_type: str) -> Optional[str]:
|
||||
"image/heif": "heif",
|
||||
"image/heic": "heic",
|
||||
"video/mp4": "mp4",
|
||||
"video/quicktime": "mov",
|
||||
"video/webm": "webm",
|
||||
"application/octet-stream": "dat",
|
||||
}
|
||||
@ -65,7 +69,12 @@ def is_flash(mime_type: str) -> bool:
|
||||
|
||||
|
||||
def is_video(mime_type: str) -> bool:
|
||||
return mime_type.lower() in ("application/ogg", "video/mp4", "video/webm")
|
||||
return mime_type.lower() in (
|
||||
"application/ogg",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/webm",
|
||||
)
|
||||
|
||||
|
||||
def is_image(mime_type: str) -> bool:
|
||||
@ -88,6 +97,7 @@ def is_animated_gif(content: bytes) -> bool:
|
||||
and len(re.findall(pattern, content)) > 1
|
||||
)
|
||||
|
||||
|
||||
def is_heif(mime_type: str) -> bool:
|
||||
return mime_type.lower() in (
|
||||
"image/heif",
|
||||
|
@ -39,13 +39,20 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
||||
length_tally = 0
|
||||
try:
|
||||
with urllib.request.urlopen(request) as handle:
|
||||
while (chunk := handle.read(_dl_chunk_size)) :
|
||||
while chunk := handle.read(_dl_chunk_size):
|
||||
length_tally += len(chunk)
|
||||
if length_tally > config.config["max_dl_filesize"]:
|
||||
raise DownloadTooLargeError(url)
|
||||
raise DownloadTooLargeError(
|
||||
"Download target exceeds maximum. (%d)"
|
||||
% (config.config["max_dl_filesize"]),
|
||||
extra_fields={"URL": url},
|
||||
)
|
||||
content_buffer += chunk
|
||||
except urllib.error.HTTPError as ex:
|
||||
raise DownloadError(url) from ex
|
||||
raise DownloadError(
|
||||
"Download target returned HTTP %d. (%s)" % (ex.code, ex.reason),
|
||||
extra_fields={"URL": url},
|
||||
) from ex
|
||||
|
||||
if (
|
||||
youtube_dl_error
|
||||
@ -57,7 +64,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
||||
|
||||
|
||||
def _get_youtube_dl_content_url(url: str) -> str:
|
||||
cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
|
||||
cmd = ["yt-dlp", "--format", "best", "--no-playlist"]
|
||||
if config.config["user_agent"]:
|
||||
cmd.extend(["--user-agent", config.config["user_agent"]])
|
||||
cmd.extend(["--get-url", url])
|
||||
@ -69,7 +76,8 @@ def _get_youtube_dl_content_url(url: str) -> str:
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.ThirdPartyError(
|
||||
"Could not extract content location from %s" % (url)
|
||||
"Could not extract content location from URL.",
|
||||
extra_fields={"URL": url},
|
||||
) from None
|
||||
|
||||
|
||||
|
@ -108,6 +108,7 @@ class PoolSerializer(serialization.BaseSerializer):
|
||||
"lastEditTime": self.serialize_last_edit_time,
|
||||
"postCount": self.serialize_post_count,
|
||||
"posts": self.serialize_posts,
|
||||
"postsMicro": self.serialize_posts_micro,
|
||||
}
|
||||
|
||||
def serialize_id(self) -> Any:
|
||||
@ -143,6 +144,14 @@ class PoolSerializer(serialization.BaseSerializer):
|
||||
]
|
||||
]
|
||||
|
||||
def serialize_posts_micro(self) -> Any:
|
||||
posts_micro = []
|
||||
for i, rel in enumerate(self.pool.posts):
|
||||
posts_micro.append(posts.serialize_micro_post(rel, None))
|
||||
if i == 2:
|
||||
break
|
||||
return posts_micro
|
||||
|
||||
|
||||
def serialize_pool(
|
||||
pool: model.Pool, options: List[str] = []
|
||||
|
@ -1,12 +1,15 @@
|
||||
import hmac
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from itertools import tee, chain, islice
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from szurubooru import config, db, errors, model, rest
|
||||
from szurubooru.func import (
|
||||
auth,
|
||||
comments,
|
||||
files,
|
||||
image_hash,
|
||||
@ -15,7 +18,6 @@ from szurubooru.func import (
|
||||
pools,
|
||||
scores,
|
||||
serialization,
|
||||
snapshots,
|
||||
tags,
|
||||
users,
|
||||
util,
|
||||
@ -96,6 +98,13 @@ FLAG_MAP = {
|
||||
model.Post.FLAG_SOUND: "sound",
|
||||
}
|
||||
|
||||
# https://stackoverflow.com/a/1012089
|
||||
def _get_nearby_iter(post_list):
|
||||
previous_item, current_item, next_item = tee(post_list, 3)
|
||||
previous_item = chain([None], previous_item)
|
||||
next_item = chain(islice(next_item, 1, None), [None])
|
||||
return zip(previous_item, current_item, next_item)
|
||||
|
||||
|
||||
def get_post_security_hash(id: int) -> str:
|
||||
return hmac.new(
|
||||
@ -337,8 +346,10 @@ class PostSerializer(serialization.BaseSerializer):
|
||||
]
|
||||
|
||||
def serialize_pools(self) -> List[Any]:
|
||||
if not auth.has_privilege(self.auth_user, "pools:list"):
|
||||
return []
|
||||
return [
|
||||
pools.serialize_micro_pool(pool)
|
||||
{**pools.serialize_micro_pool(pool), **get_pool_posts_nearby(self.post, pool)} if auth.has_privilege(self.auth_user, "pools:view") else pools.serialize_micro_pool(pool)
|
||||
for pool in sorted(
|
||||
self.post.pools, key=lambda pool: pool.creation_time
|
||||
)
|
||||
@ -968,3 +979,38 @@ def search_by_image(image_content: bytes) -> List[Tuple[float, model.Post]]:
|
||||
]
|
||||
else:
|
||||
return []
|
||||
|
||||
def serialize_safe_post(
|
||||
post: Optional[model.Post]
|
||||
) -> rest.Response:
|
||||
return {"id": getattr(post, "post_id", None)} if post else None
|
||||
|
||||
|
||||
def serialize_id_post(
|
||||
post_id: Optional[int]
|
||||
) -> rest.Response:
|
||||
return serialize_safe_post(try_get_post_by_id(post_id)) if post_id else None
|
||||
|
||||
|
||||
def get_pool_posts_nearby(
|
||||
post: model.Post, pool: model.Pool
|
||||
) -> rest.Response:
|
||||
prev_post_id = None
|
||||
next_post_id = None
|
||||
first_post_id = pool.posts[0].post_id,
|
||||
last_post_id = pool.posts[-1].post_id,
|
||||
|
||||
for previous_item, current_item, next_item in _get_nearby_iter(pool.posts):
|
||||
if post.post_id == current_item.post_id:
|
||||
if previous_item != None:
|
||||
prev_post_id = previous_item.post_id
|
||||
if next_item != None:
|
||||
next_post_id = next_item.post_id
|
||||
break
|
||||
|
||||
return {
|
||||
"firstPost": serialize_id_post(first_post_id),
|
||||
"lastPost": serialize_id_post(last_post_id),
|
||||
"previousPost": serialize_id_post(prev_post_id),
|
||||
"nextPost": serialize_id_post(next_post_id),
|
||||
}
|
||||
|
@ -83,12 +83,12 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
|
||||
|
||||
def is_valid_email(email: Optional[str]) -> bool:
|
||||
""" Return whether given email address is valid or empty. """
|
||||
"""Return whether given email address is valid or empty."""
|
||||
return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None
|
||||
|
||||
|
||||
class dotdict(dict):
|
||||
""" dot.notation access to dictionary attributes. """
|
||||
"""dot.notation access to dictionary attributes."""
|
||||
|
||||
def __getattr__(self, attr: str) -> Any:
|
||||
return self.get(attr)
|
||||
@ -98,7 +98,7 @@ class dotdict(dict):
|
||||
|
||||
|
||||
def parse_time_range(value: str) -> Tuple[datetime, datetime]:
|
||||
""" Return tuple containing min/max time for given text representation. """
|
||||
"""Return tuple containing min/max time for given text representation."""
|
||||
one_day = timedelta(days=1)
|
||||
one_second = timedelta(seconds=1)
|
||||
almost_one_day = one_day - one_second
|
||||
|
@ -7,7 +7,7 @@ from szurubooru.rest.errors import HttpBadRequest
|
||||
|
||||
|
||||
def _authenticate_basic_auth(username: str, password: str) -> model.User:
|
||||
""" Try to authenticate user. Throw AuthError for invalid users. """
|
||||
"""Try to authenticate user. Throw AuthError for invalid users."""
|
||||
user = users.get_user_by_name(username)
|
||||
if not auth.is_valid_password(user, password):
|
||||
raise errors.AuthError("Invalid password.")
|
||||
@ -17,7 +17,7 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User:
|
||||
def _authenticate_token(
|
||||
username: str, token: str
|
||||
) -> Tuple[model.User, model.UserToken]:
|
||||
""" Try to authenticate user. Throw AuthError for invalid users. """
|
||||
"""Try to authenticate user. Throw AuthError for invalid users."""
|
||||
user = users.get_user_by_name(username)
|
||||
user_token = user_tokens.get_by_user_and_token(user, token)
|
||||
if not auth.is_valid_token(user_token):
|
||||
@ -72,7 +72,7 @@ def _get_user(ctx: rest.Context, bump_login: bool) -> Optional[model.User]:
|
||||
|
||||
|
||||
def process_request(ctx: rest.Context) -> None:
|
||||
""" Bind the user to request. Update last login time if needed. """
|
||||
"""Bind the user to request. Update last login time if needed."""
|
||||
bump_login = ctx.get_param_as_bool("bump-login", default=False)
|
||||
auth_user = _get_user(ctx, bump_login)
|
||||
if auth_user:
|
||||
|
@ -11,7 +11,7 @@ from szurubooru.rest import context, errors, middleware, routes
|
||||
|
||||
|
||||
def _json_serializer(obj: Any) -> str:
|
||||
""" JSON serializer for objects not serializable by default JSON code """
|
||||
"""JSON serializer for objects not serializable by default JSON code"""
|
||||
if isinstance(obj, datetime):
|
||||
serial = obj.isoformat("T") + "Z"
|
||||
return serial
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
from typing import Any, Dict, Optional, Tuple, Callable, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
@ -114,17 +114,60 @@ def _pool_filter(
|
||||
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
|
||||
) -> SaQuery:
|
||||
assert criterion
|
||||
return search_util.create_subquery_filter(
|
||||
model.Post.post_id,
|
||||
model.PoolPost.post_id,
|
||||
model.PoolPost.pool_id,
|
||||
search_util.create_num_filter,
|
||||
)(query, criterion, negated)
|
||||
from szurubooru.search.configs import util as search_util
|
||||
subquery = db.session.query(model.PoolPost.post_id.label("foreign_id"))
|
||||
subquery = subquery.options(sa.orm.lazyload("*"))
|
||||
subquery = search_util.create_num_filter(model.PoolPost.pool_id)(subquery, criterion, False)
|
||||
subquery = subquery.subquery("t")
|
||||
expression = model.Post.post_id.in_(subquery)
|
||||
if negated:
|
||||
expression = ~expression
|
||||
return query.filter(expression)
|
||||
|
||||
|
||||
def _pool_sort(
|
||||
query: SaQuery, pool_id: Optional[int], order: str
|
||||
) -> SaQuery:
|
||||
if pool_id is None:
|
||||
return query
|
||||
db_query = query.join(model.PoolPost, sa.and_(model.PoolPost.post_id == model.Post.post_id, model.PoolPost.pool_id == pool_id))
|
||||
if order == tokens.SortToken.SORT_DESC:
|
||||
return db_query.order_by(model.PoolPost.order.desc())
|
||||
return db_query.order_by(model.PoolPost.order.asc())
|
||||
|
||||
|
||||
def _category_filter(
|
||||
query: SaQuery, criterion: Optional[criteria.BaseCriterion], negated: bool
|
||||
) -> SaQuery:
|
||||
assert criterion
|
||||
|
||||
# Step 1. find the id for the category
|
||||
q1 = db.session.query(model.TagCategory.tag_category_id).filter(
|
||||
model.TagCategory.name == criterion.value
|
||||
)
|
||||
|
||||
# Step 2. find the tags with that category
|
||||
q2 = db.session.query(model.Tag.tag_id).filter(
|
||||
model.Tag.category_id.in_(q1)
|
||||
)
|
||||
|
||||
# Step 3. find all posts that have at least one of those tags
|
||||
q3 = db.session.query(model.PostTag.post_id).filter(
|
||||
model.PostTag.tag_id.in_(q2)
|
||||
)
|
||||
|
||||
# Step 4. profit
|
||||
expr = model.Post.post_id.in_(q3)
|
||||
if negated:
|
||||
expr = ~expr
|
||||
|
||||
return query.filter(expr)
|
||||
|
||||
|
||||
class PostSearchConfig(BaseSearchConfig):
|
||||
def __init__(self) -> None:
|
||||
self.user = None # type: Optional[model.User]
|
||||
self.pool_id = None # type: Optional[int]
|
||||
|
||||
def on_search_query_parsed(self, search_query: SearchQuery) -> SaQuery:
|
||||
new_special_tokens = []
|
||||
@ -149,6 +192,10 @@ class PostSearchConfig(BaseSearchConfig):
|
||||
else:
|
||||
new_special_tokens.append(token)
|
||||
search_query.special_tokens = new_special_tokens
|
||||
self.pool_id = None
|
||||
for token in search_query.named_tokens:
|
||||
if token.name == "pool" and isinstance(token.criterion, criteria.PlainCriterion):
|
||||
self.pool_id = token.criterion.value
|
||||
|
||||
def create_around_query(self) -> SaQuery:
|
||||
return db.session.query(model.Post).options(sa.orm.lazyload("*"))
|
||||
@ -349,11 +396,12 @@ class PostSearchConfig(BaseSearchConfig):
|
||||
),
|
||||
),
|
||||
(["pool"], _pool_filter),
|
||||
(["category"], _category_filter),
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def sort_columns(self) -> Dict[str, Tuple[SaColumn, str]]:
|
||||
def sort_columns(self) -> Dict[str, Union[Tuple[SaColumn, str], Callable[[SaQuery], None]]]:
|
||||
return util.unalias_dict(
|
||||
[
|
||||
(
|
||||
@ -415,6 +463,10 @@ class PostSearchConfig(BaseSearchConfig):
|
||||
["feature-date", "feature-time"],
|
||||
(model.Post.last_feature_time, self.SORT_DESC),
|
||||
),
|
||||
(
|
||||
["pool"],
|
||||
lambda subquery, order: _pool_sort(subquery, self.pool_id, order)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -205,6 +205,7 @@ def create_subquery_filter(
|
||||
filter_column: SaColumn,
|
||||
filter_factory: SaColumn,
|
||||
subquery_decorator: Callable[[SaQuery], None] = None,
|
||||
order: SaQuery = None,
|
||||
) -> Filter:
|
||||
filter_func = filter_factory(filter_column)
|
||||
|
||||
|
@ -181,14 +181,19 @@ class Executor:
|
||||
_format_dict_keys(self.config.sort_columns),
|
||||
)
|
||||
)
|
||||
column, default_order = self.config.sort_columns[
|
||||
entry = self.config.sort_columns[
|
||||
sort_token.name
|
||||
]
|
||||
order = _get_order(sort_token.order, default_order)
|
||||
if order == sort_token.SORT_ASC:
|
||||
db_query = db_query.order_by(column.asc())
|
||||
elif order == sort_token.SORT_DESC:
|
||||
db_query = db_query.order_by(column.desc())
|
||||
if callable(entry):
|
||||
order = _get_order(sort_token.order, sort_token.SORT_DESC)
|
||||
db_query = entry(db_query, order)
|
||||
else:
|
||||
column, default_order = entry
|
||||
order = _get_order(sort_token.order, default_order)
|
||||
if order == sort_token.SORT_ASC:
|
||||
db_query = db_query.order_by(column.asc())
|
||||
elif order == sort_token.SORT_DESC:
|
||||
db_query = db_query.order_by(column.desc())
|
||||
|
||||
db_query = self.config.finalize_query(db_query)
|
||||
return db_query
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, Union
|
||||
|
||||
SaColumn = Any
|
||||
SaQuery = Any
|
||||
|
@ -14,6 +14,7 @@ def inject_config(config_injector):
|
||||
"privileges": {
|
||||
"posts:list": model.User.RANK_REGULAR,
|
||||
"posts:view": model.User.RANK_REGULAR,
|
||||
"pools:list": model.User.RANK_REGULAR,
|
||||
},
|
||||
}
|
||||
)
|
||||
@ -125,3 +126,25 @@ def test_trying_to_retrieve_single_without_privileges(
|
||||
context_factory(user=user_factory(rank=model.User.RANK_ANONYMOUS)),
|
||||
{"post_id": 999},
|
||||
)
|
||||
|
||||
|
||||
def test_get_pool_post_around(user_factory, post_factory, pool_factory, pool_post_factory):
|
||||
p1 = post_factory(id=1)
|
||||
p2 = post_factory(id=2)
|
||||
p3 = post_factory(id=3)
|
||||
db.session.add_all([p1, p2, p3])
|
||||
|
||||
pool = pool_factory(id=1)
|
||||
db.session.add(pool)
|
||||
|
||||
pool_posts = [pool_post_factory(pool=pool, post=p1), pool_post_factory(pool=pool, post=p2), pool_post_factory(pool=pool, post=p3)]
|
||||
db.session.add_all(pool_posts)
|
||||
|
||||
result = posts.get_pool_posts_nearby(p1, pool)
|
||||
assert result["previousPost"] == None and result["nextPost"]["id"] == 2
|
||||
|
||||
result = posts.get_pool_posts_nearby(p2, pool)
|
||||
assert result["previousPost"]["id"] == 1 and result["nextPost"]["id"] == 3
|
||||
|
||||
result = posts.get_pool_posts_nearby(p3, pool)
|
||||
assert result["previousPost"]["id"] == 2 and result["nextPost"] == None
|
||||
|
@ -145,8 +145,9 @@ def test_trying_to_update_without_privileges(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("type", ["suggestions", "implications"])
|
||||
def test_trying_to_create_tags_without_privileges(
|
||||
config_injector, context_factory, tag_factory, user_factory
|
||||
config_injector, context_factory, tag_factory, user_factory, type
|
||||
):
|
||||
tag = tag_factory(names=["tag"])
|
||||
db.session.add(tag)
|
||||
@ -165,16 +166,7 @@ def test_trying_to_create_tags_without_privileges(
|
||||
with pytest.raises(errors.AuthError):
|
||||
api.tag_api.update_tag(
|
||||
context_factory(
|
||||
params={"suggestions": ["tag1", "tag2"], "version": 1},
|
||||
user=user_factory(rank=model.User.RANK_REGULAR),
|
||||
),
|
||||
{"tag_name": "tag"},
|
||||
)
|
||||
db.session.rollback()
|
||||
with pytest.raises(errors.AuthError):
|
||||
api.tag_api.update_tag(
|
||||
context_factory(
|
||||
params={"implications": ["tag1", "tag2"], "version": 1},
|
||||
params={type: ["tag1", "tag2"], "version": 1},
|
||||
user=user_factory(rank=model.User.RANK_REGULAR),
|
||||
),
|
||||
{"tag_name": "tag"},
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user