Compare commits
94 commits
Author | SHA1 | Date | |
---|---|---|---|
48d5dfb4e6 | |||
541fec20ca | |||
be280acb17 | |||
|
bc7c2c7867 | ||
2e7547d3bc | |||
39bb53528c | |||
5ef62b21a0 | |||
|
61b9f81e39 | ||
|
b721865931 | ||
|
46e3295003 | ||
|
031131506e | ||
|
d102578b54 | ||
|
6edb25d87b | ||
|
93fc15f2a4 | ||
|
4f9d46e1c2 | ||
|
b72e81850d | ||
|
c1c695f082 | ||
|
4b6b231fc8 | ||
|
6b0c3cfc7f | ||
|
4ec8cb3ba2 | ||
|
8d971234a2 | ||
|
a16bb198ab | ||
|
3f182a66ad | ||
|
b52363e82d | ||
|
3bf45e4c0a | ||
|
5596f53744 | ||
|
da425afc49 | ||
|
d7394d672f | ||
|
190d795426 | ||
|
7c92ceaf6a | ||
|
9e00f37464 | ||
|
59c497e168 | ||
|
c292b96f06 | ||
|
7a82e9d581 | ||
|
4806bbe0ed | ||
|
c2fdc2d070 | ||
|
ffdf115714 | ||
|
782f069031 | ||
|
81f7ae8034 | ||
|
648121d7c3 | ||
|
42524503b9 | ||
|
8a03015349 | ||
|
2165b59158 | ||
|
244a0f0b6c | ||
|
da3b4790ad | ||
|
196f92593c | ||
|
d7d2a151a8 | ||
|
75635bbc43 | ||
|
e3062b1c77 | ||
|
e950fe7ea5 | ||
|
86f50ec742 | ||
|
8088ff3bbe | ||
|
da71c672dd | ||
|
42bb364dd0 | ||
|
5b43c5bebd | ||
|
6c3b50d287 | ||
|
6075ae9326 | ||
|
70f2164dc6 | ||
|
1b9ce79f4e | ||
|
7e5d48b6e8 | ||
|
e746f09911 | ||
|
6088e89ea1 | ||
|
79d0efc25b | ||
|
929071ea1a | ||
|
514b846781 | ||
|
b2582b7b0f | ||
|
82541536af | ||
|
8ad9457b24 | ||
|
6de0a74257 | ||
|
a22485afda | ||
|
e2419a30ba | ||
|
d5a6609f75 | ||
|
106dcc4135 | ||
|
a14ead1842 | ||
|
780b7dc6fd | ||
|
9f95e9eb90 | ||
|
9b3123a815 | ||
|
f3aa0eb801 | ||
|
98c0941c97 | ||
|
a5fbaae4b3 | ||
|
d699979d35 | ||
|
d083084407 | ||
|
ad9d3599bc | ||
|
c3b81371d8 | ||
|
c64983002e | ||
|
4f57f49ebe | ||
|
f58079e12e | ||
|
be0c867d25 | ||
|
f5338ca508 | ||
|
e4a253fd25 | ||
|
414106a477 | ||
|
fa4997fbb9 | ||
|
516b3a51a7 | ||
|
f4ca435657 |
91 changed files with 1203 additions and 606 deletions
5
.gitattributes
vendored
Normal file
5
.gitattributes
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Shell scripts require LF
|
||||||
|
*.sh text eol=lf
|
108
.github/workflows/build-containers.yml
vendored
Normal file
108
.github/workflows/build-containers.yml
vendored
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
name: Build Docker containers
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
jobs:
|
||||||
|
build-client:
|
||||||
|
name: Build and push client/ Docker container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine metadata
|
||||||
|
run: |
|
||||||
|
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
|
||||||
|
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
|
||||||
|
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
|
||||||
|
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
|
||||||
|
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
|
||||||
|
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
|
||||||
|
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
|
||||||
|
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
|
||||||
|
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
|
||||||
|
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Build Info: ${BUILD_INFO}"
|
||||||
|
echo "Build Date: ${BUILD_DATE}"
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
run: >
|
||||||
|
docker buildx build --push
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
|
--build-arg BUILD_INFO=${{ env.build_info }}
|
||||||
|
--build-arg BUILD_DATE=${{ env.build_date }}
|
||||||
|
--build-arg SOURCE_COMMIT=$GITHUB_SHA
|
||||||
|
--build-arg DOCKER_REPO=szurubooru/client
|
||||||
|
-t "szurubooru/client:latest"
|
||||||
|
-t "szurubooru/client:${{ env.major_tag }}"
|
||||||
|
-t "szurubooru/client:${{ env.minor_tag }}"
|
||||||
|
./client
|
||||||
|
|
||||||
|
build-server:
|
||||||
|
name: Build and push server/ Docker container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Determine metadata
|
||||||
|
run: |
|
||||||
|
CLOSEST_VER="$(git describe --tags --abbrev=0 $GITHUB_SHA)"
|
||||||
|
CLOSEST_MAJOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f1)"
|
||||||
|
CLOSEST_MINOR_VER="$(echo ${CLOSEST_VER} | cut -d'.' -f2)"
|
||||||
|
SHORT_COMMIT=$(echo $GITHUB_SHA | cut -c1-8)
|
||||||
|
BUILD_INFO="v${CLOSEST_VER}-${SHORT_COMMIT}"
|
||||||
|
BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||||
|
|
||||||
|
echo "major_tag=${CLOSEST_MAJOR_VER}" >> $GITHUB_ENV
|
||||||
|
echo "minor_tag=${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}" >> $GITHUB_ENV
|
||||||
|
echo "build_info=${BUILD_INFO}" >> $GITHUB_ENV
|
||||||
|
echo "build_date=${BUILD_DATE}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
echo "Build Info: ${BUILD_INFO}"
|
||||||
|
echo "Build Date: ${BUILD_DATE}"
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build container
|
||||||
|
run: >
|
||||||
|
docker buildx build --push
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64/v8
|
||||||
|
--build-arg BUILD_DATE=${{ env.build_date }}
|
||||||
|
--build-arg SOURCE_COMMIT=$GITHUB_SHA
|
||||||
|
--build-arg DOCKER_REPO=szurubooru/server
|
||||||
|
-t "szurubooru/server:latest"
|
||||||
|
-t "szurubooru/server:${{ env.major_tag }}"
|
||||||
|
-t "szurubooru/server:${{ env.minor_tag }}"
|
||||||
|
./server
|
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
28
.github/workflows/run-unit-tests.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: Run unit tests
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test-server:
|
||||||
|
name: Run pytest for server/
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Build test container
|
||||||
|
run: >
|
||||||
|
docker buildx build --load
|
||||||
|
--platform linux/amd64 --target testing
|
||||||
|
-t test_container
|
||||||
|
./server
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: >
|
||||||
|
docker run --rm -t test_container
|
||||||
|
--color=no
|
||||||
|
--cov-report=term-missing:skip-covered
|
||||||
|
--cov=szurubooru
|
||||||
|
szurubooru/
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -13,3 +13,6 @@ server/**/lib/
|
||||||
server/**/bin/
|
server/**/bin/
|
||||||
server/**/pyvenv.cfg
|
server/**/pyvenv.cfg
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
|
data/
|
||||||
|
sql/
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
repos:
|
repos:
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.2.0
|
rev: v4.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: mixed-line-ending
|
- id: mixed-line-ending
|
||||||
|
|
||||||
|
|
||||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||||
rev: v1.1.9
|
rev: v1.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: remove-tabs
|
- id: remove-tabs
|
||||||
|
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 20.8b1
|
rev: '23.1.0'
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
files: 'server/'
|
files: 'server/'
|
||||||
types: [python]
|
types: [python]
|
||||||
language_version: python3.8
|
language_version: python3.9
|
||||||
|
|
||||||
- repo: https://github.com/timothycrosley/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: '5.4.2'
|
rev: '5.12.0'
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
files: 'server/'
|
files: 'server/'
|
||||||
|
@ -31,8 +32,8 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- toml
|
- toml
|
||||||
|
|
||||||
- repo: https://github.com/prettier/prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: '2.1.1'
|
rev: v2.7.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
files: client/js/
|
files: client/js/
|
||||||
|
@ -40,7 +41,7 @@ repos:
|
||||||
args: ['--config', 'client/.prettierrc.yml']
|
args: ['--config', 'client/.prettierrc.yml']
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||||
rev: v7.8.0
|
rev: v8.33.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: eslint
|
- id: eslint
|
||||||
files: client/js/
|
files: client/js/
|
||||||
|
@ -48,8 +49,8 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- eslint-config-prettier
|
- eslint-config-prettier
|
||||||
|
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: '3.8.3'
|
rev: '6.0.0'
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
files: server/szurubooru/
|
files: server/szurubooru/
|
||||||
|
@ -57,44 +58,5 @@ repos:
|
||||||
- flake8-print
|
- flake8-print
|
||||||
args: ['--config=server/.flake8']
|
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
|
fail_fast: true
|
||||||
exclude: LICENSE.md
|
exclude: LICENSE.md
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
Szurubooru is an image board engine inspired by services such as Danbooru,
|
Szurubooru is an image board engine inspired by services such as Danbooru,
|
||||||
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
|
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
|
||||||
its roots in Polish language and has onomatopeic meaning of scraping or
|
its roots in Polish language and has onomatopeic meaning of scraping or
|
||||||
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
||||||
- Ability to retrieve web video content using [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 comments
|
||||||
- Post notes / annotations, including arbitrary polygons
|
- Post notes / annotations, including arbitrary polygons
|
||||||
- Rich JSON REST API ([see documentation](doc/API.md))
|
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
FROM node:lts as builder
|
FROM --platform=$BUILDPLATFORM node:lts as builder
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm install -g npm@lts
|
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
@ -12,7 +11,7 @@ ARG CLIENT_BUILD_ARGS=""
|
||||||
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
|
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
|
||||||
|
|
||||||
|
|
||||||
FROM scratch as approot
|
FROM --platform=$BUILDPLATFORM scratch as approot
|
||||||
|
|
||||||
COPY docker-start.sh /
|
COPY docker-start.sh /
|
||||||
|
|
||||||
|
|
|
@ -61,4 +61,4 @@ $safety-sketchy = #F3D75F
|
||||||
$safety-unsafe = #F3985F
|
$safety-unsafe = #F3985F
|
||||||
$scrollbar-thumb-color = $main-color
|
$scrollbar-thumb-color = $main-color
|
||||||
$scrollbar-bg-color = $input-enabled-background-color
|
$scrollbar-bg-color = $input-enabled-background-color
|
||||||
$transparency-grid-square-color = #F0F0F0
|
$transparency-grid-square-color = #F0F0F0
|
|
@ -127,6 +127,10 @@ $comment-border-color = #DDD
|
||||||
color: mix($main-color, $inactive-link-color-darktheme)
|
color: mix($main-color, $inactive-link-color-darktheme)
|
||||||
|
|
||||||
.comment-content
|
.comment-content
|
||||||
|
p
|
||||||
|
word-wrap: normal
|
||||||
|
word-break: break-word
|
||||||
|
|
||||||
ul, ol
|
ul, ol
|
||||||
list-style-position: inside
|
list-style-position: inside
|
||||||
margin: 1em 0
|
margin: 1em 0
|
||||||
|
|
|
@ -240,6 +240,9 @@ nav
|
||||||
background: $focused-tab-background-color-darktheme
|
background: $focused-tab-background-color-darktheme
|
||||||
&#top-navigation
|
&#top-navigation
|
||||||
background: $top-navigation-color-darktheme
|
background: $top-navigation-color-darktheme
|
||||||
|
ul
|
||||||
|
#mobile-navigation-toggle
|
||||||
|
color: $text-color-darktheme
|
||||||
|
|
||||||
a .access-key
|
a .access-key
|
||||||
text-decoration: underline
|
text-decoration: underline
|
||||||
|
@ -275,7 +278,7 @@ a .access-key
|
||||||
background: darken($message-error-background-color, 60%)
|
background: darken($message-error-background-color, 60%)
|
||||||
&.success
|
&.success
|
||||||
border: 1px solid darken($message-success-border-color, 30%)
|
border: 1px solid darken($message-success-border-color, 30%)
|
||||||
background: darken($message-success-background-color, 60%)
|
background: darken($message-success-background-color, 80%)
|
||||||
|
|
||||||
.thumbnail
|
.thumbnail
|
||||||
/*background-image: attr(data-src url)*/ /* not available yet */
|
/*background-image: attr(data-src url)*/ /* not available yet */
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
z-index: 1
|
z-index: 1
|
||||||
span
|
span
|
||||||
position: relative
|
position: relative
|
||||||
background: white
|
background: $window-color
|
||||||
padding: 0 1em
|
padding: 0 1em
|
||||||
z-index: 2
|
z-index: 2
|
||||||
|
|
||||||
|
@ -31,3 +31,5 @@
|
||||||
.page-header
|
.page-header
|
||||||
&:before
|
&:before
|
||||||
background: $top-navigation-color-darktheme
|
background: $top-navigation-color-darktheme
|
||||||
|
span
|
||||||
|
background: $window-color-darktheme
|
||||||
|
|
|
@ -114,6 +114,29 @@
|
||||||
&[data-disabled]
|
&[data-disabled]
|
||||||
background: rgba(200, 200, 200, 0.7)
|
background: rgba(200, 200, 200, 0.7)
|
||||||
|
|
||||||
|
.delete-flipper
|
||||||
|
display: inline-block
|
||||||
|
padding: 0.5em
|
||||||
|
box-sizing: border-box
|
||||||
|
border: 0
|
||||||
|
&:after
|
||||||
|
display: inline-block
|
||||||
|
width: 1em
|
||||||
|
height: 1em
|
||||||
|
text-align: center
|
||||||
|
line-height: 1em
|
||||||
|
font-size: 2.2em
|
||||||
|
&.delete
|
||||||
|
background: rgba(255, 0, 0, 0.7)
|
||||||
|
&:after
|
||||||
|
color: white
|
||||||
|
font-family: FontAwesome;
|
||||||
|
content: "\f1f8"; // fa-trash
|
||||||
|
&:not(.delete)
|
||||||
|
background: rgba(200, 200, 200, 0.7)
|
||||||
|
&:after
|
||||||
|
color: white
|
||||||
|
content: '-'
|
||||||
|
|
||||||
.thumbnail
|
.thumbnail
|
||||||
width: 100%
|
width: 100%
|
||||||
|
@ -164,6 +187,9 @@
|
||||||
vertical-align: top
|
vertical-align: top
|
||||||
@media (max-width: 1000px)
|
@media (max-width: 1000px)
|
||||||
display: block
|
display: block
|
||||||
|
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
|
||||||
|
float: left
|
||||||
|
margin-right: 1em
|
||||||
input
|
input
|
||||||
margin-bottom: 0.25em
|
margin-bottom: 0.25em
|
||||||
margin-right: 0.25em
|
margin-right: 0.25em
|
||||||
|
@ -215,7 +241,19 @@
|
||||||
.append
|
.append
|
||||||
@media (max-width: 1000px)
|
@media (max-width: 1000px)
|
||||||
margin-left: 0
|
margin-left: 0
|
||||||
|
.bulk-edit-delete
|
||||||
|
&.opened
|
||||||
|
.start
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
margin-left: 0
|
||||||
|
&:not(.opened)
|
||||||
|
.start
|
||||||
|
display: none
|
||||||
|
.append.open
|
||||||
|
@media (max-width: 1000px)
|
||||||
|
margin-left: 0
|
||||||
|
.start
|
||||||
|
margin-left: 1em
|
||||||
.safety
|
.safety
|
||||||
margin-right: 0.25em
|
margin-right: 0.25em
|
||||||
&.safety-safe
|
&.safety-safe
|
||||||
|
|
|
@ -15,39 +15,53 @@
|
||||||
border: 0
|
border: 0
|
||||||
outline: 0
|
outline: 0
|
||||||
|
|
||||||
nav.buttons
|
>.sidebar>nav.buttons, >.content nav.buttons
|
||||||
margin-top: 0
|
margin-top: 0
|
||||||
display: flex
|
display: flex
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
article
|
article
|
||||||
flex: 1 0 33%
|
flex: 1 0 33%
|
||||||
a
|
a
|
||||||
display: inline-block
|
display: inline-block
|
||||||
width: 100%
|
width: 100%
|
||||||
padding: 0.3em 0
|
padding: 0.3em 0
|
||||||
text-align: center
|
|
||||||
vertical-align: middle
|
|
||||||
transition: background 0.2s linear
|
|
||||||
&:not(.inactive):hover
|
|
||||||
background: lighten($main-color, 90%)
|
|
||||||
i
|
|
||||||
font-size: 140%
|
|
||||||
text-align: center
|
text-align: center
|
||||||
@media (max-width: 800px)
|
vertical-align: middle
|
||||||
margin-top: 2em
|
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
|
>.content
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
.post-container
|
.post-container
|
||||||
margin-bottom: 2em
|
margin-bottom: 0.6em
|
||||||
|
|
||||||
.post-content
|
.post-content
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
|
.after-mobile-controls
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.darktheme .post-view
|
||||||
|
>.sidebar, >.content
|
||||||
|
nav.buttons
|
||||||
|
article
|
||||||
|
a:not(.inactive):hover
|
||||||
|
background: unset
|
||||||
|
box-shadow: inset 0 0 0 0.3em $main-color
|
||||||
|
|
||||||
@media (max-width: 800px)
|
@media (max-width: 800px)
|
||||||
.post-view
|
.post-view
|
||||||
flex-wrap: wrap
|
flex-wrap: wrap
|
||||||
|
>.after-mobile-controls
|
||||||
|
order: 3
|
||||||
>.sidebar
|
>.sidebar
|
||||||
order: 2
|
order: 2
|
||||||
min-width: 100%
|
min-width: 100%
|
||||||
|
@ -105,7 +119,6 @@
|
||||||
h1
|
h1
|
||||||
margin-bottom: 0.5em
|
margin-bottom: 0.5em
|
||||||
.thumbnail
|
.thumbnail
|
||||||
background-position: 50% 30%
|
|
||||||
width: 4em
|
width: 4em
|
||||||
height: 3em
|
height: 3em
|
||||||
li
|
li
|
||||||
|
|
|
@ -13,8 +13,12 @@ $cancel-button-color = tomato
|
||||||
|
|
||||||
&.inactive input[type=submit],
|
&.inactive input[type=submit],
|
||||||
&.inactive .skip-duplicates
|
&.inactive .skip-duplicates
|
||||||
|
&.inactive .always-upload-similar
|
||||||
|
&.inactive .pause-remain-on-error
|
||||||
&.uploading input[type=submit],
|
&.uploading input[type=submit],
|
||||||
&.uploading .skip-duplicates,
|
&.uploading .skip-duplicates,
|
||||||
|
&.uploading .always-upload-similar
|
||||||
|
&.uploading .pause-remain-on-error
|
||||||
&:not(.uploading) .cancel
|
&:not(.uploading) .cancel
|
||||||
display: none
|
display: none
|
||||||
|
|
||||||
|
@ -39,6 +43,12 @@ $cancel-button-color = tomato
|
||||||
.skip-duplicates
|
.skip-duplicates
|
||||||
margin-left: 1em
|
margin-left: 1em
|
||||||
|
|
||||||
|
.always-upload-similar
|
||||||
|
margin-left: 1em
|
||||||
|
|
||||||
|
.pause-remain-on-error
|
||||||
|
margin-left: 1em
|
||||||
|
|
||||||
form>.messages
|
form>.messages
|
||||||
margin-top: 1em
|
margin-top: 1em
|
||||||
|
|
||||||
|
@ -52,6 +62,14 @@ $cancel-button-color = tomato
|
||||||
margin: 0 0 1.2em 0
|
margin: 0 0 1.2em 0
|
||||||
padding-left: 13em
|
padding-left: 13em
|
||||||
|
|
||||||
|
img
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
video
|
||||||
|
width: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
&>.thumbnail-wrapper
|
&>.thumbnail-wrapper
|
||||||
float: left
|
float: left
|
||||||
width: 12em
|
width: 12em
|
||||||
|
|
|
@ -86,6 +86,12 @@ div.tag-input
|
||||||
font-size: 90%
|
font-size: 90%
|
||||||
unselectable()
|
unselectable()
|
||||||
|
|
||||||
|
@keyframes tag-added-to-post
|
||||||
|
from
|
||||||
|
max-height: 0
|
||||||
|
to
|
||||||
|
max-height: 5em
|
||||||
|
|
||||||
ul.compact-tags
|
ul.compact-tags
|
||||||
width: 100%
|
width: 100%
|
||||||
margin: 0.5em 0 0 0
|
margin: 0.5em 0 0 0
|
||||||
|
@ -103,18 +109,30 @@ ul.compact-tags
|
||||||
a:focus
|
a:focus
|
||||||
outline: 0
|
outline: 0
|
||||||
box-shadow: inset 0 0 0 2px $main-color
|
box-shadow: inset 0 0 0 2px $main-color
|
||||||
|
// these 3 added when tag is added to ul
|
||||||
|
&.added, &.new, &.implication
|
||||||
|
animation: tag-added-to-post 1s ease forwards
|
||||||
&.implication
|
&.implication
|
||||||
background: $implied-tag-background-color
|
|
||||||
color: $implied-tag-text-color
|
color: $implied-tag-text-color
|
||||||
|
background-color: $implied-tag-background-color
|
||||||
&.new
|
&.new
|
||||||
background: $new-tag-background-color
|
|
||||||
color: $new-tag-text-color
|
color: $new-tag-text-color
|
||||||
|
background-color: $new-tag-background-color
|
||||||
&.duplicate
|
&.duplicate
|
||||||
background: $duplicate-tag-background-color
|
|
||||||
color: $duplicate-tag-text-color
|
color: $duplicate-tag-text-color
|
||||||
|
background-color: $duplicate-tag-background-color
|
||||||
i
|
i
|
||||||
padding-right: 0.4em
|
padding-right: 0.4em
|
||||||
|
|
||||||
|
.darktheme ul.compact-tags
|
||||||
|
li
|
||||||
|
&.new
|
||||||
|
background-color: darken($new-tag-background-color, 80%)
|
||||||
|
&.implication
|
||||||
|
background-color: darken($implied-tag-background-color, 85%)
|
||||||
|
&.duplicate
|
||||||
|
background-color: darken($duplicate-tag-background-color, 80%)
|
||||||
|
|
||||||
div.tag-input, ul.compact-tags
|
div.tag-input, ul.compact-tags
|
||||||
.tag-usages, .tag-weight, .remove-tag
|
.tag-usages, .tag-weight, .remove-tag
|
||||||
color: $inactive-link-color
|
color: $inactive-link-color
|
||||||
|
@ -134,6 +152,8 @@ div.tag-input, ul.compact-tags
|
||||||
background: $window-color-darktheme
|
background: $window-color-darktheme
|
||||||
ul li:last-child
|
ul li:last-child
|
||||||
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
|
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
|
||||||
|
p
|
||||||
|
background: darken($tag-suggestions-header-color, 80%)
|
||||||
.append
|
.append
|
||||||
color: $inactive-link-color-darktheme
|
color: $inactive-link-color-darktheme
|
||||||
div.tag-input, ul.compact-tags
|
div.tag-input, ul.compact-tags
|
||||||
|
|
|
@ -21,10 +21,11 @@
|
||||||
.details
|
.details
|
||||||
font-size: 90%
|
font-size: 90%
|
||||||
line-height: 130%
|
line-height: 130%
|
||||||
|
.image
|
||||||
|
margin: 0.25em 0.6em 0.25em 0
|
||||||
.thumbnail
|
.thumbnail
|
||||||
width: 3em
|
width: 3em
|
||||||
height: 3em
|
height: 3em
|
||||||
margin: 0.25em 0.6em 0 0
|
|
||||||
|
|
||||||
.darktheme .user-list
|
.darktheme .user-list
|
||||||
ul li
|
ul li
|
||||||
|
|
|
@ -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
|
|
|
@ -2,7 +2,7 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'/>
|
<meta charset='utf-8'/>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||||
<meta name='theme-color' content='#24aadd'/>
|
<meta name='theme-color' content='#24aadd'/>
|
||||||
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<span class='vim-nav-hint'>Next post ></span>
|
<span class='vim-nav-hint'>Next post ></span>
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
|
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||||
<article class='edit-post'>
|
<article class='edit-post'>
|
||||||
<% if (ctx.editMode) { %>
|
<% if (ctx.editMode) { %>
|
||||||
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
||||||
|
@ -36,16 +37,13 @@
|
||||||
<span class='vim-nav-hint'>Back to view mode</span>
|
<span class='vim-nav-hint'>Back to view mode</span>
|
||||||
</a>
|
</a>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
<i class='fa fa-pencil'></i>
|
||||||
<% } else { %>
|
<span class='vim-nav-hint'>Edit post</span>
|
||||||
<a class='inactive'>
|
|
||||||
<% } %>
|
|
||||||
<i class='fa fa-pencil'></i>
|
|
||||||
<span class='vim-nav-hint'>Edit post</span>
|
|
||||||
</a>
|
</a>
|
||||||
<% } %>
|
<% } %>
|
||||||
</article>
|
</article>
|
||||||
|
<% } %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class='sidebar-container'></div>
|
<div class='sidebar-container'></div>
|
||||||
|
@ -54,13 +52,16 @@
|
||||||
<div class='content'>
|
<div class='content'>
|
||||||
<div class='post-container'></div>
|
<div class='post-container'></div>
|
||||||
|
|
||||||
<% if (ctx.canListComments) { %>
|
<div class='after-mobile-controls'>
|
||||||
<div class='comments-container'></div>
|
<div class='description'></div>
|
||||||
<% } %>
|
<% if (ctx.canCreateComments) { %>
|
||||||
|
<h2>Add comment</h2>
|
||||||
|
<div class='comment-form-container'></div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<% if (ctx.canCreateComments) { %>
|
<% if (ctx.canListComments) { %>
|
||||||
<h2>Add comment</h2>
|
<div class='comments-container'></div>
|
||||||
<div class='comment-form-container'></div>
|
<% } %>
|
||||||
<% } %>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
'image/heic': 'HEIC',
|
'image/heic': 'HEIC',
|
||||||
'video/webm': 'WEBM',
|
'video/webm': 'WEBM',
|
||||||
'video/mp4': 'MPEG-4',
|
'video/mp4': 'MPEG-4',
|
||||||
|
'video/quicktime': 'MOV',
|
||||||
'application/x-shockwave-flash': 'SWF',
|
'application/x-shockwave-flash': 'SWF',
|
||||||
}[ctx.post.mimeType] +
|
}[ctx.post.mimeType] +
|
||||||
' (' +
|
' (' +
|
||||||
|
|
|
@ -15,9 +15,10 @@
|
||||||
'image/heic': 'HEIC',
|
'image/heic': 'HEIC',
|
||||||
'video/webm': 'WEBM',
|
'video/webm': 'WEBM',
|
||||||
'video/mp4': 'MPEG-4',
|
'video/mp4': 'MPEG-4',
|
||||||
|
'video/quicktime': 'MOV',
|
||||||
'application/x-shockwave-flash': 'SWF',
|
'application/x-shockwave-flash': 'SWF',
|
||||||
}[ctx.post.mimeType] %>
|
}[ctx.post.mimeType] %><!--
|
||||||
</a>
|
--></a>
|
||||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||||
<% if (ctx.post.flags.length) { %><!--
|
<% if (ctx.post.flags.length) { %><!--
|
||||||
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||||
|
@ -57,7 +58,7 @@
|
||||||
Search on
|
Search on
|
||||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
<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://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>
|
||||||
|
|
||||||
<section class='social'>
|
<section class='social'>
|
||||||
|
@ -98,10 +99,10 @@
|
||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
--><%- ctx.getPrettyName(tag.names[0]) %> <!--
|
--><%- ctx.getPrettyName(tag.names[0]) %><!--
|
||||||
--><% if (ctx.canListPosts) { %><!--
|
--><% if (ctx.canListPosts) { %><!--
|
||||||
--></a><!--
|
--></a><!--
|
||||||
--><% } %><!--
|
--><% } %> <!--
|
||||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||||
--></li><!--
|
--></li><!--
|
||||||
--><% } %><!--
|
--><% } %><!--
|
||||||
|
|
|
@ -7,12 +7,28 @@
|
||||||
|
|
||||||
<span class='skip-duplicates'>
|
<span class='skip-duplicates'>
|
||||||
<%= ctx.makeCheckbox({
|
<%= ctx.makeCheckbox({
|
||||||
text: 'Skip duplicates',
|
text: 'Skip duplicate',
|
||||||
name: 'skip-duplicates',
|
name: 'skip-duplicates',
|
||||||
checked: false,
|
checked: false,
|
||||||
}) %>
|
}) %>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span class='always-upload-similar'>
|
||||||
|
<%= ctx.makeCheckbox({
|
||||||
|
text: 'Force upload similar',
|
||||||
|
name: 'always-upload-similar',
|
||||||
|
checked: false,
|
||||||
|
}) %>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class='pause-remain-on-error'>
|
||||||
|
<%= ctx.makeCheckbox({
|
||||||
|
text: 'Pause on error',
|
||||||
|
name: 'pause-remain-on-error',
|
||||||
|
checked: true,
|
||||||
|
}) %>
|
||||||
|
</span>
|
||||||
|
|
||||||
<input type='button' value='Cancel' class='cancel'/>
|
<input type='button' value='Cancel' class='cancel'/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
text: 'Upload anonymously',
|
text: 'Upload anonymously',
|
||||||
name: 'anonymous',
|
name: 'anonymous',
|
||||||
checked: ctx.uploadable.anonymous,
|
checked: ctx.uploadable.anonymous,
|
||||||
|
readonly: ctx.uploadable.forceAnonymous,
|
||||||
}) %>
|
}) %>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||||
%><span class='append hint'>Tagging with:</span><%
|
%><span class='append hint'>Tagging with:</span><%
|
||||||
%><a href class='mousetrap button append open'>Mass tag</a><%
|
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||||
%><wbr/><%
|
|
||||||
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||||
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||||
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||||
|
@ -28,4 +27,11 @@
|
||||||
%><a href class='mousetrap button append close'>Stop editing safety</a><%
|
%><a href class='mousetrap button append close'>Stop editing safety</a><%
|
||||||
%></form><%
|
%></form><%
|
||||||
%><% } %><%
|
%><% } %><%
|
||||||
|
%><% if (ctx.canBulkDelete) { %><%
|
||||||
|
%><form class='horizontal bulk-edit bulk-edit-delete'><%
|
||||||
|
%><a href class='mousetrap button append open'>Mass delete</a><%
|
||||||
|
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
|
||||||
|
%><a href class='mousetrap button append close'>Stop deleting</a><%
|
||||||
|
%></form><%
|
||||||
|
%><% } %><%
|
||||||
%></div>
|
%></div>
|
||||||
|
|
|
@ -50,6 +50,10 @@
|
||||||
<% } %>
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
|
||||||
|
<a href class='delete-flipper'>
|
||||||
|
</a>
|
||||||
|
<% } %>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
|
@ -17,7 +17,7 @@ class HomeController {
|
||||||
buildDate: config.meta.buildDate,
|
buildDate: config.meta.buildDate,
|
||||||
canListSnapshots: api.hasPrivilege("snapshots:list"),
|
canListSnapshots: api.hasPrivilege("snapshots:list"),
|
||||||
canListPosts: api.hasPrivilege("posts:list"),
|
canListPosts: api.hasPrivilege("posts:list"),
|
||||||
isDevelopmentMode: config.environment == "development"
|
isDevelopmentMode: config.environment == "development",
|
||||||
});
|
});
|
||||||
|
|
||||||
Info.get().then(
|
Info.get().then(
|
||||||
|
|
|
@ -91,16 +91,16 @@ class PoolController {
|
||||||
_evtUpdate(e) {
|
_evtUpdate(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
if (e.detail.names !== undefined) {
|
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||||
e.detail.pool.names = e.detail.names;
|
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;
|
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;
|
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();
|
e.detail.pool.posts.clear();
|
||||||
for (let postId of e.detail.posts) {
|
for (let postId of e.detail.posts) {
|
||||||
e.detail.pool.posts.add(
|
e.detail.pool.posts.add(
|
||||||
|
|
|
@ -43,6 +43,8 @@ class PoolListController {
|
||||||
this._headerView.addEventListener(
|
this._headerView.addEventListener(
|
||||||
"submit",
|
"submit",
|
||||||
(e) => this._evtSubmit(e),
|
(e) => this._evtSubmit(e),
|
||||||
|
);
|
||||||
|
this._headerView.addEventListener(
|
||||||
"navigate",
|
"navigate",
|
||||||
(e) => this._evtNavigate(e)
|
(e) => this._evtNavigate(e)
|
||||||
);
|
);
|
||||||
|
|
|
@ -44,6 +44,7 @@ class PostListController {
|
||||||
enableSafety: api.safetyEnabled(),
|
enableSafety: api.safetyEnabled(),
|
||||||
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
|
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
|
||||||
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
|
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
|
||||||
|
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||||
bulkEdit: {
|
bulkEdit: {
|
||||||
tags: this._bulkEditTags,
|
tags: this._bulkEditTags,
|
||||||
},
|
},
|
||||||
|
@ -52,6 +53,16 @@ class PostListController {
|
||||||
this._evtNavigate(e)
|
this._evtNavigate(e)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this._headerView._bulkDeleteEditor) {
|
||||||
|
this._headerView._bulkDeleteEditor.addEventListener(
|
||||||
|
"deleteSelectedPosts",
|
||||||
|
(e) => {
|
||||||
|
this._evtDeleteSelectedPosts(e);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._postsMarkedForDeletion = [];
|
||||||
this._syncPageController();
|
this._syncPageController();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +102,38 @@ class PostListController {
|
||||||
e.detail.post.save().catch((error) => window.alert(error.message));
|
e.detail.post.save().catch((error) => window.alert(error.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_evtMarkForDeletion(e) {
|
||||||
|
const postId = e.detail;
|
||||||
|
|
||||||
|
// Add or remove post from delete list
|
||||||
|
if (e.detail.delete) {
|
||||||
|
this._postsMarkedForDeletion.push(e.detail.post);
|
||||||
|
} else {
|
||||||
|
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
|
||||||
|
(x) => x.id != e.detail.post.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_evtDeleteSelectedPosts(e) {
|
||||||
|
if (this._postsMarkedForDeletion.length == 0) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Promise.all(
|
||||||
|
this._postsMarkedForDeletion.map((post) => post.delete())
|
||||||
|
)
|
||||||
|
.catch((error) => window.alert(error.message))
|
||||||
|
.then(() => {
|
||||||
|
this._postsMarkedForDeletion = [];
|
||||||
|
this._headerView._navigate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_syncPageController() {
|
_syncPageController() {
|
||||||
this._pageController.run({
|
this._pageController.run({
|
||||||
parameters: this._ctx.parameters,
|
parameters: this._ctx.parameters,
|
||||||
|
@ -117,8 +160,10 @@ class PostListController {
|
||||||
canBulkEditSafety: api.hasPrivilege(
|
canBulkEditSafety: api.hasPrivilege(
|
||||||
"posts:bulk-edit:safety"
|
"posts:bulk-edit:safety"
|
||||||
),
|
),
|
||||||
|
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||||
bulkEdit: {
|
bulkEdit: {
|
||||||
tags: this._bulkEditTags,
|
tags: this._bulkEditTags,
|
||||||
|
markedForDeletion: this._postsMarkedForDeletion,
|
||||||
},
|
},
|
||||||
postFlow: settings.get().postFlow,
|
postFlow: settings.get().postFlow,
|
||||||
});
|
});
|
||||||
|
@ -128,6 +173,9 @@ class PostListController {
|
||||||
view.addEventListener("changeSafety", (e) =>
|
view.addEventListener("changeSafety", (e) =>
|
||||||
this._evtChangeSafety(e)
|
this._evtChangeSafety(e)
|
||||||
);
|
);
|
||||||
|
view.addEventListener("markForDeletion", (e) =>
|
||||||
|
this._evtMarkForDeletion(e)
|
||||||
|
);
|
||||||
return view;
|
return view;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -169,24 +169,27 @@ class PostMainController extends BasePostController {
|
||||||
this._view.sidebarControl.disableForm();
|
this._view.sidebarControl.disableForm();
|
||||||
this._view.sidebarControl.clearMessages();
|
this._view.sidebarControl.clearMessages();
|
||||||
const post = e.detail.post;
|
const post = e.detail.post;
|
||||||
if (e.detail.safety !== undefined) {
|
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||||
post.safety = e.detail.safety;
|
post.safety = e.detail.safety;
|
||||||
}
|
}
|
||||||
if (e.detail.flags !== undefined) {
|
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||||
post.flags = e.detail.flags;
|
post.flags = e.detail.flags;
|
||||||
}
|
}
|
||||||
if (e.detail.relations !== undefined) {
|
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||||
post.relations = e.detail.relations;
|
post.relations = e.detail.relations;
|
||||||
}
|
}
|
||||||
if (e.detail.content !== undefined) {
|
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||||
post.newContent = e.detail.content;
|
post.newContent = e.detail.content;
|
||||||
}
|
}
|
||||||
if (e.detail.thumbnail !== undefined) {
|
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||||
post.newThumbnail = e.detail.thumbnail;
|
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.source = e.detail.source;
|
||||||
}
|
}
|
||||||
|
if (e.detail.desc !== undefined && e.detail.desc !== null) {
|
||||||
|
post.desc = e.detail.desc;
|
||||||
|
}
|
||||||
post.save().then(
|
post.save().then(
|
||||||
() => {
|
() => {
|
||||||
this._view.sidebarControl.showSuccess("Post saved.");
|
this._view.sidebarControl.showSuccess("Post saved.");
|
||||||
|
|
|
@ -12,7 +12,7 @@ const PostUploadView = require("../views/post_upload_view.js");
|
||||||
const EmptyView = require("../views/empty_view.js");
|
const EmptyView = require("../views/empty_view.js");
|
||||||
|
|
||||||
const genericErrorMessage =
|
const genericErrorMessage =
|
||||||
"One of the posts needs your attention; " +
|
"One or more posts needs your attention; " +
|
||||||
'click "resume upload" when you\'re ready.';
|
'click "resume upload" when you\'re ready.';
|
||||||
|
|
||||||
class PostUploadController {
|
class PostUploadController {
|
||||||
|
@ -55,6 +55,7 @@ class PostUploadController {
|
||||||
_evtSubmit(e) {
|
_evtSubmit(e) {
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
|
let anyFailures = false;
|
||||||
|
|
||||||
e.detail.uploadables
|
e.detail.uploadables
|
||||||
.reduce(
|
.reduce(
|
||||||
|
@ -62,11 +63,45 @@ class PostUploadController {
|
||||||
promise.then(() =>
|
promise.then(() =>
|
||||||
this._uploadSinglePost(
|
this._uploadSinglePost(
|
||||||
uploadable,
|
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()
|
Promise.resolve()
|
||||||
)
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (anyFailures) {
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
})
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
|
@ -75,31 +110,13 @@ class PostUploadController {
|
||||||
ctx.controller.showSuccess("Posts uploaded.");
|
ctx.controller.showSuccess("Posts uploaded.");
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.uploadable) {
|
this._view.showError(genericErrorMessage);
|
||||||
if (error.similarPosts) {
|
|
||||||
error.uploadable.lookalikes = error.similarPosts;
|
|
||||||
this._view.updateUploadable(error.uploadable);
|
|
||||||
this._view.showInfo(genericErrorMessage);
|
|
||||||
this._view.showInfo(
|
|
||||||
error.message,
|
|
||||||
error.uploadable
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this._view.showError(genericErrorMessage);
|
|
||||||
this._view.showError(
|
|
||||||
error.message,
|
|
||||||
error.uploadable
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._view.showError(error.message);
|
|
||||||
}
|
|
||||||
this._view.enableForm();
|
this._view.enableForm();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_uploadSinglePost(uploadable, skipDuplicates) {
|
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
|
||||||
progress.start();
|
progress.start();
|
||||||
let reverseSearchPromise = Promise.resolve();
|
let reverseSearchPromise = Promise.resolve();
|
||||||
if (!uploadable.lookalikesConfirmed) {
|
if (!uploadable.lookalikesConfirmed) {
|
||||||
|
@ -128,7 +145,10 @@ class PostUploadController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify about similar posts
|
// notify about similar posts
|
||||||
if (searchResult.similarPosts.length) {
|
if (
|
||||||
|
searchResult.similarPosts.length &&
|
||||||
|
!alwaysUploadSimilar
|
||||||
|
) {
|
||||||
let error = new Error(
|
let error = new Error(
|
||||||
`Found ${searchResult.similarPosts.length} similar ` +
|
`Found ${searchResult.similarPosts.length} similar ` +
|
||||||
"posts.\nYou can resume or discard this upload."
|
"posts.\nYou can resume or discard this upload."
|
||||||
|
|
|
@ -95,13 +95,13 @@ class TagController {
|
||||||
_evtUpdate(e) {
|
_evtUpdate(e) {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
if (e.detail.names !== undefined) {
|
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||||
e.detail.tag.names = e.detail.names;
|
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;
|
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.description = e.detail.description;
|
||||||
}
|
}
|
||||||
e.detail.tag.save().then(
|
e.detail.tag.save().then(
|
||||||
|
|
|
@ -31,9 +31,8 @@ class UserController {
|
||||||
userTokenPromise = UserToken.get(userName).then(
|
userTokenPromise = UserToken.get(userName).then(
|
||||||
(userTokens) => {
|
(userTokens) => {
|
||||||
return userTokens.map((token) => {
|
return userTokens.map((token) => {
|
||||||
token.isCurrentAuthToken = api.isCurrentAuthToken(
|
token.isCurrentAuthToken =
|
||||||
token
|
api.isCurrentAuthToken(token);
|
||||||
);
|
|
||||||
return token;
|
return token;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -176,21 +175,21 @@ class UserController {
|
||||||
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
||||||
const infix = isLoggedIn ? "self" : "any";
|
const infix = isLoggedIn ? "self" : "any";
|
||||||
|
|
||||||
if (e.detail.name !== undefined) {
|
if (e.detail.name !== undefined && e.detail.name !== null) {
|
||||||
e.detail.user.name = e.detail.name;
|
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;
|
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;
|
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;
|
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;
|
e.detail.user.avatarStyle = e.detail.avatarStyle;
|
||||||
if (e.detail.avatarContent) {
|
if (e.detail.avatarContent) {
|
||||||
e.detail.user.avatarContent = e.detail.avatarContent;
|
e.detail.user.avatarContent = e.detail.avatarContent;
|
||||||
|
@ -303,7 +302,7 @@ class UserController {
|
||||||
this._view.clearMessages();
|
this._view.clearMessages();
|
||||||
this._view.disableForm();
|
this._view.disableForm();
|
||||||
|
|
||||||
if (e.detail.note !== undefined) {
|
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||||
e.detail.userToken.note = e.detail.note;
|
e.detail.userToken.note = e.detail.note;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,9 +45,8 @@ class ExpanderControl {
|
||||||
// eslint-disable-next-line accessor-pairs
|
// eslint-disable-next-line accessor-pairs
|
||||||
set title(newTitle) {
|
set title(newTitle) {
|
||||||
if (this._expanderNode) {
|
if (this._expanderNode) {
|
||||||
this._expanderNode.querySelector(
|
this._expanderNode.querySelector("header span").textContent =
|
||||||
"header span"
|
newTitle;
|
||||||
).textContent = newTitle;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,12 @@ class FileDropperControl extends events.EventTarget {
|
||||||
this._urlInputNode.addEventListener("keydown", (e) =>
|
this._urlInputNode.addEventListener("keydown", (e) =>
|
||||||
this._evtUrlInputKeyDown(e)
|
this._evtUrlInputKeyDown(e)
|
||||||
);
|
);
|
||||||
|
this._urlInputNode.addEventListener("paste", (e) => {
|
||||||
|
// document.onpaste is used on the post-upload page.
|
||||||
|
// And this event is used on the post edit page.
|
||||||
|
if (document.getElementById("post-upload")) return;
|
||||||
|
this._evtPaste(e)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (this._urlConfirmButtonNode) {
|
if (this._urlConfirmButtonNode) {
|
||||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
this._urlConfirmButtonNode.addEventListener("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;
|
this._originalHtml = this._dropperNode.innerHTML;
|
||||||
views.replaceContent(target, source);
|
views.replaceContent(target, source);
|
||||||
}
|
}
|
||||||
|
@ -129,6 +140,17 @@ class FileDropperControl extends events.EventTarget {
|
||||||
this._emitFiles(e.dataTransfer.files);
|
this._emitFiles(e.dataTransfer.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_evtPaste(e) {
|
||||||
|
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||||
|
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
|
||||||
|
|
||||||
|
if (!this._options.allowMultiple && fileList.length > 1) {
|
||||||
|
window.alert("Cannot select multiple files.");
|
||||||
|
} else if (fileList.length > 0) {
|
||||||
|
this._emitFiles(fileList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_evtUrlInputKeyDown(e) {
|
_evtUrlInputKeyDown(e) {
|
||||||
if (e.which !== KEY_RETURN) {
|
if (e.which !== KEY_RETURN) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -103,6 +103,30 @@ class PostContentControl {
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshSize() {
|
_refreshSize() {
|
||||||
|
if (window.innerWidth <= 800) {
|
||||||
|
const buttons = document.querySelector(".sidebar > .buttons");
|
||||||
|
if (buttons) {
|
||||||
|
const content = document.querySelector(".content");
|
||||||
|
content.insertBefore(buttons, content.querySelector(".post-container + *"));
|
||||||
|
|
||||||
|
const afterControls = document.querySelector(".content > .after-mobile-controls");
|
||||||
|
if (afterControls) {
|
||||||
|
afterControls.parentElement.parentElement.appendChild(afterControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const buttons = document.querySelector(".content > .buttons");
|
||||||
|
if (buttons) {
|
||||||
|
const sidebar = document.querySelector(".sidebar");
|
||||||
|
sidebar.insertBefore(buttons, sidebar.firstElementChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterControls = document.querySelector(".content + .after-mobile-controls");
|
||||||
|
if (afterControls) {
|
||||||
|
document.querySelector(".content").appendChild(afterControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._currentFitFunction();
|
this._currentFitFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,9 +203,8 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this._formNode) {
|
if (this._formNode) {
|
||||||
const inputNodes = this._formNode.querySelectorAll(
|
const inputNodes =
|
||||||
"input, textarea"
|
this._formNode.querySelectorAll("input, textarea");
|
||||||
);
|
|
||||||
for (let node of inputNodes) {
|
for (let node of inputNodes) {
|
||||||
node.addEventListener("change", (e) =>
|
node.addEventListener("change", (e) =>
|
||||||
this.dispatchEvent(new CustomEvent("change"))
|
this.dispatchEvent(new CustomEvent("change"))
|
||||||
|
@ -428,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
thumbnail:
|
thumbnail:
|
||||||
this._newPostThumbnail !== undefined
|
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
|
||||||
? this._newPostThumbnail
|
? this._newPostThumbnail
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
||||||
|
|
|
@ -727,9 +727,8 @@ class PostNotesOverlayControl extends events.EventTarget {
|
||||||
}
|
}
|
||||||
|
|
||||||
_showNoteText(note) {
|
_showNoteText(note) {
|
||||||
this._textNode.querySelector(
|
this._textNode.querySelector(".wrapper").innerHTML =
|
||||||
".wrapper"
|
misc.formatMarkdown(note.text);
|
||||||
).innerHTML = misc.formatMarkdown(note.text);
|
|
||||||
this._textNode.style.display = "block";
|
this._textNode.style.display = "block";
|
||||||
const bodyRect = document.body.getBoundingClientRect();
|
const bodyRect = document.body.getBoundingClientRect();
|
||||||
const noteRect = this._textNode.getBoundingClientRect();
|
const noteRect = this._textNode.getBoundingClientRect();
|
||||||
|
|
|
@ -196,9 +196,10 @@ class TagInputControl extends events.EventTarget {
|
||||||
const listItemNode = this._createListItemNode(tag);
|
const listItemNode = this._createListItemNode(tag);
|
||||||
if (!tag.category) {
|
if (!tag.category) {
|
||||||
listItemNode.classList.add("new");
|
listItemNode.classList.add("new");
|
||||||
}
|
} else if (source === SOURCE_IMPLICATION) {
|
||||||
if (source === SOURCE_IMPLICATION) {
|
|
||||||
listItemNode.classList.add("implication");
|
listItemNode.classList.add("implication");
|
||||||
|
} else {
|
||||||
|
listItemNode.classList.add("added");
|
||||||
}
|
}
|
||||||
this._tagListNode.prependChild(listItemNode);
|
this._tagListNode.prependChild(listItemNode);
|
||||||
_fadeOutListItemNodeStatus(listItemNode);
|
_fadeOutListItemNodeStatus(listItemNode);
|
||||||
|
|
|
@ -4,12 +4,12 @@ const config = require("./config.js");
|
||||||
|
|
||||||
if (config.environment == "development") {
|
if (config.environment == "development") {
|
||||||
var ws = new WebSocket("ws://" + location.hostname + ":8080");
|
var ws = new WebSocket("ws://" + location.hostname + ":8080");
|
||||||
ws.addEventListener('open', function (event) {
|
ws.addEventListener("open", function (event) {
|
||||||
console.log("Live-reloading websocket connected.");
|
console.log("Live-reloading websocket connected.");
|
||||||
});
|
});
|
||||||
ws.addEventListener('message', (event) => {
|
ws.addEventListener("message", (event) => {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
if (event.data == 'reload'){
|
if (event.data == "reload") {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -114,6 +114,10 @@ class Post extends events.EventTarget {
|
||||||
return this._notes;
|
return this._notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get desc() {
|
||||||
|
return this._desc;
|
||||||
|
}
|
||||||
|
|
||||||
get comments() {
|
get comments() {
|
||||||
return this._comments;
|
return this._comments;
|
||||||
}
|
}
|
||||||
|
@ -271,12 +275,15 @@ class Post extends events.EventTarget {
|
||||||
if (this._newContent) {
|
if (this._newContent) {
|
||||||
files.content = this._newContent;
|
files.content = this._newContent;
|
||||||
}
|
}
|
||||||
if (this._newThumbnail !== undefined) {
|
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
|
||||||
files.thumbnail = this._newThumbnail;
|
files.thumbnail = this._newThumbnail;
|
||||||
}
|
}
|
||||||
if (this._source !== this._orig._source) {
|
if (this._source !== this._orig._source) {
|
||||||
detail.source = this._source;
|
detail.source = this._source;
|
||||||
}
|
}
|
||||||
|
if (this._desc !== this._orig._desc) {
|
||||||
|
detail.desc = this._desc;
|
||||||
|
}
|
||||||
|
|
||||||
let apiPromise = this._id
|
let apiPromise = this._id
|
||||||
? api.put(uri.formatApiLink("post", this.id), detail, files)
|
? api.put(uri.formatApiLink("post", this.id), detail, files)
|
||||||
|
|
|
@ -11,13 +11,13 @@ const defaultSettings = {
|
||||||
upscaleSmallPosts: false,
|
upscaleSmallPosts: false,
|
||||||
endlessScroll: false,
|
endlessScroll: false,
|
||||||
keyboardShortcuts: true,
|
keyboardShortcuts: true,
|
||||||
transparencyGrid: true,
|
transparencyGrid: false,
|
||||||
fitMode: "fit-both",
|
fitMode: "fit-both",
|
||||||
tagSuggestions: true,
|
tagSuggestions: true,
|
||||||
autoplayVideos: false,
|
autoplayVideos: false,
|
||||||
postsPerPage: 42,
|
postsPerPage: 42,
|
||||||
tagUnderscoresAsSpaces: false,
|
tagUnderscoresAsSpaces: false,
|
||||||
darkTheme: false,
|
darkTheme: true,
|
||||||
postFlow: false,
|
postFlow: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -65,17 +65,6 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
|
||||||
// post, user and tags permalinks
|
// post, user and tags permalinks
|
||||||
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
||||||
preprocess(text) {
|
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(
|
text = text.replace(
|
||||||
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
||||||
"$1[$2]($2)"
|
"$1[$2]($2)"
|
||||||
|
@ -136,12 +125,8 @@ function createRenderer() {
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
const renderer = new marked.Renderer();
|
||||||
renderer.image = (href, title, alt) => {
|
renderer.image = (href, title, alt) => {
|
||||||
let [
|
let [_, url, width, height] =
|
||||||
_,
|
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||||
url,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
|
||||||
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
|
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
|
||||||
if (width) {
|
if (width) {
|
||||||
res += '" width="' + width;
|
res += '" width="' + width;
|
||||||
|
@ -174,7 +159,7 @@ function formatMarkdown(text) {
|
||||||
for (let wrapper of wrappers) {
|
for (let wrapper of wrappers) {
|
||||||
text = wrapper.preprocess(text);
|
text = wrapper.preprocess(text);
|
||||||
}
|
}
|
||||||
text = marked(text, options);
|
text = marked.parse(text, options);
|
||||||
wrappers.reverse();
|
wrappers.reverse();
|
||||||
for (let wrapper of wrappers) {
|
for (let wrapper of wrappers) {
|
||||||
text = wrapper.postprocess(text);
|
text = wrapper.postprocess(text);
|
||||||
|
@ -200,7 +185,7 @@ function formatInlineMarkdown(text) {
|
||||||
for (let wrapper of wrappers) {
|
for (let wrapper of wrappers) {
|
||||||
text = wrapper.preprocess(text);
|
text = wrapper.preprocess(text);
|
||||||
}
|
}
|
||||||
text = marked.inlineLexer(text, [], options);
|
text = marked.parseInline(text, options);
|
||||||
wrappers.reverse();
|
wrappers.reverse();
|
||||||
for (let wrapper of wrappers) {
|
for (let wrapper of wrappers) {
|
||||||
text = wrapper.postprocess(text);
|
text = wrapper.postprocess(text);
|
||||||
|
|
|
@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTagLink(name, includeHash, includeCount, tag) {
|
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);
|
let text = misc.getPrettyName(name);
|
||||||
if (includeHash === true) {
|
if (includeHash === true) {
|
||||||
text = "#" + text;
|
text = "#" + text;
|
||||||
}
|
}
|
||||||
if (includeCount === true) {
|
if (includeCount === true) {
|
||||||
text += " (" + (tag ? tag.postCount : 0) + ")";
|
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
|
||||||
}
|
}
|
||||||
return api.hasPrivilege("tags:view")
|
return api.hasPrivilege("tags:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
|
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePoolLink(id, includeHash, includeCount, pool, name) {
|
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(
|
let text = misc.getPrettyName(
|
||||||
name ? name : pool ? pool.names[0] : "unknown"
|
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
|
||||||
);
|
);
|
||||||
if (includeHash === true) {
|
if (includeHash === true) {
|
||||||
text = "#" + text;
|
text = "#" + text;
|
||||||
}
|
}
|
||||||
if (includeCount === true) {
|
if (includeCount === true) {
|
||||||
text += " (" + (pool ? pool.postCount : 0) + ")";
|
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
|
||||||
}
|
}
|
||||||
return api.hasPrivilege("pools:view")
|
return api.hasPrivilege("pools:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
|
@ -264,7 +264,7 @@ function makeUserLink(user) {
|
||||||
let text = makeThumbnail(user ? user.avatarUrl : null);
|
let text = makeThumbnail(user ? user.avatarUrl : null);
|
||||||
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
||||||
const link =
|
const link =
|
||||||
user && api.hasPrivilege("users:view")
|
user && user.name && api.hasPrivilege("users:view")
|
||||||
? makeElement(
|
? makeElement(
|
||||||
"a",
|
"a",
|
||||||
{ href: uri.formatClientLink("user", user.name) },
|
{ href: uri.formatClientLink("user", user.name) },
|
||||||
|
|
|
@ -25,9 +25,8 @@ class PostMainView {
|
||||||
views.replaceContent(this._hostNode, sourceNode);
|
views.replaceContent(this._hostNode, sourceNode);
|
||||||
views.syncScrollPosition();
|
views.syncScrollPosition();
|
||||||
|
|
||||||
const topNavigationNode = document.body.querySelector(
|
const topNavigationNode =
|
||||||
"#top-navigation"
|
document.body.querySelector("#top-navigation");
|
||||||
);
|
|
||||||
|
|
||||||
this._postContentControl = new PostContentControl(
|
this._postContentControl = new PostContentControl(
|
||||||
postContainerNode,
|
postContainerNode,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const events = require("../events.js");
|
const events = require("../events.js");
|
||||||
|
const api = require("../api.js");
|
||||||
const views = require("../util/views.js");
|
const views = require("../util/views.js");
|
||||||
const FileDropperControl = require("../controls/file_dropper_control.js");
|
const FileDropperControl = require("../controls/file_dropper_control.js");
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ function _mimeTypeToPostType(mimeType) {
|
||||||
"image/heic": "image",
|
"image/heic": "image",
|
||||||
"video/mp4": "video",
|
"video/mp4": "video",
|
||||||
"video/webm": "video",
|
"video/webm": "video",
|
||||||
|
"video/quicktime": "video",
|
||||||
}[mimeType] || "unknown"
|
}[mimeType] || "unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -34,7 +36,8 @@ class Uploadable extends events.EventTarget {
|
||||||
this.flags = [];
|
this.flags = [];
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
this.relations = [];
|
this.relations = [];
|
||||||
this.anonymous = false;
|
this.anonymous = !api.isLoggedIn();
|
||||||
|
this.forceAnonymous = !api.isLoggedIn();
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {}
|
destroy() {}
|
||||||
|
@ -118,6 +121,7 @@ class Url extends Uploadable {
|
||||||
heif: "image/heif",
|
heif: "image/heif",
|
||||||
heic: "image/heic",
|
heic: "image/heic",
|
||||||
mp4: "video/mp4",
|
mp4: "video/mp4",
|
||||||
|
mov: "video/quicktime",
|
||||||
webm: "video/webm",
|
webm: "video/webm",
|
||||||
};
|
};
|
||||||
for (let extension of Object.keys(mime)) {
|
for (let extension of Object.keys(mime)) {
|
||||||
|
@ -283,7 +287,7 @@ class PostUploadView extends events.EventTarget {
|
||||||
for (let uploadable of this._uploadables) {
|
for (let uploadable of this._uploadables) {
|
||||||
this._updateUploadableFromDom(uploadable);
|
this._updateUploadableFromDom(uploadable);
|
||||||
}
|
}
|
||||||
this._submitButtonNode.value = "Resume upload";
|
this._submitButtonNode.value = "Resume";
|
||||||
this._emit("submit");
|
this._emit("submit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,6 +362,10 @@ class PostUploadView extends events.EventTarget {
|
||||||
detail: {
|
detail: {
|
||||||
uploadables: this._uploadables,
|
uploadables: this._uploadables,
|
||||||
skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
|
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]");
|
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() {
|
get _submitButtonNode() {
|
||||||
return this._hostNode.querySelector("form [type=submit]");
|
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 {
|
class PostsHeaderView extends events.EventTarget {
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
super();
|
super();
|
||||||
|
@ -186,6 +214,13 @@ class PostsHeaderView extends events.EventTarget {
|
||||||
this._bulkEditors.push(this._bulkSafetyEditor);
|
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) {
|
for (let editor of this._bulkEditors) {
|
||||||
editor.addEventListener("submit", (e) => {
|
editor.addEventListener("submit", (e) => {
|
||||||
this._navigate();
|
this._navigate();
|
||||||
|
@ -204,6 +239,8 @@ class PostsHeaderView extends events.EventTarget {
|
||||||
this._openBulkEditor(this._bulkTagEditor);
|
this._openBulkEditor(this._bulkTagEditor);
|
||||||
} else if (ctx.parameters.safety && this._bulkSafetyEditor) {
|
} else if (ctx.parameters.safety && this._bulkSafetyEditor) {
|
||||||
this._openBulkEditor(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");
|
return this._hostNode.querySelector(".bulk-edit-safety");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _bulkEditDeleteNode() {
|
||||||
|
return this._hostNode.querySelector(".bulk-edit-delete");
|
||||||
|
}
|
||||||
|
|
||||||
_openBulkEditor(editor) {
|
_openBulkEditor(editor) {
|
||||||
editor.toggleOpen(true);
|
editor.toggleOpen(true);
|
||||||
this._hideBulkEditorsExcept(editor);
|
this._hideBulkEditorsExcept(editor);
|
||||||
|
@ -253,9 +294,8 @@ class PostsHeaderView extends events.EventTarget {
|
||||||
e.target.classList.toggle("disabled");
|
e.target.classList.toggle("disabled");
|
||||||
const safety = e.target.getAttribute("data-safety");
|
const safety = e.target.getAttribute("data-safety");
|
||||||
let browsingSettings = settings.get();
|
let browsingSettings = settings.get();
|
||||||
browsingSettings.listPosts[safety] = !browsingSettings.listPosts[
|
browsingSettings.listPosts[safety] =
|
||||||
safety
|
!browsingSettings.listPosts[safety];
|
||||||
];
|
|
||||||
settings.save(browsingSettings, true);
|
settings.save(browsingSettings, true);
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("navigate", {
|
new CustomEvent("navigate", {
|
||||||
|
@ -294,6 +334,10 @@ class PostsHeaderView extends events.EventTarget {
|
||||||
this._bulkSafetyEditor && this._bulkSafetyEditor.opened
|
this._bulkSafetyEditor && this._bulkSafetyEditor.opened
|
||||||
? "1"
|
? "1"
|
||||||
: null;
|
: null;
|
||||||
|
parameters.delete =
|
||||||
|
this._bulkDeleteEditor && this._bulkDeleteEditor.opened
|
||||||
|
? "1"
|
||||||
|
: null;
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("navigate", { detail: { parameters: parameters } })
|
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();
|
this._syncBulkEditorsHighlights();
|
||||||
|
@ -56,6 +63,10 @@ class PostsPageView extends events.EventTarget {
|
||||||
return listItemNode.querySelector(".safety-flipper");
|
return listItemNode.querySelector(".safety-flipper");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getDeleteFlipperNode(listItemNode) {
|
||||||
|
return listItemNode.querySelector(".delete-flipper");
|
||||||
|
}
|
||||||
|
|
||||||
_evtPostChange(e) {
|
_evtPostChange(e) {
|
||||||
const listItemNode = this._postIdToListItemNode[e.detail.post.id];
|
const listItemNode = this._postIdToListItemNode[e.detail.post.id];
|
||||||
for (let node of listItemNode.querySelectorAll("[data-disabled]")) {
|
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() {
|
_syncBulkEditorsHighlights() {
|
||||||
for (let listItemNode of this._listItemNodes) {
|
for (let listItemNode of this._listItemNodes) {
|
||||||
const postId = listItemNode.getAttribute("data-post-id");
|
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) {
|
_evtDelete(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const userToken = this._tokens[
|
const userToken =
|
||||||
parseInt(e.target.getAttribute("data-token-id"))
|
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||||
];
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent("delete", {
|
new CustomEvent("delete", {
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -110,9 +109,8 @@ class UserTokenView extends events.EventTarget {
|
||||||
|
|
||||||
_evtChangeNoteClick(e) {
|
_evtChangeNoteClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const userToken = this._tokens[
|
const userToken =
|
||||||
parseInt(e.target.getAttribute("data-token-id"))
|
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||||
];
|
|
||||||
const text = window.prompt(
|
const text = window.prompt(
|
||||||
"Please enter the new name:",
|
"Please enter the new name:",
|
||||||
userToken.note !== null ? userToken.note : undefined
|
userToken.note !== null ? userToken.note : undefined
|
||||||
|
|
274
client/package-lock.json
generated
274
client/package-lock.json
generated
|
@ -10,7 +10,7 @@
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"ios-inner-height": "^1.0.3",
|
"ios-inner-height": "^1.0.3",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"marked": "^0.7.0",
|
"marked": "^4.0.10",
|
||||||
"mousetrap": "^1.6.2",
|
"mousetrap": "^1.6.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"superagent": "^3.8.3"
|
"superagent": "^3.8.3"
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
"jimp": "^0.13.0",
|
"jimp": "^0.13.0",
|
||||||
"pretty-error": "^3.0.3",
|
"pretty-error": "^3.0.3",
|
||||||
"stylus": "^0.54.8",
|
"stylus": "^0.54.8",
|
||||||
"terser": "^3.7.7",
|
"terser": "^4.8.1",
|
||||||
"underscore": "^1.12.1",
|
"underscore": "^1.12.1",
|
||||||
"watchify": "^4.0.0",
|
"watchify": "^4.0.0",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.4.6"
|
||||||
|
@ -476,24 +476,6 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/asn1.js": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||||
|
@ -1506,16 +1488,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cached-path-relative": {
|
"node_modules/cached-path-relative": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"get-intrinsic": "^1.0.2"
|
"get-intrinsic": "^1.0.2"
|
||||||
|
@ -1682,9 +1663,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/cookiejar": {
|
"node_modules/cookiejar": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||||
},
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.7",
|
||||||
|
@ -1850,9 +1831,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/decode-uri-component": {
|
"node_modules/decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
|
@ -2254,8 +2235,7 @@
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/get-assigned-identifiers": {
|
"node_modules/get-assigned-identifiers": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -2267,7 +2247,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
|
@ -2351,7 +2330,6 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.1"
|
"function-bind": "^1.1.1"
|
||||||
},
|
},
|
||||||
|
@ -2384,7 +2362,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
|
@ -2859,9 +2836,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/jpeg-js": {
|
"node_modules/jpeg-js": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/js-cookie": {
|
"node_modules/js-cookie": {
|
||||||
|
@ -2997,14 +2974,14 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "0.7.0",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
|
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/md5.js": {
|
"node_modules/md5.js": {
|
||||||
|
@ -3108,9 +3085,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minimist": {
|
"node_modules/minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
|
@ -3224,7 +3201,6 @@
|
||||||
"version": "1.10.3",
|
"version": "1.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
@ -3385,9 +3361,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-parse": {
|
"node_modules/path-parse": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/path-platform": {
|
"node_modules/path-platform": {
|
||||||
|
@ -3507,11 +3483,17 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.5.2",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/querystring": {
|
"node_modules/querystring": {
|
||||||
|
@ -3770,15 +3752,22 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.6.1",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||||
"dev": true,
|
"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": {
|
"dependencies": {
|
||||||
"array-filter": "~0.0.0",
|
"call-bind": "^1.0.0",
|
||||||
"array-map": "~0.0.0",
|
"get-intrinsic": "^1.0.2",
|
||||||
"array-reduce": "~0.0.0",
|
"object-inspect": "^1.9.0"
|
||||||
"jsonify": "~0.0.0"
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-concat": {
|
"node_modules/simple-concat": {
|
||||||
|
@ -3983,12 +3972,6 @@
|
||||||
"minimist": "^1.1.0"
|
"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": {
|
"node_modules/superagent": {
|
||||||
"version": "3.8.3",
|
"version": "3.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||||
|
@ -4028,26 +4011,26 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "3.7.7",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "~2.14.1",
|
"commander": "^2.20.0",
|
||||||
"source-map": "~0.6.1",
|
"source-map": "~0.6.1",
|
||||||
"source-map-support": "~0.5.6"
|
"source-map-support": "~0.5.12"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"terser": "bin/uglifyjs"
|
"terser": "bin/terser"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser/node_modules/commander": {
|
"node_modules/terser/node_modules/commander": {
|
||||||
"version": "2.14.1",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/terser/node_modules/source-map": {
|
"node_modules/terser/node_modules/source-map": {
|
||||||
|
@ -4060,9 +4043,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser/node_modules/source-map-support": {
|
"node_modules/terser/node_modules/source-map-support": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
|
@ -5047,24 +5030,6 @@
|
||||||
"picomatch": "^2.0.4"
|
"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": {
|
"asn1.js": {
|
||||||
"version": "4.10.1",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||||
|
@ -6053,16 +6018,15 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cached-path-relative": {
|
"cached-path-relative": {
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"call-bind": {
|
"call-bind": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"get-intrinsic": "^1.0.2"
|
"get-intrinsic": "^1.0.2"
|
||||||
|
@ -6211,9 +6175,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"cookiejar": {
|
"cookiejar": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||||
},
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "2.5.7",
|
"version": "2.5.7",
|
||||||
|
@ -6363,9 +6327,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"decode-uri-component": {
|
"decode-uri-component": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"define-properties": {
|
"define-properties": {
|
||||||
|
@ -6697,8 +6661,7 @@
|
||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"get-assigned-identifiers": {
|
"get-assigned-identifiers": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
@ -6710,7 +6673,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.1",
|
"function-bind": "^1.1.1",
|
||||||
"has": "^1.0.3",
|
"has": "^1.0.3",
|
||||||
|
@ -6778,7 +6740,6 @@
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.1"
|
"function-bind": "^1.1.1"
|
||||||
}
|
}
|
||||||
|
@ -6801,8 +6762,7 @@
|
||||||
"has-symbols": {
|
"has-symbols": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"hash-base": {
|
"hash-base": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
|
@ -7158,9 +7118,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"jpeg-js": {
|
"jpeg-js": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"js-cookie": {
|
"js-cookie": {
|
||||||
|
@ -7280,9 +7240,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"marked": {
|
"marked": {
|
||||||
"version": "0.7.0",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
|
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
|
||||||
},
|
},
|
||||||
"md5.js": {
|
"md5.js": {
|
||||||
"version": "1.3.4",
|
"version": "1.3.4",
|
||||||
|
@ -7364,9 +7324,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
|
@ -7466,8 +7426,7 @@
|
||||||
"object-inspect": {
|
"object-inspect": {
|
||||||
"version": "1.10.3",
|
"version": "1.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"object-keys": {
|
"object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
|
@ -7607,9 +7566,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-parse": {
|
"path-parse": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"path-platform": {
|
"path-platform": {
|
||||||
|
@ -7705,9 +7664,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.5.2",
|
"version": "6.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||||
|
"requires": {
|
||||||
|
"side-channel": "^1.0.4"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"querystring": {
|
"querystring": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
|
@ -7936,15 +7898,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shell-quote": {
|
"shell-quote": {
|
||||||
"version": "1.6.1",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||||
"dev": true,
|
"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": {
|
"requires": {
|
||||||
"array-filter": "~0.0.0",
|
"call-bind": "^1.0.0",
|
||||||
"array-map": "~0.0.0",
|
"get-intrinsic": "^1.0.2",
|
||||||
"array-reduce": "~0.0.0",
|
"object-inspect": "^1.9.0"
|
||||||
"jsonify": "~0.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"simple-concat": {
|
"simple-concat": {
|
||||||
|
@ -8116,14 +8082,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "^1.1.0"
|
"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": {
|
"superagent": {
|
||||||
|
@ -8159,20 +8117,20 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser": {
|
"terser": {
|
||||||
"version": "3.7.7",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"commander": "~2.14.1",
|
"commander": "^2.20.0",
|
||||||
"source-map": "~0.6.1",
|
"source-map": "~0.6.1",
|
||||||
"source-map-support": "~0.5.6"
|
"source-map-support": "~0.5.12"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": {
|
"commander": {
|
||||||
"version": "2.14.1",
|
"version": "2.20.3",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
|
@ -8182,9 +8140,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"source-map-support": {
|
"source-map-support": {
|
||||||
"version": "0.5.6",
|
"version": "0.5.21",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"buffer-from": "^1.0.0",
|
"buffer-from": "^1.0.0",
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node build.js",
|
"build": "node build.js",
|
||||||
"watch": "node build.js --watch"
|
"watch": "node build.js --watch",
|
||||||
|
"build-container": "docker build -t szurubooru/client:dev ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^2.0.17",
|
"dompurify": "^2.0.17",
|
||||||
"font-awesome": "^4.7.0",
|
"font-awesome": "^4.7.0",
|
||||||
"ios-inner-height": "^1.0.3",
|
"ios-inner-height": "^1.0.3",
|
||||||
"js-cookie": "^2.2.0",
|
"js-cookie": "^2.2.0",
|
||||||
"marked": "^0.7.0",
|
"marked": "^4.0.10",
|
||||||
"mousetrap": "^1.6.2",
|
"mousetrap": "^1.6.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"superagent": "^3.8.3"
|
"superagent": "^3.8.3"
|
||||||
|
@ -28,7 +29,7 @@
|
||||||
"jimp": "^0.13.0",
|
"jimp": "^0.13.0",
|
||||||
"pretty-error": "^3.0.3",
|
"pretty-error": "^3.0.3",
|
||||||
"stylus": "^0.54.8",
|
"stylus": "^0.54.8",
|
||||||
"terser": "^3.7.7",
|
"terser": "^4.8.1",
|
||||||
"underscore": "^1.12.1",
|
"underscore": "^1.12.1",
|
||||||
"watchify": "^4.0.0",
|
"watchify": "^4.0.0",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.4.6"
|
||||||
|
|
38
doc/API.md
38
doc/API.md
|
@ -37,6 +37,7 @@
|
||||||
- [Creating post](#creating-post)
|
- [Creating post](#creating-post)
|
||||||
- [Updating post](#updating-post)
|
- [Updating post](#updating-post)
|
||||||
- [Getting post](#getting-post)
|
- [Getting post](#getting-post)
|
||||||
|
- [Getting around post](#getting-around-post)
|
||||||
- [Deleting post](#deleting-post)
|
- [Deleting post](#deleting-post)
|
||||||
- [Merging posts](#merging-posts)
|
- [Merging posts](#merging-posts)
|
||||||
- [Rating post](#rating-post)
|
- [Rating post](#rating-post)
|
||||||
|
@ -53,7 +54,7 @@
|
||||||
- [Deleting pool category](#deleting-pool-category)
|
- [Deleting pool category](#deleting-pool-category)
|
||||||
- [Setting default pool category](#setting-default-pool-category)
|
- [Setting default pool category](#setting-default-pool-category)
|
||||||
- Pools
|
- Pools
|
||||||
- [Listing pools](#listing-pool)
|
- [Listing pools](#listing-pools)
|
||||||
- [Creating pool](#creating-pool)
|
- [Creating pool](#creating-pool)
|
||||||
- [Updating pool](#updating-pool)
|
- [Updating pool](#updating-pool)
|
||||||
- [Getting pool](#getting-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
|
accepts a file named `content`, the client should pass
|
||||||
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
|
`{"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
|
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)
|
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
|
||||||
to download content from popular sites such as youtube, gfycat, etc. Access to
|
download content from popular sites such as youtube, gfycat, etc. Access to
|
||||||
youtube-dl can be configured with the `'uploads:use_downloader'` permission
|
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
|
Finally, in some cases the user might want to reuse one file between the
|
||||||
requests to save the bandwidth (for example, reverse search + consecutive
|
requests to save the bandwidth (for example, reverse search + consecutive
|
||||||
|
@ -322,7 +323,7 @@ data.
|
||||||
{
|
{
|
||||||
"name": <name>,
|
"name": <name>,
|
||||||
"color": <color>,
|
"color": <color>,
|
||||||
"order": <order> // optional
|
"order": <order>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -951,6 +952,29 @@ data.
|
||||||
|
|
||||||
Retrieves information about an existing post.
|
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.
|
||||||
|
|
||||||
## Deleting post
|
## Deleting post
|
||||||
- **Request**
|
- **Request**
|
||||||
|
|
||||||
|
@ -1365,7 +1389,7 @@ data.
|
||||||
## Creating pool
|
## Creating pool
|
||||||
- **Request**
|
- **Request**
|
||||||
|
|
||||||
`POST /pools/create`
|
`POST /pool`
|
||||||
|
|
||||||
- **Input**
|
- **Input**
|
||||||
|
|
||||||
|
@ -2467,7 +2491,7 @@ One file together with its metadata posted to the site.
|
||||||
## Micro post
|
## Micro post
|
||||||
**Description**
|
**Description**
|
||||||
|
|
||||||
A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
|
A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields.
|
||||||
|
|
||||||
## Note
|
## Note
|
||||||
**Description**
|
**Description**
|
||||||
|
|
|
@ -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
|
Read the comments to guide you. Note that `.env` should be in the root
|
||||||
directory of this repository.
|
directory of this repository.
|
||||||
|
|
||||||
### Running the Application
|
4. Pull the containers:
|
||||||
|
|
||||||
Download containers:
|
This pulls the latest containers from docker.io:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose pull
|
user@host:szuru$ docker-compose pull
|
||||||
```
|
```
|
||||||
|
|
||||||
For first run, it is recommended to start the database separately:
|
If you have modified the application's source and would like to manually
|
||||||
```console
|
build it, follow the instructions in [**Building**](#Building) instead,
|
||||||
user@host:szuru$ docker-compose up -d sql
|
then read here once you're done.
|
||||||
```
|
|
||||||
|
|
||||||
To start all containers:
|
5. Run it!
|
||||||
```console
|
|
||||||
user@host:szuru$ docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
To view/monitor the application logs:
|
For first run, it is recommended to start the database separately:
|
||||||
```console
|
```console
|
||||||
user@host:szuru$ docker-compose logs -f
|
user@host:szuru$ docker-compose up -d sql
|
||||||
# (CTRL+C to exit)
|
```
|
||||||
```
|
|
||||||
|
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
|
### Additional Features
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,12 @@ BUILD_INFO=latest
|
||||||
# otherwise the port specified here will be publicly accessible
|
# otherwise the port specified here will be publicly accessible
|
||||||
PORT=8080
|
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
|
# URL base to run szurubooru under
|
||||||
# See "Additional Features" section in INSTALL.md
|
# See "Additional Features" section in INSTALL.md
|
||||||
BASE_URL=/
|
BASE_URL=/
|
||||||
|
|
|
@ -2,14 +2,16 @@
|
||||||
##
|
##
|
||||||
## 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
|
## orchestration services
|
||||||
version: '2'
|
#version: '2'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
server:
|
server:
|
||||||
image: szurubooru/server:latest
|
# image: szurubooru/server:latest
|
||||||
|
build: server
|
||||||
depends_on:
|
depends_on:
|
||||||
- sql
|
- sql
|
||||||
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
## These should be the names of the dependent containers listed below,
|
## These should be the names of the dependent containers listed below,
|
||||||
## or FQDNs/IP addresses if these services are running outside of Docker
|
## or FQDNs/IP addresses if these services are running outside of Docker
|
||||||
|
@ -21,12 +23,15 @@ services:
|
||||||
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
#POSTGRES_DB: defaults to same as POSTGRES_USER
|
||||||
#POSTGRES_PORT: 5432
|
#POSTGRES_PORT: 5432
|
||||||
#LOG_SQL: 0 (1 for verbose SQL logs)
|
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||||
|
THREADS:
|
||||||
volumes:
|
volumes:
|
||||||
- "${MOUNT_DATA}:/data"
|
- "${MOUNT_DATA}:/data"
|
||||||
- "./server/config.yaml:/opt/app/config.yaml"
|
- "./server/config.yaml:/opt/app/config.yaml"
|
||||||
|
|
||||||
client:
|
client:
|
||||||
image: szurubooru/client:latest
|
# image: szurubooru/client:latest
|
||||||
|
build: client
|
||||||
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
- server
|
- server
|
||||||
environment:
|
environment:
|
||||||
|
@ -43,5 +48,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER:
|
POSTGRES_USER:
|
||||||
POSTGRES_PASSWORD:
|
POSTGRES_PASSWORD:
|
||||||
|
# ports:
|
||||||
|
# - 5432:5432
|
||||||
volumes:
|
volumes:
|
||||||
- "${MOUNT_SQL}:/var/lib/postgresql/data"
|
- "${MOUNT_SQL}:/var/lib/postgresql/data"
|
||||||
|
|
|
@ -7,8 +7,13 @@ WORKDIR /opt/app
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
python3 \
|
python3 \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
ffmpeg \
|
|
||||||
py3-pip \
|
py3-pip \
|
||||||
|
build-base \
|
||||||
|
libheif \
|
||||||
|
libheif-dev \
|
||||||
|
libavif \
|
||||||
|
libavif-dev \
|
||||||
|
ffmpeg \
|
||||||
# from requirements.txt:
|
# from requirements.txt:
|
||||||
py3-yaml \
|
py3-yaml \
|
||||||
py3-psycopg2 \
|
py3-psycopg2 \
|
||||||
|
@ -18,26 +23,21 @@ RUN apk --no-cache add \
|
||||||
py3-pillow \
|
py3-pillow \
|
||||||
py3-pynacl \
|
py3-pynacl \
|
||||||
py3-tz \
|
py3-tz \
|
||||||
py3-pyrfc3339 \
|
py3-pyrfc3339
|
||||||
build-base \
|
RUN pip3 install --no-cache-dir --disable-pip-version-check \
|
||||||
&& apk --no-cache add \
|
"alembic>=0.8.5" \
|
||||||
libheif \
|
|
||||||
libavif \
|
|
||||||
libheif-dev \
|
|
||||||
libavif-dev \
|
|
||||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
|
||||||
alembic \
|
|
||||||
"coloredlogs==5.0" \
|
"coloredlogs==5.0" \
|
||||||
youtube_dl \
|
"pyheif==0.6.1" \
|
||||||
pillow-avif-plugin \
|
"heif-image-plugin>=0.3.2" \
|
||||||
pyheif-pillow-opener \
|
yt-dlp \
|
||||||
&& apk --no-cache del py3-pip
|
"pillow-avif-plugin~=1.1.0"
|
||||||
|
RUN apk --no-cache del py3-pip
|
||||||
|
|
||||||
COPY ./ /opt/app/
|
COPY ./ /opt/app/
|
||||||
RUN rm -rf /opt/app/szurubooru/tests
|
RUN rm -rf /opt/app/szurubooru/tests
|
||||||
|
|
||||||
|
|
||||||
FROM prereqs as testing
|
FROM --platform=$BUILDPLATFORM prereqs as testing
|
||||||
WORKDIR /opt/app
|
WORKDIR /opt/app
|
||||||
|
|
||||||
RUN apk --no-cache add \
|
RUN apk --no-cache add \
|
||||||
|
@ -83,6 +83,9 @@ ARG PORT=6666
|
||||||
ENV PORT=${PORT}
|
ENV PORT=${PORT}
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
|
||||||
|
ARG THREADS=4
|
||||||
|
ENV THREADS=${THREADS}
|
||||||
|
|
||||||
VOLUME ["/data/"]
|
VOLUME ["/data/"]
|
||||||
|
|
||||||
ARG DOCKER_REPO
|
ARG DOCKER_REPO
|
||||||
|
|
|
@ -103,6 +103,7 @@ privileges:
|
||||||
'posts:edit:content': power
|
'posts:edit:content': power
|
||||||
'posts:edit:flags': regular
|
'posts:edit:flags': regular
|
||||||
'posts:edit:notes': regular
|
'posts:edit:notes': regular
|
||||||
|
'posts:edit:desc': regular
|
||||||
'posts:edit:relations': regular
|
'posts:edit:relations': regular
|
||||||
'posts:edit:safety': power
|
'posts:edit:safety': power
|
||||||
'posts:edit:source': regular
|
'posts:edit:source': regular
|
||||||
|
@ -115,6 +116,7 @@ privileges:
|
||||||
'posts:favorite': regular
|
'posts:favorite': regular
|
||||||
'posts:bulk-edit:tags': power
|
'posts:bulk-edit:tags': power
|
||||||
'posts:bulk-edit:safety': power
|
'posts:bulk-edit:safety': power
|
||||||
|
'posts:bulk-edit:delete': power
|
||||||
|
|
||||||
'tags:create': regular
|
'tags:create': regular
|
||||||
'tags:edit:names': power
|
'tags:edit:names': power
|
||||||
|
|
|
@ -4,5 +4,5 @@ cd /opt/app
|
||||||
|
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
|
|
||||||
echo "Starting szurubooru API on port ${PORT}"
|
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||||
exec waitress-serve-3 --port ${PORT} szurubooru.facade:app
|
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
|
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
|
certifi>=2017.11.5
|
||||||
|
coloredlogs==5.0
|
||||||
|
heif-image-plugin==0.3.2
|
||||||
numpy>=1.8.2
|
numpy>=1.8.2
|
||||||
|
pillow-avif-plugin~=1.1.0
|
||||||
pillow>=4.3.0
|
pillow>=4.3.0
|
||||||
|
psycopg2-binary>=2.6.1
|
||||||
|
pyheif==0.6.1
|
||||||
pynacl>=1.2.1
|
pynacl>=1.2.1
|
||||||
pytz>=2018.3
|
|
||||||
pyRFC3339>=1.0
|
pyRFC3339>=1.0
|
||||||
pillow-avif-plugin>=1.1.0
|
pytz>=2018.3
|
||||||
pyheif-pillow-opener>=0.1.0
|
pyyaml>=3.11
|
||||||
youtube_dl
|
SQLAlchemy>=1.0.12, <1.4
|
||||||
|
yt-dlp
|
||||||
|
|
|
@ -91,6 +91,15 @@ def reset_filenames() -> None:
|
||||||
rename_in_dir("posts/custom-thumbnails/")
|
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:
|
def main() -> None:
|
||||||
parser_top = ArgumentParser(
|
parser_top = ArgumentParser(
|
||||||
description="Collection of CLI commands for an administrator to use",
|
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 "
|
help="reset and rename the content and thumbnail "
|
||||||
"filenames in case of a lost/changed secret key",
|
"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()
|
command = parser_top.parse_args()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -123,6 +138,8 @@ def main() -> None:
|
||||||
check_audio()
|
check_audio()
|
||||||
elif command.reset_filenames:
|
elif command.reset_filenames:
|
||||||
reset_filenames()
|
reset_filenames()
|
||||||
|
elif command.regenerate_thumbnails:
|
||||||
|
regenerate_thumbnails()
|
||||||
except errors.BaseError as e:
|
except errors.BaseError as e:
|
||||||
print(e, file=stderr)
|
print(e, file=stderr)
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ def create_post(
|
||||||
source = ctx.get_param_as_string("contentUrl", default="")
|
source = ctx.get_param_as_string("contentUrl", default="")
|
||||||
relations = ctx.get_param_as_int_list("relations", default=[])
|
relations = ctx.get_param_as_int_list("relations", default=[])
|
||||||
notes = ctx.get_param_as_list("notes", default=[])
|
notes = ctx.get_param_as_list("notes", default=[])
|
||||||
|
desc = ctx.get_param_as_string("desc", default="")
|
||||||
flags = ctx.get_param_as_string_list(
|
flags = ctx.get_param_as_string_list(
|
||||||
"flags", default=posts.get_default_flags(content)
|
"flags", default=posts.get_default_flags(content)
|
||||||
)
|
)
|
||||||
|
@ -85,6 +86,7 @@ def create_post(
|
||||||
posts.update_post_source(post, source)
|
posts.update_post_source(post, source)
|
||||||
posts.update_post_relations(post, relations)
|
posts.update_post_relations(post, relations)
|
||||||
posts.update_post_notes(post, notes)
|
posts.update_post_notes(post, notes)
|
||||||
|
posts.update_post_desc(post, desc)
|
||||||
posts.update_post_flags(post, flags)
|
posts.update_post_flags(post, flags)
|
||||||
if ctx.has_file("thumbnail"):
|
if ctx.has_file("thumbnail"):
|
||||||
posts.update_post_thumbnail(post, ctx.get_file("thumbnail"))
|
posts.update_post_thumbnail(post, ctx.get_file("thumbnail"))
|
||||||
|
@ -159,6 +161,9 @@ def update_post(ctx: rest.Context, params: Dict[str, str]) -> rest.Response:
|
||||||
if ctx.has_param("notes"):
|
if ctx.has_param("notes"):
|
||||||
auth.verify_privilege(ctx.user, "posts:edit:notes")
|
auth.verify_privilege(ctx.user, "posts:edit:notes")
|
||||||
posts.update_post_notes(post, ctx.get_param_as_list("notes"))
|
posts.update_post_notes(post, ctx.get_param_as_list("notes"))
|
||||||
|
if ctx.has_param("desc"):
|
||||||
|
auth.verify_privilege(ctx.user, "posts:edit:desc")
|
||||||
|
posts.update_post_desc(post, ctx.get_param_as_string("desc"))
|
||||||
if ctx.has_param("flags"):
|
if ctx.has_param("flags"):
|
||||||
auth.verify_privilege(ctx.user, "posts:edit:flags")
|
auth.verify_privilege(ctx.user, "posts:edit:flags")
|
||||||
posts.update_post_flags(post, ctx.get_param_as_string_list("flags"))
|
posts.update_post_flags(post, ctx.get_param_as_string_list("flags"))
|
||||||
|
|
|
@ -21,7 +21,7 @@ def _merge(left: Dict, right: Dict) -> Dict:
|
||||||
return left
|
return left
|
||||||
|
|
||||||
|
|
||||||
def _docker_config() -> Dict:
|
def _container_config() -> Dict:
|
||||||
if "TEST_ENVIRONMENT" not in os.environ:
|
if "TEST_ENVIRONMENT" not in os.environ:
|
||||||
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
||||||
if key not in os.environ:
|
if key not in os.environ:
|
||||||
|
@ -33,7 +33,7 @@ def _docker_config() -> Dict:
|
||||||
"show_sql": int(os.getenv("LOG_SQL", 0)),
|
"show_sql": int(os.getenv("LOG_SQL", 0)),
|
||||||
"data_url": os.getenv("DATA_URL", "data/"),
|
"data_url": os.getenv("DATA_URL", "data/"),
|
||||||
"data_dir": "/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"),
|
"user": os.getenv("POSTGRES_USER"),
|
||||||
"pass": os.getenv("POSTGRES_PASSWORD"),
|
"pass": os.getenv("POSTGRES_PASSWORD"),
|
||||||
|
@ -49,6 +49,15 @@ def _file_config(filename: str) -> Dict:
|
||||||
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
|
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:
|
def _read_config() -> Dict:
|
||||||
ret = _file_config("config.yaml.dist")
|
ret = _file_config("config.yaml.dist")
|
||||||
if os.path.isfile("config.yaml"):
|
if os.path.isfile("config.yaml"):
|
||||||
|
@ -57,8 +66,8 @@ def _read_config() -> Dict:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"'config.yaml' should be a file, not a directory, skipping"
|
"'config.yaml' should be a file, not a directory, skipping"
|
||||||
)
|
)
|
||||||
if os.path.exists("/.dockerenv"):
|
if _running_inside_container():
|
||||||
ret = _merge(ret, _docker_config())
|
ret = _merge(ret, _container_config())
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ _live_migrations = (
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> Callable[[Any, Any], Any]:
|
def create_app() -> Callable[[Any, Any], Any]:
|
||||||
""" Create a WSGI compatible App object. """
|
"""Create a WSGI compatible App object."""
|
||||||
validate_config()
|
validate_config()
|
||||||
coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s")
|
coloredlogs.install(fmt="[%(asctime)-15s] %(name)s %(message)s")
|
||||||
if config.config["debug"]:
|
if config.config["debug"]:
|
||||||
|
|
|
@ -25,7 +25,7 @@ RANK_MAP = OrderedDict(
|
||||||
|
|
||||||
|
|
||||||
def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
def get_password_hash(salt: str, password: str) -> Tuple[str, int]:
|
||||||
""" Retrieve argon2id password hash. """
|
"""Retrieve argon2id password hash."""
|
||||||
return (
|
return (
|
||||||
pwhash.argon2id.str(
|
pwhash.argon2id.str(
|
||||||
(config.config["secret"] + salt + password).encode("utf8")
|
(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(
|
def get_sha256_legacy_password_hash(
|
||||||
salt: str, password: str
|
salt: str, password: str
|
||||||
) -> Tuple[str, int]:
|
) -> Tuple[str, int]:
|
||||||
""" Retrieve old-style sha256 password hash. """
|
"""Retrieve old-style sha256 password hash."""
|
||||||
digest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
digest.update(config.config["secret"].encode("utf8"))
|
digest.update(config.config["secret"].encode("utf8"))
|
||||||
digest.update(salt.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]:
|
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 = hashlib.sha1()
|
||||||
digest.update(b"1A2/$_4xVa")
|
digest.update(b"1A2/$_4xVa")
|
||||||
digest.update(salt.encode("utf8"))
|
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:
|
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
|
assert user
|
||||||
digest = hashlib.md5()
|
digest = hashlib.md5()
|
||||||
digest.update(config.config["secret"].encode("utf8"))
|
digest.update(config.config["secret"].encode("utf8"))
|
||||||
|
|
|
@ -4,12 +4,10 @@ from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Callable, List, Optional, Set, Tuple
|
from typing import Any, Callable, List, Optional, Set, Tuple
|
||||||
|
|
||||||
|
import HeifImagePlugin
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PIL import Image
|
|
||||||
import pillow_avif
|
import pillow_avif
|
||||||
import pyheif
|
from PIL import Image
|
||||||
from pyheif_pillow_opener import register_heif_opener
|
|
||||||
register_heif_opener()
|
|
||||||
|
|
||||||
from szurubooru import config, errors
|
from szurubooru import config, errors
|
||||||
|
|
||||||
|
@ -44,7 +42,7 @@ def _preprocess_image(content: bytes) -> NpMatrix:
|
||||||
try:
|
try:
|
||||||
img = Image.open(BytesIO(content))
|
img = Image.open(BytesIO(content))
|
||||||
return np.asarray(img.convert("L"), dtype=np.uint8)
|
return np.asarray(img.convert("L"), dtype=np.uint8)
|
||||||
except IOError:
|
except (IOError, ValueError):
|
||||||
raise errors.ProcessingError(
|
raise errors.ProcessingError(
|
||||||
"Unable to generate a signature hash " "for this image."
|
"Unable to generate a signature hash " "for this image."
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,9 @@ import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
import HeifImagePlugin
|
||||||
|
import pillow_avif
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
|
|
||||||
from szurubooru import errors
|
from szurubooru import errors
|
||||||
|
@ -17,7 +20,7 @@ logger = logging.getLogger(__name__)
|
||||||
def convert_heif_to_png(content: bytes) -> bytes:
|
def convert_heif_to_png(content: bytes) -> bytes:
|
||||||
img = PILImage.open(BytesIO(content))
|
img = PILImage.open(BytesIO(content))
|
||||||
img_byte_arr = BytesIO()
|
img_byte_arr = BytesIO()
|
||||||
img.save(img_byte_arr, format='PNG')
|
img.save(img_byte_arr, format="PNG")
|
||||||
return img_byte_arr.getvalue()
|
return img_byte_arr.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
@ -96,7 +99,7 @@ class Image:
|
||||||
"-f",
|
"-f",
|
||||||
"lavfi",
|
"lavfi",
|
||||||
"-i",
|
"-i",
|
||||||
"color=white:s=%dx%d" % (self.width, self.height),
|
"color=black:s=%dx%d" % (self.width, self.height),
|
||||||
"-i",
|
"-i",
|
||||||
"{path}",
|
"{path}",
|
||||||
"-f",
|
"-f",
|
||||||
|
@ -276,10 +279,10 @@ class Image:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
cli,
|
cli,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.DEVNULL,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.PIPE,
|
||||||
)
|
)
|
||||||
out, err = proc.communicate(input=self.content)
|
out, err = proc.communicate()
|
||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Failed to execute ffmpeg command (cli=%r, err=%r)",
|
"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":
|
if content[0:4] == b"\x1A\x45\xDF\xA3":
|
||||||
return "video/webm"
|
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"
|
return "video/mp4"
|
||||||
|
|
||||||
|
if content[4:12] == b"ftypqt ":
|
||||||
|
return "video/quicktime"
|
||||||
|
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,6 +57,7 @@ def get_extension(mime_type: str) -> Optional[str]:
|
||||||
"image/heif": "heif",
|
"image/heif": "heif",
|
||||||
"image/heic": "heic",
|
"image/heic": "heic",
|
||||||
"video/mp4": "mp4",
|
"video/mp4": "mp4",
|
||||||
|
"video/quicktime": "mov",
|
||||||
"video/webm": "webm",
|
"video/webm": "webm",
|
||||||
"application/octet-stream": "dat",
|
"application/octet-stream": "dat",
|
||||||
}
|
}
|
||||||
|
@ -65,7 +69,12 @@ def is_flash(mime_type: str) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def is_video(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:
|
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
|
and len(re.findall(pattern, content)) > 1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_heif(mime_type: str) -> bool:
|
def is_heif(mime_type: str) -> bool:
|
||||||
return mime_type.lower() in (
|
return mime_type.lower() in (
|
||||||
"image/heif",
|
"image/heif",
|
||||||
|
|
|
@ -39,13 +39,20 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
||||||
length_tally = 0
|
length_tally = 0
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(request) as handle:
|
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)
|
length_tally += len(chunk)
|
||||||
if length_tally > config.config["max_dl_filesize"]:
|
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
|
content_buffer += chunk
|
||||||
except urllib.error.HTTPError as ex:
|
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 (
|
if (
|
||||||
youtube_dl_error
|
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:
|
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"]:
|
if config.config["user_agent"]:
|
||||||
cmd.extend(["--user-agent", config.config["user_agent"]])
|
cmd.extend(["--user-agent", config.config["user_agent"]])
|
||||||
cmd.extend(["--get-url", url])
|
cmd.extend(["--get-url", url])
|
||||||
|
@ -69,7 +76,8 @@ def _get_youtube_dl_content_url(url: str) -> str:
|
||||||
)
|
)
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
raise errors.ThirdPartyError(
|
raise errors.ThirdPartyError(
|
||||||
"Could not extract content location from %s" % (url)
|
"Could not extract content location from URL.",
|
||||||
|
extra_fields={"URL": url},
|
||||||
) from None
|
) from None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -197,6 +197,7 @@ class PostSerializer(serialization.BaseSerializer):
|
||||||
"favoritedBy": self.serialize_favorited_by,
|
"favoritedBy": self.serialize_favorited_by,
|
||||||
"hasCustomThumbnail": self.serialize_has_custom_thumbnail,
|
"hasCustomThumbnail": self.serialize_has_custom_thumbnail,
|
||||||
"notes": self.serialize_notes,
|
"notes": self.serialize_notes,
|
||||||
|
"desc": self.serialize_desc,
|
||||||
"comments": self.serialize_comments,
|
"comments": self.serialize_comments,
|
||||||
"pools": self.serialize_pools,
|
"pools": self.serialize_pools,
|
||||||
}
|
}
|
||||||
|
@ -328,6 +329,9 @@ class PostSerializer(serialization.BaseSerializer):
|
||||||
key=lambda x: x["polygon"],
|
key=lambda x: x["polygon"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def serialize_desc(self) -> Any:
|
||||||
|
return self.post.desc
|
||||||
|
|
||||||
def serialize_comments(self) -> Any:
|
def serialize_comments(self) -> Any:
|
||||||
return [
|
return [
|
||||||
comments.serialize_comment(comment, self.auth_user)
|
comments.serialize_comment(comment, self.auth_user)
|
||||||
|
@ -779,6 +783,11 @@ def update_post_notes(post: model.Post, notes: Any) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_post_desc(post: model.Post, desc: str) -> None:
|
||||||
|
assert post
|
||||||
|
post.desc = desc
|
||||||
|
|
||||||
|
|
||||||
def update_post_flags(post: model.Post, flags: List[str]) -> None:
|
def update_post_flags(post: model.Post, flags: List[str]) -> None:
|
||||||
assert post
|
assert post
|
||||||
target_flags = []
|
target_flags = []
|
||||||
|
|
|
@ -83,12 +83,12 @@ def flip(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
|
|
||||||
|
|
||||||
def is_valid_email(email: Optional[str]) -> bool:
|
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
|
return not email or re.match(r"^[^@]*@[^@]*\.[^@]*$", email) is not None
|
||||||
|
|
||||||
|
|
||||||
class dotdict(dict):
|
class dotdict(dict):
|
||||||
""" dot.notation access to dictionary attributes. """
|
"""dot.notation access to dictionary attributes."""
|
||||||
|
|
||||||
def __getattr__(self, attr: str) -> Any:
|
def __getattr__(self, attr: str) -> Any:
|
||||||
return self.get(attr)
|
return self.get(attr)
|
||||||
|
@ -98,7 +98,7 @@ class dotdict(dict):
|
||||||
|
|
||||||
|
|
||||||
def parse_time_range(value: str) -> Tuple[datetime, datetime]:
|
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_day = timedelta(days=1)
|
||||||
one_second = timedelta(seconds=1)
|
one_second = timedelta(seconds=1)
|
||||||
almost_one_day = one_day - one_second
|
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:
|
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)
|
user = users.get_user_by_name(username)
|
||||||
if not auth.is_valid_password(user, password):
|
if not auth.is_valid_password(user, password):
|
||||||
raise errors.AuthError("Invalid password.")
|
raise errors.AuthError("Invalid password.")
|
||||||
|
@ -17,7 +17,7 @@ def _authenticate_basic_auth(username: str, password: str) -> model.User:
|
||||||
def _authenticate_token(
|
def _authenticate_token(
|
||||||
username: str, token: str
|
username: str, token: str
|
||||||
) -> Tuple[model.User, model.UserToken]:
|
) -> 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 = users.get_user_by_name(username)
|
||||||
user_token = user_tokens.get_by_user_and_token(user, token)
|
user_token = user_tokens.get_by_user_and_token(user, token)
|
||||||
if not auth.is_valid_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:
|
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)
|
bump_login = ctx.get_param_as_bool("bump-login", default=False)
|
||||||
auth_user = _get_user(ctx, bump_login)
|
auth_user = _get_user(ctx, bump_login)
|
||||||
if auth_user:
|
if auth_user:
|
||||||
|
|
|
@ -29,6 +29,7 @@ def upgrade():
|
||||||
sa.Column("image_width", sa.Integer(), nullable=True),
|
sa.Column("image_width", sa.Integer(), nullable=True),
|
||||||
sa.Column("image_height", sa.Integer(), nullable=True),
|
sa.Column("image_height", sa.Integer(), nullable=True),
|
||||||
sa.Column("flags", sa.Integer(), nullable=False),
|
sa.Column("flags", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("description", sa.UnicodeText, nullable=True, default=""),
|
||||||
sa.Column("auto_fav_count", sa.Integer(), nullable=False),
|
sa.Column("auto_fav_count", sa.Integer(), nullable=False),
|
||||||
sa.Column("auto_score", sa.Integer(), nullable=False),
|
sa.Column("auto_score", sa.Integer(), nullable=False),
|
||||||
sa.Column("auto_feature_count", sa.Integer(), nullable=False),
|
sa.Column("auto_feature_count", sa.Integer(), nullable=False),
|
||||||
|
|
|
@ -110,7 +110,6 @@ class PostNote(Base):
|
||||||
|
|
||||||
post = sa.orm.relationship("Post")
|
post = sa.orm.relationship("Post")
|
||||||
|
|
||||||
|
|
||||||
class PostRelation(Base):
|
class PostRelation(Base):
|
||||||
__tablename__ = "post_relation"
|
__tablename__ = "post_relation"
|
||||||
|
|
||||||
|
@ -222,6 +221,7 @@ class Post(Base):
|
||||||
canvas_width = sa.Column("image_width", sa.Integer)
|
canvas_width = sa.Column("image_width", sa.Integer)
|
||||||
canvas_height = sa.Column("image_height", sa.Integer)
|
canvas_height = sa.Column("image_height", sa.Integer)
|
||||||
mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False)
|
mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False)
|
||||||
|
desc = sa.Column("description", sa.UnicodeText, nullable=True, default="")
|
||||||
|
|
||||||
# foreign tables
|
# foreign tables
|
||||||
user = sa.orm.relationship("User")
|
user = sa.orm.relationship("User")
|
||||||
|
|
|
@ -11,7 +11,7 @@ from szurubooru.rest import context, errors, middleware, routes
|
||||||
|
|
||||||
|
|
||||||
def _json_serializer(obj: Any) -> str:
|
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):
|
if isinstance(obj, datetime):
|
||||||
serial = obj.isoformat("T") + "Z"
|
serial = obj.isoformat("T") + "Z"
|
||||||
return serial
|
return serial
|
||||||
|
|
|
@ -122,6 +122,34 @@ def _pool_filter(
|
||||||
)(query, criterion, negated)
|
)(query, criterion, negated)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
class PostSearchConfig(BaseSearchConfig):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.user = None # type: Optional[model.User]
|
self.user = None # type: Optional[model.User]
|
||||||
|
@ -349,6 +377,7 @@ class PostSearchConfig(BaseSearchConfig):
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(["pool"], _pool_filter),
|
(["pool"], _pool_filter),
|
||||||
|
(["category"], _category_filter),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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(
|
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"])
|
tag = tag_factory(names=["tag"])
|
||||||
db.session.add(tag)
|
db.session.add(tag)
|
||||||
|
@ -165,16 +166,7 @@ def test_trying_to_create_tags_without_privileges(
|
||||||
with pytest.raises(errors.AuthError):
|
with pytest.raises(errors.AuthError):
|
||||||
api.tag_api.update_tag(
|
api.tag_api.update_tag(
|
||||||
context_factory(
|
context_factory(
|
||||||
params={"suggestions": ["tag1", "tag2"], "version": 1},
|
params={type: ["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},
|
|
||||||
user=user_factory(rank=model.User.RANK_REGULAR),
|
user=user_factory(rank=model.User.RANK_REGULAR),
|
||||||
),
|
),
|
||||||
{"tag_name": "tag"},
|
{"tag_name": "tag"},
|
||||||
|
|
BIN
server/szurubooru/tests/assets/mov.mov
Normal file
BIN
server/szurubooru/tests/assets/mov.mov
Normal file
Binary file not shown.
|
@ -43,14 +43,26 @@ def query_logger(pytestconfig):
|
||||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture(scope="function", autouse=True)
|
@pytest.fixture(scope="function", autouse=True)
|
||||||
def session(query_logger, postgresql_db):
|
def session(query_logger, transacted_postgresql_db):
|
||||||
|
db.session = transacted_postgresql_db.session
|
||||||
|
transacted_postgresql_db.create_table(*model.Base.metadata.sorted_tables)
|
||||||
|
try:
|
||||||
|
yield transacted_postgresql_db.session
|
||||||
|
finally:
|
||||||
|
transacted_postgresql_db.reset_db()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def nontransacted_session(query_logger, postgresql_db):
|
||||||
|
old_db_session = db.session
|
||||||
db.session = postgresql_db.session
|
db.session = postgresql_db.session
|
||||||
postgresql_db.create_table(*model.Base.metadata.sorted_tables)
|
postgresql_db.create_table(*model.Base.metadata.sorted_tables)
|
||||||
try:
|
try:
|
||||||
yield postgresql_db.session
|
yield postgresql_db.session
|
||||||
finally:
|
finally:
|
||||||
postgresql_db.reset_db()
|
postgresql_db.reset_db()
|
||||||
|
db.session = old_db_session
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
|
@ -7,6 +7,7 @@ from szurubooru.func import mime
|
||||||
"input_path,expected_mime_type",
|
"input_path,expected_mime_type",
|
||||||
[
|
[
|
||||||
("mp4.mp4", "video/mp4"),
|
("mp4.mp4", "video/mp4"),
|
||||||
|
("mov.mov", "video/quicktime"),
|
||||||
("webm.webm", "video/webm"),
|
("webm.webm", "video/webm"),
|
||||||
("flash.swf", "application/x-shockwave-flash"),
|
("flash.swf", "application/x-shockwave-flash"),
|
||||||
("png.png", "image/png"),
|
("png.png", "image/png"),
|
||||||
|
@ -35,6 +36,7 @@ def test_get_mime_type_for_empty_file():
|
||||||
[
|
[
|
||||||
("video/mp4", "mp4"),
|
("video/mp4", "mp4"),
|
||||||
("video/webm", "webm"),
|
("video/webm", "webm"),
|
||||||
|
("video/quicktime", "mov"),
|
||||||
("application/x-shockwave-flash", "swf"),
|
("application/x-shockwave-flash", "swf"),
|
||||||
("image/png", "png"),
|
("image/png", "png"),
|
||||||
("image/jpeg", "jpg"),
|
("image/jpeg", "jpg"),
|
||||||
|
@ -70,6 +72,8 @@ def test_is_flash(input_mime_type, expected_state):
|
||||||
("VIDEO/WEBM", True),
|
("VIDEO/WEBM", True),
|
||||||
("video/mp4", True),
|
("video/mp4", True),
|
||||||
("VIDEO/MP4", True),
|
("VIDEO/MP4", True),
|
||||||
|
("video/quicktime", True),
|
||||||
|
("VIDEO/QUICKTIME", True),
|
||||||
("video/anything_else", False),
|
("video/anything_else", False),
|
||||||
("application/ogg", True),
|
("application/ogg", True),
|
||||||
("not a video", False),
|
("not a video", False),
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from szurubooru import errors
|
from szurubooru import errors
|
||||||
|
@ -16,6 +18,9 @@ def inject_config(tmpdir, config_injector):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
def test_download():
|
def test_download():
|
||||||
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
||||||
|
|
||||||
|
@ -62,6 +67,9 @@ def test_download():
|
||||||
assert actual_content == expected_content
|
assert actual_content == expected_content
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"url",
|
"url",
|
||||||
[
|
[
|
||||||
|
@ -74,6 +82,9 @@ def test_too_large_download(url):
|
||||||
net.download(url, use_video_downloader=True)
|
net.download(url, use_video_downloader=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"url,expected_sha1",
|
"url,expected_sha1",
|
||||||
[
|
[
|
||||||
|
@ -96,6 +107,9 @@ def test_content_download(url, expected_sha1):
|
||||||
assert get_sha1(actual_content) == expected_sha1
|
assert get_sha1(actual_content) == expected_sha1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
def test_bad_content_downlaod():
|
def test_bad_content_downlaod():
|
||||||
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
||||||
with pytest.raises(errors.ThirdPartyError):
|
with pytest.raises(errors.ThirdPartyError):
|
||||||
|
@ -108,11 +122,13 @@ def test_no_webhooks(config_injector):
|
||||||
assert len(res) == 0
|
assert len(res) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"webhook,status_code",
|
"webhook,status_code",
|
||||||
[
|
[
|
||||||
("https://postman-echo.com/post", 200),
|
("https://postman-echo.com/post", 200),
|
||||||
("http://localhost/", 400),
|
|
||||||
("https://postman-echo.com/get", 400),
|
("https://postman-echo.com/get", 400),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -121,6 +137,9 @@ def test_single_webhook(config_injector, webhook, status_code):
|
||||||
assert ret == status_code
|
assert ret == status_code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||||
|
)
|
||||||
def test_multiple_webhooks(config_injector):
|
def test_multiple_webhooks(config_injector):
|
||||||
config_injector(
|
config_injector(
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest # noqa: F401
|
||||||
|
|
||||||
from szurubooru import db, model
|
from szurubooru import db, model
|
||||||
from szurubooru.func import snapshots, users
|
from szurubooru.func import snapshots, users
|
||||||
|
@ -144,46 +144,6 @@ def test_create(tag_factory, user_factory):
|
||||||
assert results[0].data == "mocked"
|
assert results[0].data == "mocked"
|
||||||
|
|
||||||
|
|
||||||
def test_modify_saves_non_empty_diffs(post_factory, user_factory):
|
|
||||||
if "sqlite" in db.session.get_bind().driver:
|
|
||||||
pytest.xfail(
|
|
||||||
"SQLite doesn't support transaction isolation, "
|
|
||||||
"which is required to retrieve original entity"
|
|
||||||
)
|
|
||||||
post = post_factory()
|
|
||||||
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")]
|
|
||||||
user = user_factory()
|
|
||||||
db.session.add_all([post, user])
|
|
||||||
db.session.commit()
|
|
||||||
post.source = "new source"
|
|
||||||
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")]
|
|
||||||
db.session.flush()
|
|
||||||
with patch("szurubooru.func.snapshots._post_to_webhooks"):
|
|
||||||
snapshots.modify(post, user)
|
|
||||||
db.session.flush()
|
|
||||||
results = db.session.query(model.Snapshot).all()
|
|
||||||
assert len(results) == 1
|
|
||||||
assert results[0].data == {
|
|
||||||
"type": "object change",
|
|
||||||
"value": {
|
|
||||||
"source": {
|
|
||||||
"type": "primitive change",
|
|
||||||
"old-value": None,
|
|
||||||
"new-value": "new source",
|
|
||||||
},
|
|
||||||
"notes": {
|
|
||||||
"type": "list change",
|
|
||||||
"removed": [
|
|
||||||
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"}
|
|
||||||
],
|
|
||||||
"added": [
|
|
||||||
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory):
|
def test_modify_doesnt_save_empty_diffs(tag_factory, user_factory):
|
||||||
tag = tag_factory(names=["dummy"])
|
tag = tag_factory(names=["dummy"])
|
||||||
user = user_factory()
|
user = user_factory()
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from szurubooru import db, model
|
||||||
|
from szurubooru.func import snapshots
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def session(query_logger, postgresql_db):
|
||||||
|
"""
|
||||||
|
Override db session for this specific test section only
|
||||||
|
"""
|
||||||
|
db.session = postgresql_db.session
|
||||||
|
postgresql_db.create_table(*model.Base.metadata.sorted_tables)
|
||||||
|
try:
|
||||||
|
yield postgresql_db.session
|
||||||
|
finally:
|
||||||
|
postgresql_db.reset_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_modify_saves_non_empty_diffs(post_factory, user_factory):
|
||||||
|
if "sqlite" in db.session.get_bind().driver:
|
||||||
|
pytest.xfail(
|
||||||
|
"SQLite doesn't support transaction isolation, "
|
||||||
|
"which is required to retrieve original entity"
|
||||||
|
)
|
||||||
|
post = post_factory()
|
||||||
|
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="old")]
|
||||||
|
user = user_factory()
|
||||||
|
db.session.add_all([post, user])
|
||||||
|
db.session.commit()
|
||||||
|
post.source = "new source"
|
||||||
|
post.notes = [model.PostNote(polygon=[(0, 0), (0, 1), (1, 1)], text="new")]
|
||||||
|
db.session.flush()
|
||||||
|
with patch("szurubooru.func.snapshots._post_to_webhooks"):
|
||||||
|
snapshots.modify(post, user)
|
||||||
|
db.session.flush()
|
||||||
|
results = db.session.query(model.Snapshot).all()
|
||||||
|
assert len(results) == 1
|
||||||
|
assert results[0].data == {
|
||||||
|
"type": "object change",
|
||||||
|
"value": {
|
||||||
|
"source": {
|
||||||
|
"type": "primitive change",
|
||||||
|
"old-value": None,
|
||||||
|
"new-value": "new source",
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "list change",
|
||||||
|
"removed": [
|
||||||
|
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "old"}
|
||||||
|
],
|
||||||
|
"added": [
|
||||||
|
{"polygon": [[0, 0], [0, 1], [1, 1]], "text": "new"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
|
@ -107,17 +107,16 @@ def test_update_category_name_reusing_other_name(
|
||||||
tag_categories.update_category_name(category, "NAME")
|
tag_categories.update_category_name(category, "NAME")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["name", "NAME"])
|
||||||
def test_update_category_name_reusing_own_name(
|
def test_update_category_name_reusing_own_name(
|
||||||
config_injector, tag_category_factory
|
config_injector, tag_category_factory, name
|
||||||
):
|
):
|
||||||
config_injector({"tag_category_name_regex": ".*"})
|
config_injector({"tag_category_name_regex": ".*"})
|
||||||
for name in ["name", "NAME"]:
|
category = tag_category_factory(name="name")
|
||||||
category = tag_category_factory(name="name")
|
db.session.add(category)
|
||||||
db.session.add(category)
|
db.session.flush()
|
||||||
db.session.flush()
|
tag_categories.update_category_name(category, name)
|
||||||
tag_categories.update_category_name(category, name)
|
assert category.name == name
|
||||||
assert category.name == name
|
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_category_color_with_empty_string(tag_category_factory):
|
def test_update_category_color_with_empty_string(tag_category_factory):
|
||||||
|
|
|
@ -513,15 +513,14 @@ def test_update_tag_names_trying_to_use_taken_name(
|
||||||
tags.update_tag_names(tag, ["A"])
|
tags.update_tag_names(tag, ["A"])
|
||||||
|
|
||||||
|
|
||||||
def test_update_tag_names_reusing_own_name(config_injector, tag_factory):
|
@pytest.mark.parametrize("name", list("aA"))
|
||||||
|
def test_update_tag_names_reusing_own_name(config_injector, tag_factory, name):
|
||||||
config_injector({"tag_name_regex": "^[a-zA-Z]*$"})
|
config_injector({"tag_name_regex": "^[a-zA-Z]*$"})
|
||||||
for name in list("aA"):
|
tag = tag_factory(names=["a"])
|
||||||
tag = tag_factory(names=["a"])
|
db.session.add(tag)
|
||||||
db.session.add(tag)
|
db.session.flush()
|
||||||
db.session.flush()
|
tags.update_tag_names(tag, [name])
|
||||||
tags.update_tag_names(tag, [name])
|
assert [tag_name.name for tag_name in tag.names] == [name]
|
||||||
assert [tag_name.name for tag_name in tag.names] == [name]
|
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
|
def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
|
||||||
|
@ -533,7 +532,6 @@ def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
db.session.refresh(tag)
|
db.session.refresh(tag)
|
||||||
assert [tag_name.name for tag_name in tag.names] == ["b", "a"]
|
assert [tag_name.name for tag_name in tag.names] == ["b", "a"]
|
||||||
db.session.rollback()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"])
|
@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"])
|
||||||
|
|
|
@ -136,8 +136,6 @@ def test_escaping(
|
||||||
)
|
)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
if db_driver and db.session.get_bind().driver != db_driver:
|
|
||||||
pytest.xfail()
|
|
||||||
if expected_pool_names is None:
|
if expected_pool_names is None:
|
||||||
with pytest.raises(errors.SearchError):
|
with pytest.raises(errors.SearchError):
|
||||||
executor.execute(input, offset=0, limit=100)
|
executor.execute(input, offset=0, limit=100)
|
||||||
|
|
|
@ -863,3 +863,55 @@ def test_tumbleweed(
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
verify_unpaged("special:tumbleweed", [4])
|
verify_unpaged("special:tumbleweed", [4])
|
||||||
verify_unpaged("-special:tumbleweed", [1, 2, 3])
|
verify_unpaged("-special:tumbleweed", [1, 2, 3])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"input,expected_post_ids",
|
||||||
|
[
|
||||||
|
("category:cat1", [1, 2, 3]),
|
||||||
|
("category:cat2", [3, 4]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_search_by_tag_category(
|
||||||
|
verify_unpaged,
|
||||||
|
post_factory,
|
||||||
|
tag_factory,
|
||||||
|
tag_category_factory,
|
||||||
|
input,
|
||||||
|
expected_post_ids,
|
||||||
|
):
|
||||||
|
cat1 = tag_category_factory(name="cat1")
|
||||||
|
cat2 = tag_category_factory(name="cat2")
|
||||||
|
tag1 = tag_factory(names=["t1"], category=cat1)
|
||||||
|
tag2 = tag_factory(names=["t2"], category=cat1)
|
||||||
|
tag3 = tag_factory(names=["t3"], category=cat2)
|
||||||
|
|
||||||
|
post1 = post_factory(id=1)
|
||||||
|
post1.tags.append(tag1)
|
||||||
|
|
||||||
|
post2 = post_factory(id=2)
|
||||||
|
post2.tags.append(tag2)
|
||||||
|
|
||||||
|
post3 = post_factory(id=3)
|
||||||
|
post3.tags.append(tag1)
|
||||||
|
post3.tags.append(tag3)
|
||||||
|
|
||||||
|
post4 = post_factory(id=4)
|
||||||
|
post4.tags.append(tag3)
|
||||||
|
|
||||||
|
post5 = post_factory(id=5)
|
||||||
|
|
||||||
|
db.session.add_all(
|
||||||
|
[
|
||||||
|
tag1,
|
||||||
|
tag2,
|
||||||
|
tag3,
|
||||||
|
post1,
|
||||||
|
post2,
|
||||||
|
post3,
|
||||||
|
post4,
|
||||||
|
post5,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
db.session.flush()
|
||||||
|
verify_unpaged(input, expected_post_ids)
|
||||||
|
|
|
@ -134,8 +134,6 @@ def test_escaping(executor, tag_factory, input, expected_tag_names, db_driver):
|
||||||
)
|
)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
if db_driver and db.session.get_bind().driver != db_driver:
|
|
||||||
pytest.xfail()
|
|
||||||
if expected_tag_names is None:
|
if expected_tag_names is None:
|
||||||
with pytest.raises(errors.SearchError):
|
with pytest.raises(errors.SearchError):
|
||||||
executor.execute(input, offset=0, limit=100)
|
executor.execute(input, offset=0, limit=100)
|
||||||
|
|
Reference in a new issue