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/**/pyvenv.cfg
|
||||
__pycache__/
|
||||
|
||||
data/
|
||||
sql/
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
repos:
|
||||
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: mixed-line-ending
|
||||
|
||||
|
||||
- repo: https://github.com/Lucas-C/pre-commit-hooks
|
||||
rev: v1.1.9
|
||||
rev: v1.4.2
|
||||
hooks:
|
||||
- id: remove-tabs
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
rev: '23.1.0'
|
||||
hooks:
|
||||
- id: black
|
||||
files: 'server/'
|
||||
types: [python]
|
||||
language_version: python3.8
|
||||
language_version: python3.9
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: '5.4.2'
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: '5.12.0'
|
||||
hooks:
|
||||
- id: isort
|
||||
files: 'server/'
|
||||
|
@ -31,8 +32,8 @@ repos:
|
|||
additional_dependencies:
|
||||
- toml
|
||||
|
||||
- repo: https://github.com/prettier/prettier
|
||||
rev: '2.1.1'
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v2.7.1
|
||||
hooks:
|
||||
- id: prettier
|
||||
files: client/js/
|
||||
|
@ -40,7 +41,7 @@ repos:
|
|||
args: ['--config', 'client/.prettierrc.yml']
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-eslint
|
||||
rev: v7.8.0
|
||||
rev: v8.33.0
|
||||
hooks:
|
||||
- id: eslint
|
||||
files: client/js/
|
||||
|
@ -48,8 +49,8 @@ repos:
|
|||
additional_dependencies:
|
||||
- eslint-config-prettier
|
||||
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: '3.8.3'
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: '6.0.0'
|
||||
hooks:
|
||||
- id: flake8
|
||||
files: server/szurubooru/
|
||||
|
@ -57,44 +58,5 @@ repos:
|
|||
- flake8-print
|
||||
args: ['--config=server/.flake8']
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: docker-build-client
|
||||
name: Docker - build client
|
||||
entry: bash -c 'docker build client/'
|
||||
language: system
|
||||
types: [file]
|
||||
files: client/
|
||||
pass_filenames: false
|
||||
|
||||
- id: docker-build-server
|
||||
name: Docker - build server
|
||||
entry: bash -c 'docker build server/'
|
||||
language: system
|
||||
types: [file]
|
||||
files: server/
|
||||
pass_filenames: false
|
||||
|
||||
- id: pytest
|
||||
name: pytest
|
||||
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) szurubooru/'
|
||||
language: system
|
||||
types: [python]
|
||||
files: server/szurubooru/
|
||||
exclude: server/szurubooru/migrations/
|
||||
pass_filenames: false
|
||||
stages: [push]
|
||||
|
||||
- id: pytest-cov
|
||||
name: pytest
|
||||
entry: bash -c 'docker run --rm -t $(docker build --target testing -q server/) --cov-report=term-missing:skip-covered --cov=szurubooru szurubooru/'
|
||||
language: system
|
||||
types: [python]
|
||||
files: server/szurubooru/
|
||||
exclude: server/szurubooru/migrations/
|
||||
pass_filenames: false
|
||||
verbose: true
|
||||
stages: [manual]
|
||||
|
||||
fail_fast: true
|
||||
exclude: LICENSE.md
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
Szurubooru is an image board engine inspired by services such as Danbooru,
|
||||
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
|
||||
its roots in Polish language and has onomatopeic meaning of scraping or
|
||||
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
|
||||
|
||||
## Features
|
||||
|
||||
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
|
||||
- Ability to retrieve web video content using [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
- Post comments
|
||||
- Post notes / annotations, including arbitrary polygons
|
||||
- Rich JSON REST API ([see documentation](doc/API.md))
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
FROM node:lts as builder
|
||||
FROM --platform=$BUILDPLATFORM node:lts as builder
|
||||
WORKDIR /opt/app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm install -g npm@lts
|
||||
RUN npm install
|
||||
|
||||
COPY . ./
|
||||
|
@ -12,7 +11,7 @@ ARG CLIENT_BUILD_ARGS=""
|
|||
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
|
||||
|
||||
|
||||
FROM scratch as approot
|
||||
FROM --platform=$BUILDPLATFORM scratch as approot
|
||||
|
||||
COPY docker-start.sh /
|
||||
|
||||
|
|
|
@ -127,6 +127,10 @@ $comment-border-color = #DDD
|
|||
color: mix($main-color, $inactive-link-color-darktheme)
|
||||
|
||||
.comment-content
|
||||
p
|
||||
word-wrap: normal
|
||||
word-break: break-word
|
||||
|
||||
ul, ol
|
||||
list-style-position: inside
|
||||
margin: 1em 0
|
||||
|
|
|
@ -240,6 +240,9 @@ nav
|
|||
background: $focused-tab-background-color-darktheme
|
||||
&#top-navigation
|
||||
background: $top-navigation-color-darktheme
|
||||
ul
|
||||
#mobile-navigation-toggle
|
||||
color: $text-color-darktheme
|
||||
|
||||
a .access-key
|
||||
text-decoration: underline
|
||||
|
@ -275,7 +278,7 @@ a .access-key
|
|||
background: darken($message-error-background-color, 60%)
|
||||
&.success
|
||||
border: 1px solid darken($message-success-border-color, 30%)
|
||||
background: darken($message-success-background-color, 60%)
|
||||
background: darken($message-success-background-color, 80%)
|
||||
|
||||
.thumbnail
|
||||
/*background-image: attr(data-src url)*/ /* not available yet */
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
z-index: 1
|
||||
span
|
||||
position: relative
|
||||
background: white
|
||||
background: $window-color
|
||||
padding: 0 1em
|
||||
z-index: 2
|
||||
|
||||
|
@ -31,3 +31,5 @@
|
|||
.page-header
|
||||
&:before
|
||||
background: $top-navigation-color-darktheme
|
||||
span
|
||||
background: $window-color-darktheme
|
||||
|
|
|
@ -114,6 +114,29 @@
|
|||
&[data-disabled]
|
||||
background: rgba(200, 200, 200, 0.7)
|
||||
|
||||
.delete-flipper
|
||||
display: inline-block
|
||||
padding: 0.5em
|
||||
box-sizing: border-box
|
||||
border: 0
|
||||
&:after
|
||||
display: inline-block
|
||||
width: 1em
|
||||
height: 1em
|
||||
text-align: center
|
||||
line-height: 1em
|
||||
font-size: 2.2em
|
||||
&.delete
|
||||
background: rgba(255, 0, 0, 0.7)
|
||||
&:after
|
||||
color: white
|
||||
font-family: FontAwesome;
|
||||
content: "\f1f8"; // fa-trash
|
||||
&:not(.delete)
|
||||
background: rgba(200, 200, 200, 0.7)
|
||||
&:after
|
||||
color: white
|
||||
content: '-'
|
||||
|
||||
.thumbnail
|
||||
width: 100%
|
||||
|
@ -164,6 +187,9 @@
|
|||
vertical-align: top
|
||||
@media (max-width: 1000px)
|
||||
display: block
|
||||
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
|
||||
float: left
|
||||
margin-right: 1em
|
||||
input
|
||||
margin-bottom: 0.25em
|
||||
margin-right: 0.25em
|
||||
|
@ -215,7 +241,19 @@
|
|||
.append
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
|
||||
.bulk-edit-delete
|
||||
&.opened
|
||||
.start
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
&:not(.opened)
|
||||
.start
|
||||
display: none
|
||||
.append.open
|
||||
@media (max-width: 1000px)
|
||||
margin-left: 0
|
||||
.start
|
||||
margin-left: 1em
|
||||
.safety
|
||||
margin-right: 0.25em
|
||||
&.safety-safe
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
border: 0
|
||||
outline: 0
|
||||
|
||||
nav.buttons
|
||||
>.sidebar>nav.buttons, >.content nav.buttons
|
||||
margin-top: 0
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
|
@ -27,27 +27,41 @@
|
|||
padding: 0.3em 0
|
||||
text-align: center
|
||||
vertical-align: middle
|
||||
transition: background 0.2s linear
|
||||
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: 2em
|
||||
margin-top: 0.6em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
>.content
|
||||
width: 100%
|
||||
|
||||
.post-container
|
||||
margin-bottom: 2em
|
||||
margin-bottom: 0.6em
|
||||
|
||||
.post-content
|
||||
margin: 0
|
||||
|
||||
.after-mobile-controls
|
||||
width: 100%
|
||||
|
||||
.darktheme .post-view
|
||||
>.sidebar, >.content
|
||||
nav.buttons
|
||||
article
|
||||
a:not(.inactive):hover
|
||||
background: unset
|
||||
box-shadow: inset 0 0 0 0.3em $main-color
|
||||
|
||||
@media (max-width: 800px)
|
||||
.post-view
|
||||
flex-wrap: wrap
|
||||
>.after-mobile-controls
|
||||
order: 3
|
||||
>.sidebar
|
||||
order: 2
|
||||
min-width: 100%
|
||||
|
@ -105,7 +119,6 @@
|
|||
h1
|
||||
margin-bottom: 0.5em
|
||||
.thumbnail
|
||||
background-position: 50% 30%
|
||||
width: 4em
|
||||
height: 3em
|
||||
li
|
||||
|
|
|
@ -13,8 +13,12 @@ $cancel-button-color = tomato
|
|||
|
||||
&.inactive input[type=submit],
|
||||
&.inactive .skip-duplicates
|
||||
&.inactive .always-upload-similar
|
||||
&.inactive .pause-remain-on-error
|
||||
&.uploading input[type=submit],
|
||||
&.uploading .skip-duplicates,
|
||||
&.uploading .always-upload-similar
|
||||
&.uploading .pause-remain-on-error
|
||||
&:not(.uploading) .cancel
|
||||
display: none
|
||||
|
||||
|
@ -39,6 +43,12 @@ $cancel-button-color = tomato
|
|||
.skip-duplicates
|
||||
margin-left: 1em
|
||||
|
||||
.always-upload-similar
|
||||
margin-left: 1em
|
||||
|
||||
.pause-remain-on-error
|
||||
margin-left: 1em
|
||||
|
||||
form>.messages
|
||||
margin-top: 1em
|
||||
|
||||
|
@ -52,6 +62,14 @@ $cancel-button-color = tomato
|
|||
margin: 0 0 1.2em 0
|
||||
padding-left: 13em
|
||||
|
||||
img
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
video
|
||||
width: 100%
|
||||
height: 100%
|
||||
|
||||
&>.thumbnail-wrapper
|
||||
float: left
|
||||
width: 12em
|
||||
|
|
|
@ -86,6 +86,12 @@ div.tag-input
|
|||
font-size: 90%
|
||||
unselectable()
|
||||
|
||||
@keyframes tag-added-to-post
|
||||
from
|
||||
max-height: 0
|
||||
to
|
||||
max-height: 5em
|
||||
|
||||
ul.compact-tags
|
||||
width: 100%
|
||||
margin: 0.5em 0 0 0
|
||||
|
@ -103,18 +109,30 @@ ul.compact-tags
|
|||
a:focus
|
||||
outline: 0
|
||||
box-shadow: inset 0 0 0 2px $main-color
|
||||
// these 3 added when tag is added to ul
|
||||
&.added, &.new, &.implication
|
||||
animation: tag-added-to-post 1s ease forwards
|
||||
&.implication
|
||||
background: $implied-tag-background-color
|
||||
color: $implied-tag-text-color
|
||||
background-color: $implied-tag-background-color
|
||||
&.new
|
||||
background: $new-tag-background-color
|
||||
color: $new-tag-text-color
|
||||
background-color: $new-tag-background-color
|
||||
&.duplicate
|
||||
background: $duplicate-tag-background-color
|
||||
color: $duplicate-tag-text-color
|
||||
background-color: $duplicate-tag-background-color
|
||||
i
|
||||
padding-right: 0.4em
|
||||
|
||||
.darktheme ul.compact-tags
|
||||
li
|
||||
&.new
|
||||
background-color: darken($new-tag-background-color, 80%)
|
||||
&.implication
|
||||
background-color: darken($implied-tag-background-color, 85%)
|
||||
&.duplicate
|
||||
background-color: darken($duplicate-tag-background-color, 80%)
|
||||
|
||||
div.tag-input, ul.compact-tags
|
||||
.tag-usages, .tag-weight, .remove-tag
|
||||
color: $inactive-link-color
|
||||
|
@ -134,6 +152,8 @@ div.tag-input, ul.compact-tags
|
|||
background: $window-color-darktheme
|
||||
ul li:last-child
|
||||
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
|
||||
p
|
||||
background: darken($tag-suggestions-header-color, 80%)
|
||||
.append
|
||||
color: $inactive-link-color-darktheme
|
||||
div.tag-input, ul.compact-tags
|
||||
|
|
|
@ -21,10 +21,11 @@
|
|||
.details
|
||||
font-size: 90%
|
||||
line-height: 130%
|
||||
.image
|
||||
margin: 0.25em 0.6em 0.25em 0
|
||||
.thumbnail
|
||||
width: 3em
|
||||
height: 3em
|
||||
margin: 0.25em 0.6em 0 0
|
||||
|
||||
.darktheme .user-list
|
||||
ul li
|
||||
|
|
|
@ -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>
|
||||
<head>
|
||||
<meta charset='utf-8'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<meta name='theme-color' content='#24aadd'/>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'/>
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<span class='vim-nav-hint'>Next post ></span>
|
||||
</a>
|
||||
</article>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<article class='edit-post'>
|
||||
<% if (ctx.editMode) { %>
|
||||
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
|
@ -36,16 +37,13 @@
|
|||
<span class='vim-nav-hint'>Back to view mode</span>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
|
||||
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
|
||||
<% } else { %>
|
||||
<a class='inactive'>
|
||||
<% } %>
|
||||
<i class='fa fa-pencil'></i>
|
||||
<span class='vim-nav-hint'>Edit post</span>
|
||||
</a>
|
||||
<% } %>
|
||||
</article>
|
||||
<% } %>
|
||||
</nav>
|
||||
|
||||
<div class='sidebar-container'></div>
|
||||
|
@ -54,13 +52,16 @@
|
|||
<div class='content'>
|
||||
<div class='post-container'></div>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
|
||||
<div class='after-mobile-controls'>
|
||||
<div class='description'></div>
|
||||
<% if (ctx.canCreateComments) { %>
|
||||
<h2>Add comment</h2>
|
||||
<div class='comment-form-container'></div>
|
||||
<% } %>
|
||||
|
||||
<% if (ctx.canListComments) { %>
|
||||
<div class='comments-container'></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
'image/heic': 'HEIC',
|
||||
'video/webm': 'WEBM',
|
||||
'video/mp4': 'MPEG-4',
|
||||
'video/quicktime': 'MOV',
|
||||
'application/x-shockwave-flash': 'SWF',
|
||||
}[ctx.post.mimeType] +
|
||||
' (' +
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
'image/heic': 'HEIC',
|
||||
'video/webm': 'WEBM',
|
||||
'video/mp4': 'MPEG-4',
|
||||
'video/quicktime': 'MOV',
|
||||
'application/x-shockwave-flash': 'SWF',
|
||||
}[ctx.post.mimeType] %>
|
||||
</a>
|
||||
}[ctx.post.mimeType] %><!--
|
||||
--></a>
|
||||
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
|
||||
<% if (ctx.post.flags.length) { %><!--
|
||||
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
|
||||
|
@ -57,7 +58,7 @@
|
|||
Search on
|
||||
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> ·
|
||||
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> ·
|
||||
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
|
||||
</section>
|
||||
|
||||
<section class='social'>
|
||||
|
@ -98,10 +99,10 @@
|
|||
--><% if (ctx.canListPosts) { %><!--
|
||||
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
|
||||
--><% } %><!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %> <!--
|
||||
--><%- ctx.getPrettyName(tag.names[0]) %><!--
|
||||
--><% if (ctx.canListPosts) { %><!--
|
||||
--></a><!--
|
||||
--><% } %><!--
|
||||
--><% } %> <!--
|
||||
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
|
||||
--></li><!--
|
||||
--><% } %><!--
|
||||
|
|
|
@ -7,12 +7,28 @@
|
|||
|
||||
<span class='skip-duplicates'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Skip duplicates',
|
||||
text: 'Skip duplicate',
|
||||
name: 'skip-duplicates',
|
||||
checked: false,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<span class='always-upload-similar'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Force upload similar',
|
||||
name: 'always-upload-similar',
|
||||
checked: false,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<span class='pause-remain-on-error'>
|
||||
<%= ctx.makeCheckbox({
|
||||
text: 'Pause on error',
|
||||
name: 'pause-remain-on-error',
|
||||
checked: true,
|
||||
}) %>
|
||||
</span>
|
||||
|
||||
<input type='button' value='Cancel' class='cancel'/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@
|
|||
text: 'Upload anonymously',
|
||||
name: 'anonymous',
|
||||
checked: ctx.uploadable.anonymous,
|
||||
readonly: ctx.uploadable.forceAnonymous,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
%><form class='horizontal bulk-edit bulk-edit-tags'><%
|
||||
%><span class='append hint'>Tagging with:</span><%
|
||||
%><a href class='mousetrap button append open'>Mass tag</a><%
|
||||
%><wbr/><%
|
||||
%><%= ctx.makeTextInput({name: 'tag', value: ctx.parameters.tag}) %><%
|
||||
%><input class='mousetrap start' type='submit' value='Start tagging'/><%
|
||||
%><a href class='mousetrap button append close'>Stop tagging</a><%
|
||||
|
@ -28,4 +27,11 @@
|
|||
%><a href class='mousetrap button append close'>Stop editing safety</a><%
|
||||
%></form><%
|
||||
%><% } %><%
|
||||
%><% if (ctx.canBulkDelete) { %><%
|
||||
%><form class='horizontal bulk-edit bulk-edit-delete'><%
|
||||
%><a href class='mousetrap button append open'>Mass delete</a><%
|
||||
%><input class='mousetrap start' type='submit' value='Delete selected posts'/><%
|
||||
%><a href class='mousetrap button append close'>Stop deleting</a><%
|
||||
%></form><%
|
||||
%><% } %><%
|
||||
%></div>
|
||||
|
|
|
@ -50,6 +50,10 @@
|
|||
<% } %>
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
|
||||
<a href class='delete-flipper'>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
|
|
|
@ -17,7 +17,7 @@ class HomeController {
|
|||
buildDate: config.meta.buildDate,
|
||||
canListSnapshots: api.hasPrivilege("snapshots:list"),
|
||||
canListPosts: api.hasPrivilege("posts:list"),
|
||||
isDevelopmentMode: config.environment == "development"
|
||||
isDevelopmentMode: config.environment == "development",
|
||||
});
|
||||
|
||||
Info.get().then(
|
||||
|
|
|
@ -91,16 +91,16 @@ class PoolController {
|
|||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.pool.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.pool.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.pool.description = e.detail.description;
|
||||
}
|
||||
if (e.detail.posts !== undefined) {
|
||||
if (e.detail.posts !== undefined && e.detail.posts !== null) {
|
||||
e.detail.pool.posts.clear();
|
||||
for (let postId of e.detail.posts) {
|
||||
e.detail.pool.posts.add(
|
||||
|
|
|
@ -43,6 +43,8 @@ class PoolListController {
|
|||
this._headerView.addEventListener(
|
||||
"submit",
|
||||
(e) => this._evtSubmit(e),
|
||||
);
|
||||
this._headerView.addEventListener(
|
||||
"navigate",
|
||||
(e) => this._evtNavigate(e)
|
||||
);
|
||||
|
|
|
@ -44,6 +44,7 @@ class PostListController {
|
|||
enableSafety: api.safetyEnabled(),
|
||||
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
|
||||
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
|
||||
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||
bulkEdit: {
|
||||
tags: this._bulkEditTags,
|
||||
},
|
||||
|
@ -52,6 +53,16 @@ class PostListController {
|
|||
this._evtNavigate(e)
|
||||
);
|
||||
|
||||
if (this._headerView._bulkDeleteEditor) {
|
||||
this._headerView._bulkDeleteEditor.addEventListener(
|
||||
"deleteSelectedPosts",
|
||||
(e) => {
|
||||
this._evtDeleteSelectedPosts(e);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this._postsMarkedForDeletion = [];
|
||||
this._syncPageController();
|
||||
}
|
||||
|
||||
|
@ -91,6 +102,38 @@ class PostListController {
|
|||
e.detail.post.save().catch((error) => window.alert(error.message));
|
||||
}
|
||||
|
||||
_evtMarkForDeletion(e) {
|
||||
const postId = e.detail;
|
||||
|
||||
// Add or remove post from delete list
|
||||
if (e.detail.delete) {
|
||||
this._postsMarkedForDeletion.push(e.detail.post);
|
||||
} else {
|
||||
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
|
||||
(x) => x.id != e.detail.post.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_evtDeleteSelectedPosts(e) {
|
||||
if (this._postsMarkedForDeletion.length == 0) return;
|
||||
|
||||
if (
|
||||
confirm(
|
||||
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
|
||||
)
|
||||
) {
|
||||
Promise.all(
|
||||
this._postsMarkedForDeletion.map((post) => post.delete())
|
||||
)
|
||||
.catch((error) => window.alert(error.message))
|
||||
.then(() => {
|
||||
this._postsMarkedForDeletion = [];
|
||||
this._headerView._navigate();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_syncPageController() {
|
||||
this._pageController.run({
|
||||
parameters: this._ctx.parameters,
|
||||
|
@ -117,8 +160,10 @@ class PostListController {
|
|||
canBulkEditSafety: api.hasPrivilege(
|
||||
"posts:bulk-edit:safety"
|
||||
),
|
||||
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
|
||||
bulkEdit: {
|
||||
tags: this._bulkEditTags,
|
||||
markedForDeletion: this._postsMarkedForDeletion,
|
||||
},
|
||||
postFlow: settings.get().postFlow,
|
||||
});
|
||||
|
@ -128,6 +173,9 @@ class PostListController {
|
|||
view.addEventListener("changeSafety", (e) =>
|
||||
this._evtChangeSafety(e)
|
||||
);
|
||||
view.addEventListener("markForDeletion", (e) =>
|
||||
this._evtMarkForDeletion(e)
|
||||
);
|
||||
return view;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -169,24 +169,27 @@ class PostMainController extends BasePostController {
|
|||
this._view.sidebarControl.disableForm();
|
||||
this._view.sidebarControl.clearMessages();
|
||||
const post = e.detail.post;
|
||||
if (e.detail.safety !== undefined) {
|
||||
if (e.detail.safety !== undefined && e.detail.safety !== null) {
|
||||
post.safety = e.detail.safety;
|
||||
}
|
||||
if (e.detail.flags !== undefined) {
|
||||
if (e.detail.flags !== undefined && e.detail.flags !== null) {
|
||||
post.flags = e.detail.flags;
|
||||
}
|
||||
if (e.detail.relations !== undefined) {
|
||||
if (e.detail.relations !== undefined && e.detail.relations !== null) {
|
||||
post.relations = e.detail.relations;
|
||||
}
|
||||
if (e.detail.content !== undefined) {
|
||||
if (e.detail.content !== undefined && e.detail.content !== null) {
|
||||
post.newContent = e.detail.content;
|
||||
}
|
||||
if (e.detail.thumbnail !== undefined) {
|
||||
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
|
||||
post.newThumbnail = e.detail.thumbnail;
|
||||
}
|
||||
if (e.detail.source !== undefined) {
|
||||
if (e.detail.source !== undefined && e.detail.source !== null) {
|
||||
post.source = e.detail.source;
|
||||
}
|
||||
if (e.detail.desc !== undefined && e.detail.desc !== null) {
|
||||
post.desc = e.detail.desc;
|
||||
}
|
||||
post.save().then(
|
||||
() => {
|
||||
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 genericErrorMessage =
|
||||
"One of the posts needs your attention; " +
|
||||
"One or more posts needs your attention; " +
|
||||
'click "resume upload" when you\'re ready.';
|
||||
|
||||
class PostUploadController {
|
||||
|
@ -55,6 +55,7 @@ class PostUploadController {
|
|||
_evtSubmit(e) {
|
||||
this._view.disableForm();
|
||||
this._view.clearMessages();
|
||||
let anyFailures = false;
|
||||
|
||||
e.detail.uploadables
|
||||
.reduce(
|
||||
|
@ -62,11 +63,45 @@ class PostUploadController {
|
|||
promise.then(() =>
|
||||
this._uploadSinglePost(
|
||||
uploadable,
|
||||
e.detail.skipDuplicates
|
||||
)
|
||||
e.detail.skipDuplicates,
|
||||
e.detail.alwaysUploadSimilar
|
||||
).catch((error) => {
|
||||
anyFailures = true;
|
||||
if (error.uploadable) {
|
||||
if (error.similarPosts) {
|
||||
error.uploadable.lookalikes =
|
||||
error.similarPosts;
|
||||
this._view.updateUploadable(
|
||||
error.uploadable
|
||||
);
|
||||
this._view.showInfo(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
} else {
|
||||
this._view.showError(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._view.showError(
|
||||
error.message,
|
||||
uploadable
|
||||
);
|
||||
}
|
||||
if (e.detail.pauseRemainOnError) {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
),
|
||||
Promise.resolve()
|
||||
)
|
||||
.then(() => {
|
||||
if (anyFailures) {
|
||||
return Promise.reject();
|
||||
}
|
||||
})
|
||||
.then(
|
||||
() => {
|
||||
this._view.clearMessages();
|
||||
|
@ -75,31 +110,13 @@ class PostUploadController {
|
|||
ctx.controller.showSuccess("Posts uploaded.");
|
||||
},
|
||||
(error) => {
|
||||
if (error.uploadable) {
|
||||
if (error.similarPosts) {
|
||||
error.uploadable.lookalikes = error.similarPosts;
|
||||
this._view.updateUploadable(error.uploadable);
|
||||
this._view.showInfo(genericErrorMessage);
|
||||
this._view.showInfo(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
} else {
|
||||
this._view.showError(genericErrorMessage);
|
||||
this._view.showError(
|
||||
error.message,
|
||||
error.uploadable
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._view.showError(error.message);
|
||||
}
|
||||
this._view.enableForm();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_uploadSinglePost(uploadable, skipDuplicates) {
|
||||
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
|
||||
progress.start();
|
||||
let reverseSearchPromise = Promise.resolve();
|
||||
if (!uploadable.lookalikesConfirmed) {
|
||||
|
@ -128,7 +145,10 @@ class PostUploadController {
|
|||
}
|
||||
|
||||
// notify about similar posts
|
||||
if (searchResult.similarPosts.length) {
|
||||
if (
|
||||
searchResult.similarPosts.length &&
|
||||
!alwaysUploadSimilar
|
||||
) {
|
||||
let error = new Error(
|
||||
`Found ${searchResult.similarPosts.length} similar ` +
|
||||
"posts.\nYou can resume or discard this upload."
|
||||
|
|
|
@ -95,13 +95,13 @@ class TagController {
|
|||
_evtUpdate(e) {
|
||||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
if (e.detail.names !== undefined) {
|
||||
if (e.detail.names !== undefined && e.detail.names !== null) {
|
||||
e.detail.tag.names = e.detail.names;
|
||||
}
|
||||
if (e.detail.category !== undefined) {
|
||||
if (e.detail.category !== undefined && e.detail.category !== null) {
|
||||
e.detail.tag.category = e.detail.category;
|
||||
}
|
||||
if (e.detail.description !== undefined) {
|
||||
if (e.detail.description !== undefined && e.detail.description !== null) {
|
||||
e.detail.tag.description = e.detail.description;
|
||||
}
|
||||
e.detail.tag.save().then(
|
||||
|
|
|
@ -31,9 +31,8 @@ class UserController {
|
|||
userTokenPromise = UserToken.get(userName).then(
|
||||
(userTokens) => {
|
||||
return userTokens.map((token) => {
|
||||
token.isCurrentAuthToken = api.isCurrentAuthToken(
|
||||
token
|
||||
);
|
||||
token.isCurrentAuthToken =
|
||||
api.isCurrentAuthToken(token);
|
||||
return token;
|
||||
});
|
||||
},
|
||||
|
@ -176,21 +175,21 @@ class UserController {
|
|||
const isLoggedIn = api.isLoggedIn(e.detail.user);
|
||||
const infix = isLoggedIn ? "self" : "any";
|
||||
|
||||
if (e.detail.name !== undefined) {
|
||||
if (e.detail.name !== undefined && e.detail.name !== null) {
|
||||
e.detail.user.name = e.detail.name;
|
||||
}
|
||||
if (e.detail.email !== undefined) {
|
||||
if (e.detail.email !== undefined && e.detail.email !== null) {
|
||||
e.detail.user.email = e.detail.email;
|
||||
}
|
||||
if (e.detail.rank !== undefined) {
|
||||
if (e.detail.rank !== undefined && e.detail.rank !== null) {
|
||||
e.detail.user.rank = e.detail.rank;
|
||||
}
|
||||
|
||||
if (e.detail.password !== undefined) {
|
||||
if (e.detail.password !== undefined && e.detail.password !== null) {
|
||||
e.detail.user.password = e.detail.password;
|
||||
}
|
||||
|
||||
if (e.detail.avatarStyle !== undefined) {
|
||||
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
|
||||
e.detail.user.avatarStyle = e.detail.avatarStyle;
|
||||
if (e.detail.avatarContent) {
|
||||
e.detail.user.avatarContent = e.detail.avatarContent;
|
||||
|
@ -303,7 +302,7 @@ class UserController {
|
|||
this._view.clearMessages();
|
||||
this._view.disableForm();
|
||||
|
||||
if (e.detail.note !== undefined) {
|
||||
if (e.detail.note !== undefined && e.detail.note !== null) {
|
||||
e.detail.userToken.note = e.detail.note;
|
||||
}
|
||||
|
||||
|
|
|
@ -45,9 +45,8 @@ class ExpanderControl {
|
|||
// eslint-disable-next-line accessor-pairs
|
||||
set title(newTitle) {
|
||||
if (this._expanderNode) {
|
||||
this._expanderNode.querySelector(
|
||||
"header span"
|
||||
).textContent = newTitle;
|
||||
this._expanderNode.querySelector("header span").textContent =
|
||||
newTitle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,12 @@ class FileDropperControl extends events.EventTarget {
|
|||
this._urlInputNode.addEventListener("keydown", (e) =>
|
||||
this._evtUrlInputKeyDown(e)
|
||||
);
|
||||
this._urlInputNode.addEventListener("paste", (e) => {
|
||||
// document.onpaste is used on the post-upload page.
|
||||
// And this event is used on the post edit page.
|
||||
if (document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
});
|
||||
}
|
||||
if (this._urlConfirmButtonNode) {
|
||||
this._urlConfirmButtonNode.addEventListener("click", (e) =>
|
||||
|
@ -55,6 +61,11 @@ class FileDropperControl extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
document.onpaste = (e) => {
|
||||
if (!document.getElementById("post-upload")) return;
|
||||
this._evtPaste(e)
|
||||
}
|
||||
|
||||
this._originalHtml = this._dropperNode.innerHTML;
|
||||
views.replaceContent(target, source);
|
||||
}
|
||||
|
@ -129,6 +140,17 @@ class FileDropperControl extends events.EventTarget {
|
|||
this._emitFiles(e.dataTransfer.files);
|
||||
}
|
||||
|
||||
_evtPaste(e) {
|
||||
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
|
||||
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
|
||||
|
||||
if (!this._options.allowMultiple && fileList.length > 1) {
|
||||
window.alert("Cannot select multiple files.");
|
||||
} else if (fileList.length > 0) {
|
||||
this._emitFiles(fileList);
|
||||
}
|
||||
}
|
||||
|
||||
_evtUrlInputKeyDown(e) {
|
||||
if (e.which !== KEY_RETURN) {
|
||||
return;
|
||||
|
|
|
@ -103,6 +103,30 @@ class PostContentControl {
|
|||
}
|
||||
|
||||
_refreshSize() {
|
||||
if (window.innerWidth <= 800) {
|
||||
const buttons = document.querySelector(".sidebar > .buttons");
|
||||
if (buttons) {
|
||||
const content = document.querySelector(".content");
|
||||
content.insertBefore(buttons, content.querySelector(".post-container + *"));
|
||||
|
||||
const afterControls = document.querySelector(".content > .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
afterControls.parentElement.parentElement.appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const buttons = document.querySelector(".content > .buttons");
|
||||
if (buttons) {
|
||||
const sidebar = document.querySelector(".sidebar");
|
||||
sidebar.insertBefore(buttons, sidebar.firstElementChild);
|
||||
}
|
||||
|
||||
const afterControls = document.querySelector(".content + .after-mobile-controls");
|
||||
if (afterControls) {
|
||||
document.querySelector(".content").appendChild(afterControls);
|
||||
}
|
||||
}
|
||||
|
||||
this._currentFitFunction();
|
||||
}
|
||||
|
||||
|
|
|
@ -203,9 +203,8 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
);
|
||||
|
||||
if (this._formNode) {
|
||||
const inputNodes = this._formNode.querySelectorAll(
|
||||
"input, textarea"
|
||||
);
|
||||
const inputNodes =
|
||||
this._formNode.querySelectorAll("input, textarea");
|
||||
for (let node of inputNodes) {
|
||||
node.addEventListener("change", (e) =>
|
||||
this.dispatchEvent(new CustomEvent("change"))
|
||||
|
@ -428,7 +427,7 @@ class PostEditSidebarControl extends events.EventTarget {
|
|||
: undefined,
|
||||
|
||||
thumbnail:
|
||||
this._newPostThumbnail !== undefined
|
||||
this._newPostThumbnail !== undefined && this._newPostThumbnail !== null
|
||||
? this._newPostThumbnail
|
||||
: undefined,
|
||||
|
||||
|
|
|
@ -727,9 +727,8 @@ class PostNotesOverlayControl extends events.EventTarget {
|
|||
}
|
||||
|
||||
_showNoteText(note) {
|
||||
this._textNode.querySelector(
|
||||
".wrapper"
|
||||
).innerHTML = misc.formatMarkdown(note.text);
|
||||
this._textNode.querySelector(".wrapper").innerHTML =
|
||||
misc.formatMarkdown(note.text);
|
||||
this._textNode.style.display = "block";
|
||||
const bodyRect = document.body.getBoundingClientRect();
|
||||
const noteRect = this._textNode.getBoundingClientRect();
|
||||
|
|
|
@ -196,9 +196,10 @@ class TagInputControl extends events.EventTarget {
|
|||
const listItemNode = this._createListItemNode(tag);
|
||||
if (!tag.category) {
|
||||
listItemNode.classList.add("new");
|
||||
}
|
||||
if (source === SOURCE_IMPLICATION) {
|
||||
} else if (source === SOURCE_IMPLICATION) {
|
||||
listItemNode.classList.add("implication");
|
||||
} else {
|
||||
listItemNode.classList.add("added");
|
||||
}
|
||||
this._tagListNode.prependChild(listItemNode);
|
||||
_fadeOutListItemNodeStatus(listItemNode);
|
||||
|
|
|
@ -4,12 +4,12 @@ const config = require("./config.js");
|
|||
|
||||
if (config.environment == "development") {
|
||||
var ws = new WebSocket("ws://" + location.hostname + ":8080");
|
||||
ws.addEventListener('open', function (event) {
|
||||
ws.addEventListener("open", function (event) {
|
||||
console.log("Live-reloading websocket connected.");
|
||||
});
|
||||
ws.addEventListener('message', (event) => {
|
||||
ws.addEventListener("message", (event) => {
|
||||
console.log(event);
|
||||
if (event.data == 'reload'){
|
||||
if (event.data == "reload") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -114,6 +114,10 @@ class Post extends events.EventTarget {
|
|||
return this._notes;
|
||||
}
|
||||
|
||||
get desc() {
|
||||
return this._desc;
|
||||
}
|
||||
|
||||
get comments() {
|
||||
return this._comments;
|
||||
}
|
||||
|
@ -271,12 +275,15 @@ class Post extends events.EventTarget {
|
|||
if (this._newContent) {
|
||||
files.content = this._newContent;
|
||||
}
|
||||
if (this._newThumbnail !== undefined) {
|
||||
if (this._newThumbnail !== undefined && this._newThumbnail !== null) {
|
||||
files.thumbnail = this._newThumbnail;
|
||||
}
|
||||
if (this._source !== this._orig._source) {
|
||||
detail.source = this._source;
|
||||
}
|
||||
if (this._desc !== this._orig._desc) {
|
||||
detail.desc = this._desc;
|
||||
}
|
||||
|
||||
let apiPromise = this._id
|
||||
? api.put(uri.formatApiLink("post", this.id), detail, files)
|
||||
|
|
|
@ -11,13 +11,13 @@ const defaultSettings = {
|
|||
upscaleSmallPosts: false,
|
||||
endlessScroll: false,
|
||||
keyboardShortcuts: true,
|
||||
transparencyGrid: true,
|
||||
transparencyGrid: false,
|
||||
fitMode: "fit-both",
|
||||
tagSuggestions: true,
|
||||
autoplayVideos: false,
|
||||
postsPerPage: 42,
|
||||
tagUnderscoresAsSpaces: false,
|
||||
darkTheme: false,
|
||||
darkTheme: true,
|
||||
postFlow: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -65,17 +65,6 @@ class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
|
|||
// post, user and tags permalinks
|
||||
class EntityPermalinkWrapper extends BaseMarkdownWrapper {
|
||||
preprocess(text) {
|
||||
// URL-based permalinks
|
||||
text = text.replace(new RegExp("\\b/post/(\\d+)/?\\b", "g"), "@$1");
|
||||
text = text.replace(
|
||||
new RegExp("\\b/tag/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"#$1"
|
||||
);
|
||||
text = text.replace(
|
||||
new RegExp("\\b/user/([a-zA-Z0-9_-]+?)/?", "g"),
|
||||
"+$1"
|
||||
);
|
||||
|
||||
text = text.replace(
|
||||
/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
|
||||
"$1[$2]($2)"
|
||||
|
@ -136,12 +125,8 @@ function createRenderer() {
|
|||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.image = (href, title, alt) => {
|
||||
let [
|
||||
_,
|
||||
url,
|
||||
width,
|
||||
height,
|
||||
] = /^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let [_, url, width, height] =
|
||||
/^(.+?)(?:\s=\s*(\d*)\s*x\s*(\d*)\s*)?$/.exec(href);
|
||||
let res = '<img src="' + sanitize(url) + '" alt="' + sanitize(alt);
|
||||
if (width) {
|
||||
res += '" width="' + width;
|
||||
|
@ -174,7 +159,7 @@ function formatMarkdown(text) {
|
|||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked(text, options);
|
||||
text = marked.parse(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
|
@ -200,7 +185,7 @@ function formatInlineMarkdown(text) {
|
|||
for (let wrapper of wrappers) {
|
||||
text = wrapper.preprocess(text);
|
||||
}
|
||||
text = marked.inlineLexer(text, [], options);
|
||||
text = marked.parseInline(text, options);
|
||||
wrappers.reverse();
|
||||
for (let wrapper of wrappers) {
|
||||
text = wrapper.postprocess(text);
|
||||
|
|
|
@ -209,13 +209,13 @@ function makePostLink(id, includeHash) {
|
|||
}
|
||||
|
||||
function makeTagLink(name, includeHash, includeCount, tag) {
|
||||
const category = tag ? tag.category : "unknown";
|
||||
const category = tag && tag.category ? tag.category : "unknown";
|
||||
let text = misc.getPrettyName(name);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (tag ? tag.postCount : 0) + ")";
|
||||
text += " (" + (tag && tag.postCount ? tag.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("tags:view")
|
||||
? makeElement(
|
||||
|
@ -234,15 +234,15 @@ function makeTagLink(name, includeHash, includeCount, tag) {
|
|||
}
|
||||
|
||||
function makePoolLink(id, includeHash, includeCount, pool, name) {
|
||||
const category = pool ? pool.category : "unknown";
|
||||
const category = pool && pool.category ? pool.category : "unknown";
|
||||
let text = misc.getPrettyName(
|
||||
name ? name : pool ? pool.names[0] : "unknown"
|
||||
name ? name : pool && pool.names ? pool.names[0] : "pool " + id
|
||||
);
|
||||
if (includeHash === true) {
|
||||
text = "#" + text;
|
||||
}
|
||||
if (includeCount === true) {
|
||||
text += " (" + (pool ? pool.postCount : 0) + ")";
|
||||
text += " (" + (pool && pool.postCount ? pool.postCount : 0) + ")";
|
||||
}
|
||||
return api.hasPrivilege("pools:view")
|
||||
? makeElement(
|
||||
|
@ -264,7 +264,7 @@ function makeUserLink(user) {
|
|||
let text = makeThumbnail(user ? user.avatarUrl : null);
|
||||
text += user && user.name ? misc.escapeHtml(user.name) : "Anonymous";
|
||||
const link =
|
||||
user && api.hasPrivilege("users:view")
|
||||
user && user.name && api.hasPrivilege("users:view")
|
||||
? makeElement(
|
||||
"a",
|
||||
{ href: uri.formatClientLink("user", user.name) },
|
||||
|
|
|
@ -25,9 +25,8 @@ class PostMainView {
|
|||
views.replaceContent(this._hostNode, sourceNode);
|
||||
views.syncScrollPosition();
|
||||
|
||||
const topNavigationNode = document.body.querySelector(
|
||||
"#top-navigation"
|
||||
);
|
||||
const topNavigationNode =
|
||||
document.body.querySelector("#top-navigation");
|
||||
|
||||
this._postContentControl = new PostContentControl(
|
||||
postContainerNode,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"use strict";
|
||||
|
||||
const events = require("../events.js");
|
||||
const api = require("../api.js");
|
||||
const views = require("../util/views.js");
|
||||
const FileDropperControl = require("../controls/file_dropper_control.js");
|
||||
|
||||
|
@ -21,6 +22,7 @@ function _mimeTypeToPostType(mimeType) {
|
|||
"image/heic": "image",
|
||||
"video/mp4": "video",
|
||||
"video/webm": "video",
|
||||
"video/quicktime": "video",
|
||||
}[mimeType] || "unknown"
|
||||
);
|
||||
}
|
||||
|
@ -34,7 +36,8 @@ class Uploadable extends events.EventTarget {
|
|||
this.flags = [];
|
||||
this.tags = [];
|
||||
this.relations = [];
|
||||
this.anonymous = false;
|
||||
this.anonymous = !api.isLoggedIn();
|
||||
this.forceAnonymous = !api.isLoggedIn();
|
||||
}
|
||||
|
||||
destroy() {}
|
||||
|
@ -118,6 +121,7 @@ class Url extends Uploadable {
|
|||
heif: "image/heif",
|
||||
heic: "image/heic",
|
||||
mp4: "video/mp4",
|
||||
mov: "video/quicktime",
|
||||
webm: "video/webm",
|
||||
};
|
||||
for (let extension of Object.keys(mime)) {
|
||||
|
@ -283,7 +287,7 @@ class PostUploadView extends events.EventTarget {
|
|||
for (let uploadable of this._uploadables) {
|
||||
this._updateUploadableFromDom(uploadable);
|
||||
}
|
||||
this._submitButtonNode.value = "Resume upload";
|
||||
this._submitButtonNode.value = "Resume";
|
||||
this._emit("submit");
|
||||
}
|
||||
|
||||
|
@ -358,6 +362,10 @@ class PostUploadView extends events.EventTarget {
|
|||
detail: {
|
||||
uploadables: this._uploadables,
|
||||
skipDuplicates: this._skipDuplicatesCheckboxNode.checked,
|
||||
alwaysUploadSimilar:
|
||||
this._alwaysUploadSimilarCheckboxNode.checked,
|
||||
pauseRemainOnError:
|
||||
this._pauseRemainOnErrorCheckboxNode.checked,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -421,6 +429,18 @@ class PostUploadView extends events.EventTarget {
|
|||
return this._hostNode.querySelector("form [name=skip-duplicates]");
|
||||
}
|
||||
|
||||
get _alwaysUploadSimilarCheckboxNode() {
|
||||
return this._hostNode.querySelector(
|
||||
"form [name=always-upload-similar]"
|
||||
);
|
||||
}
|
||||
|
||||
get _pauseRemainOnErrorCheckboxNode() {
|
||||
return this._hostNode.querySelector(
|
||||
"form [name=pause-remain-on-error]"
|
||||
);
|
||||
}
|
||||
|
||||
get _submitButtonNode() {
|
||||
return this._hostNode.querySelector("form [type=submit]");
|
||||
}
|
||||
|
|
|
@ -141,6 +141,34 @@ class BulkTagEditor extends BulkEditor {
|
|||
}
|
||||
}
|
||||
|
||||
class BulkDeleteEditor extends BulkEditor {
|
||||
constructor(hostNode) {
|
||||
super(hostNode);
|
||||
this._hostNode.addEventListener("submit", (e) =>
|
||||
this._evtFormSubmit(e)
|
||||
);
|
||||
}
|
||||
|
||||
_evtFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("deleteSelectedPosts", { detail: {} })
|
||||
);
|
||||
}
|
||||
|
||||
_evtOpenLinkClick(e) {
|
||||
e.preventDefault();
|
||||
this.toggleOpen(true);
|
||||
this.dispatchEvent(new CustomEvent("open", { detail: {} }));
|
||||
}
|
||||
|
||||
_evtCloseLinkClick(e) {
|
||||
e.preventDefault();
|
||||
this.toggleOpen(false);
|
||||
this.dispatchEvent(new CustomEvent("close", { detail: {} }));
|
||||
}
|
||||
}
|
||||
|
||||
class PostsHeaderView extends events.EventTarget {
|
||||
constructor(ctx) {
|
||||
super();
|
||||
|
@ -186,6 +214,13 @@ class PostsHeaderView extends events.EventTarget {
|
|||
this._bulkEditors.push(this._bulkSafetyEditor);
|
||||
}
|
||||
|
||||
if (this._bulkEditDeleteNode) {
|
||||
this._bulkDeleteEditor = new BulkDeleteEditor(
|
||||
this._bulkEditDeleteNode
|
||||
);
|
||||
this._bulkEditors.push(this._bulkDeleteEditor);
|
||||
}
|
||||
|
||||
for (let editor of this._bulkEditors) {
|
||||
editor.addEventListener("submit", (e) => {
|
||||
this._navigate();
|
||||
|
@ -204,6 +239,8 @@ class PostsHeaderView extends events.EventTarget {
|
|||
this._openBulkEditor(this._bulkTagEditor);
|
||||
} else if (ctx.parameters.safety && this._bulkSafetyEditor) {
|
||||
this._openBulkEditor(this._bulkSafetyEditor);
|
||||
} else if (ctx.parameters.delete && this._bulkDeleteEditor) {
|
||||
this._openBulkEditor(this._bulkDeleteEditor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,6 +264,10 @@ class PostsHeaderView extends events.EventTarget {
|
|||
return this._hostNode.querySelector(".bulk-edit-safety");
|
||||
}
|
||||
|
||||
get _bulkEditDeleteNode() {
|
||||
return this._hostNode.querySelector(".bulk-edit-delete");
|
||||
}
|
||||
|
||||
_openBulkEditor(editor) {
|
||||
editor.toggleOpen(true);
|
||||
this._hideBulkEditorsExcept(editor);
|
||||
|
@ -253,9 +294,8 @@ class PostsHeaderView extends events.EventTarget {
|
|||
e.target.classList.toggle("disabled");
|
||||
const safety = e.target.getAttribute("data-safety");
|
||||
let browsingSettings = settings.get();
|
||||
browsingSettings.listPosts[safety] = !browsingSettings.listPosts[
|
||||
safety
|
||||
];
|
||||
browsingSettings.listPosts[safety] =
|
||||
!browsingSettings.listPosts[safety];
|
||||
settings.save(browsingSettings, true);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", {
|
||||
|
@ -294,6 +334,10 @@ class PostsHeaderView extends events.EventTarget {
|
|||
this._bulkSafetyEditor && this._bulkSafetyEditor.opened
|
||||
? "1"
|
||||
: null;
|
||||
parameters.delete =
|
||||
this._bulkDeleteEditor && this._bulkDeleteEditor.opened
|
||||
? "1"
|
||||
: null;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("navigate", { detail: { parameters: parameters } })
|
||||
);
|
||||
|
|
|
@ -39,6 +39,13 @@ class PostsPageView extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
|
||||
if (deleteFlipperNode) {
|
||||
deleteFlipperNode.addEventListener("click", (e) =>
|
||||
this._evtBulkToggleDeleteClick(e, post)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._syncBulkEditorsHighlights();
|
||||
|
@ -56,6 +63,10 @@ class PostsPageView extends events.EventTarget {
|
|||
return listItemNode.querySelector(".safety-flipper");
|
||||
}
|
||||
|
||||
_getDeleteFlipperNode(listItemNode) {
|
||||
return listItemNode.querySelector(".delete-flipper");
|
||||
}
|
||||
|
||||
_evtPostChange(e) {
|
||||
const listItemNode = this._postIdToListItemNode[e.detail.post.id];
|
||||
for (let node of listItemNode.querySelectorAll("[data-disabled]")) {
|
||||
|
@ -99,6 +110,20 @@ class PostsPageView extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
_evtBulkToggleDeleteClick(e, post) {
|
||||
e.preventDefault();
|
||||
const linkNode = e.target;
|
||||
linkNode.classList.toggle("delete");
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("markForDeletion", {
|
||||
detail: {
|
||||
post,
|
||||
delete: linkNode.classList.contains("delete"),
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_syncBulkEditorsHighlights() {
|
||||
for (let listItemNode of this._listItemNodes) {
|
||||
const postId = listItemNode.getAttribute("data-post-id");
|
||||
|
@ -123,6 +148,16 @@ class PostsPageView extends events.EventTarget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFlipperNode = this._getDeleteFlipperNode(listItemNode);
|
||||
if (deleteFlipperNode) {
|
||||
deleteFlipperNode.classList.toggle(
|
||||
"delete",
|
||||
this._ctx.bulkEdit.markedForDeletion.some(
|
||||
(x) => x.id == postId
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,9 +72,8 @@ class UserTokenView extends events.EventTarget {
|
|||
|
||||
_evtDelete(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("delete", {
|
||||
detail: {
|
||||
|
@ -110,9 +109,8 @@ class UserTokenView extends events.EventTarget {
|
|||
|
||||
_evtChangeNoteClick(e) {
|
||||
e.preventDefault();
|
||||
const userToken = this._tokens[
|
||||
parseInt(e.target.getAttribute("data-token-id"))
|
||||
];
|
||||
const userToken =
|
||||
this._tokens[parseInt(e.target.getAttribute("data-token-id"))];
|
||||
const text = window.prompt(
|
||||
"Please enter the new name:",
|
||||
userToken.note !== null ? userToken.note : undefined
|
||||
|
|
274
client/package-lock.json
generated
274
client/package-lock.json
generated
|
@ -10,7 +10,7 @@
|
|||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
|
@ -28,7 +28,7 @@
|
|||
"jimp": "^0.13.0",
|
||||
"pretty-error": "^3.0.3",
|
||||
"stylus": "^0.54.8",
|
||||
"terser": "^3.7.7",
|
||||
"terser": "^4.8.1",
|
||||
"underscore": "^1.12.1",
|
||||
"watchify": "^4.0.0",
|
||||
"ws": "^7.4.6"
|
||||
|
@ -476,24 +476,6 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-filter": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
|
||||
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-map": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/array-reduce": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
|
@ -1506,16 +1488,15 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
|
@ -1682,9 +1663,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "2.5.7",
|
||||
|
@ -1850,9 +1831,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
|
@ -2254,8 +2235,7 @@
|
|||
"node_modules/function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"node_modules/get-assigned-identifiers": {
|
||||
"version": "1.2.0",
|
||||
|
@ -2267,7 +2247,6 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
|
@ -2351,7 +2330,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.1"
|
||||
},
|
||||
|
@ -2384,7 +2362,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
|
@ -2859,9 +2836,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/jpeg-js": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
|
@ -2997,14 +2974,14 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==",
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw==",
|
||||
"bin": {
|
||||
"marked": "bin/marked"
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/md5.js": {
|
||||
|
@ -3108,9 +3085,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
|
@ -3224,7 +3201,6 @@
|
|||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
|
@ -3385,9 +3361,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-platform": {
|
||||
|
@ -3507,11 +3483,17 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/querystring": {
|
||||
|
@ -3770,15 +3752,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
||||
"dev": true,
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"dependencies": {
|
||||
"array-filter": "~0.0.0",
|
||||
"array-map": "~0.0.0",
|
||||
"array-reduce": "~0.0.0",
|
||||
"jsonify": "~0.0.0"
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
|
@ -3983,12 +3972,6 @@
|
|||
"minimist": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/subarg/node_modules/minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-3.8.3.tgz",
|
||||
|
@ -4028,26 +4011,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"commander": "~2.14.1",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.6"
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/uglifyjs"
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map": {
|
||||
|
@ -4060,9 +4043,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/source-map-support": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
|
@ -5047,24 +5030,6 @@
|
|||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"array-filter": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
|
||||
"integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=",
|
||||
"dev": true
|
||||
},
|
||||
"array-map": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
|
||||
"integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
|
||||
"dev": true
|
||||
},
|
||||
"array-reduce": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
|
||||
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
|
||||
"dev": true
|
||||
},
|
||||
"asn1.js": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
|
||||
|
@ -6053,16 +6018,15 @@
|
|||
"dev": true
|
||||
},
|
||||
"cached-path-relative": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz",
|
||||
"integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.1.0.tgz",
|
||||
"integrity": "sha512-WF0LihfemtesFcJgO7xfOoOcnWzY/QHR4qeDqV44jPU3HTI54+LnfXK3SA27AVVGCdZFgjjFFaqUA9Jx7dMJZA==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.2"
|
||||
|
@ -6211,9 +6175,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz",
|
||||
"integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA=="
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.5.7",
|
||||
|
@ -6363,9 +6327,9 @@
|
|||
}
|
||||
},
|
||||
"decode-uri-component": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz",
|
||||
"integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
|
@ -6697,8 +6661,7 @@
|
|||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"get-assigned-identifiers": {
|
||||
"version": "1.2.0",
|
||||
|
@ -6710,7 +6673,6 @@
|
|||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
|
||||
"integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
|
@ -6778,7 +6740,6 @@
|
|||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
|
@ -6801,8 +6762,7 @@
|
|||
"has-symbols": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
|
||||
},
|
||||
"hash-base": {
|
||||
"version": "3.0.4",
|
||||
|
@ -7158,9 +7118,9 @@
|
|||
}
|
||||
},
|
||||
"jpeg-js": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.0.tgz",
|
||||
"integrity": "sha512-960VHmtN1vTpasX/1LupLohdP5odwAT7oK/VSm6mW0M58LbrBnowLAPWAZhWGhDAGjzbMnPXZxzB/QYgBwkN0w==",
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"dev": true
|
||||
},
|
||||
"js-cookie": {
|
||||
|
@ -7280,9 +7240,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"marked": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-0.7.0.tgz",
|
||||
"integrity": "sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg=="
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-4.0.10.tgz",
|
||||
"integrity": "sha512-+QvuFj0nGgO970fySghXGmuw+Fd0gD2x3+MqCWLIPf5oxdv1Ka6b2q+z9RP01P/IaKPMEramy+7cNy/Lw8c3hw=="
|
||||
},
|
||||
"md5.js": {
|
||||
"version": "1.3.4",
|
||||
|
@ -7364,9 +7324,9 @@
|
|||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"mkdirp": {
|
||||
|
@ -7466,8 +7426,7 @@
|
|||
"object-inspect": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz",
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw=="
|
||||
},
|
||||
"object-keys": {
|
||||
"version": "1.1.1",
|
||||
|
@ -7607,9 +7566,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"path-parse": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
|
||||
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true
|
||||
},
|
||||
"path-platform": {
|
||||
|
@ -7705,9 +7664,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
|
||||
"integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"querystring": {
|
||||
"version": "0.2.0",
|
||||
|
@ -7936,15 +7898,19 @@
|
|||
}
|
||||
},
|
||||
"shell-quote": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
|
||||
"integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
|
||||
"dev": true,
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
|
||||
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
|
||||
"dev": true
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
|
||||
"requires": {
|
||||
"array-filter": "~0.0.0",
|
||||
"array-map": "~0.0.0",
|
||||
"array-reduce": "~0.0.0",
|
||||
"jsonify": "~0.0.0"
|
||||
"call-bind": "^1.0.0",
|
||||
"get-intrinsic": "^1.0.2",
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"simple-concat": {
|
||||
|
@ -8116,14 +8082,6 @@
|
|||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"superagent": {
|
||||
|
@ -8159,20 +8117,20 @@
|
|||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "3.7.7",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.7.7.tgz",
|
||||
"integrity": "sha512-RRLIxE7S52vSOI9cEbOaisgBd2y6MNgfg2ihUkidsFnuP1eDmZ79+lBWbyvgfFTAc/r8nSjL0k3cpZDDIYiYiA==",
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz",
|
||||
"integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"commander": "~2.14.1",
|
||||
"commander": "^2.20.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.6"
|
||||
"source-map-support": "~0.5.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.14.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
|
||||
"integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==",
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
|
@ -8182,9 +8140,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.6.tgz",
|
||||
"integrity": "sha512-N4KXEz7jcKqPf2b2vZF11lQIz9W5ZMuUcIOGj243lduidkf2fjkVKJS9vNxVWn3u/uxX38AcE8U9nnH9FPcq+g==",
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
|
|
|
@ -3,14 +3,15 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"watch": "node build.js --watch"
|
||||
"watch": "node build.js --watch",
|
||||
"build-container": "docker build -t szurubooru/client:dev ."
|
||||
},
|
||||
"dependencies": {
|
||||
"dompurify": "^2.0.17",
|
||||
"font-awesome": "^4.7.0",
|
||||
"ios-inner-height": "^1.0.3",
|
||||
"js-cookie": "^2.2.0",
|
||||
"marked": "^0.7.0",
|
||||
"marked": "^4.0.10",
|
||||
"mousetrap": "^1.6.2",
|
||||
"nprogress": "^0.2.0",
|
||||
"superagent": "^3.8.3"
|
||||
|
@ -28,7 +29,7 @@
|
|||
"jimp": "^0.13.0",
|
||||
"pretty-error": "^3.0.3",
|
||||
"stylus": "^0.54.8",
|
||||
"terser": "^3.7.7",
|
||||
"terser": "^4.8.1",
|
||||
"underscore": "^1.12.1",
|
||||
"watchify": "^4.0.0",
|
||||
"ws": "^7.4.6"
|
||||
|
|
38
doc/API.md
38
doc/API.md
|
@ -37,6 +37,7 @@
|
|||
- [Creating post](#creating-post)
|
||||
- [Updating post](#updating-post)
|
||||
- [Getting post](#getting-post)
|
||||
- [Getting around post](#getting-around-post)
|
||||
- [Deleting post](#deleting-post)
|
||||
- [Merging posts](#merging-posts)
|
||||
- [Rating post](#rating-post)
|
||||
|
@ -53,7 +54,7 @@
|
|||
- [Deleting pool category](#deleting-pool-category)
|
||||
- [Setting default pool category](#setting-default-pool-category)
|
||||
- Pools
|
||||
- [Listing pools](#listing-pool)
|
||||
- [Listing pools](#listing-pools)
|
||||
- [Creating pool](#creating-pool)
|
||||
- [Updating pool](#updating-pool)
|
||||
- [Getting pool](#getting-pool)
|
||||
|
@ -164,9 +165,9 @@ way. The files, however, should be passed as regular fields appended with a
|
|||
accepts a file named `content`, the client should pass
|
||||
`{"contentUrl":"http://example.com/file.jpg"}` as a part of the JSON message
|
||||
body. When creating or updating post content using this method, the server can
|
||||
also be configured to employ [youtube-dl](https://github.com/ytdl-org/youtube-dl)
|
||||
to download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
youtube-dl can be configured with the `'uploads:use_downloader'` permission
|
||||
also be configured to employ [yt-dlp](https://github.com/yt-dlp/yt-dlp) to
|
||||
download content from popular sites such as youtube, gfycat, etc. Access to
|
||||
yt-dlp can be configured with the `'uploads:use_downloader'` permission
|
||||
|
||||
Finally, in some cases the user might want to reuse one file between the
|
||||
requests to save the bandwidth (for example, reverse search + consecutive
|
||||
|
@ -322,7 +323,7 @@ data.
|
|||
{
|
||||
"name": <name>,
|
||||
"color": <color>,
|
||||
"order": <order> // optional
|
||||
"order": <order>
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -951,6 +952,29 @@ data.
|
|||
|
||||
Retrieves information about an existing post.
|
||||
|
||||
## Getting around post
|
||||
- **Request**
|
||||
|
||||
`GET /post/<id>/around`
|
||||
|
||||
- **Output**
|
||||
|
||||
```json5
|
||||
{
|
||||
"prev": <post-resource>,
|
||||
"next": <post-resource>
|
||||
}
|
||||
```
|
||||
|
||||
- **Errors**
|
||||
|
||||
- the post does not exist
|
||||
- privileges are too low
|
||||
|
||||
- **Description**
|
||||
|
||||
Retrieves information about posts that are before or after an existing post.
|
||||
|
||||
## Deleting post
|
||||
- **Request**
|
||||
|
||||
|
@ -1365,7 +1389,7 @@ data.
|
|||
## Creating pool
|
||||
- **Request**
|
||||
|
||||
`POST /pools/create`
|
||||
`POST /pool`
|
||||
|
||||
- **Input**
|
||||
|
||||
|
@ -2467,7 +2491,7 @@ One file together with its metadata posted to the site.
|
|||
## Micro post
|
||||
**Description**
|
||||
|
||||
A [post resource](#post) stripped down to `name` and `thumbnailUrl` fields.
|
||||
A [post resource](#post) stripped down to `id` and `thumbnailUrl` fields.
|
||||
|
||||
## Note
|
||||
**Description**
|
||||
|
|
|
@ -34,13 +34,19 @@ and Docker Compose (version 1.6.0 or greater) already installed.
|
|||
Read the comments to guide you. Note that `.env` should be in the root
|
||||
directory of this repository.
|
||||
|
||||
### Running the Application
|
||||
4. Pull the containers:
|
||||
|
||||
Download containers:
|
||||
This pulls the latest containers from docker.io:
|
||||
```console
|
||||
user@host:szuru$ docker-compose pull
|
||||
```
|
||||
|
||||
If you have modified the application's source and would like to manually
|
||||
build it, follow the instructions in [**Building**](#Building) instead,
|
||||
then read here once you're done.
|
||||
|
||||
5. Run it!
|
||||
|
||||
For first run, it is recommended to start the database separately:
|
||||
```console
|
||||
user@host:szuru$ docker-compose up -d sql
|
||||
|
@ -57,11 +63,51 @@ user@host:szuru$ docker-compose logs -f
|
|||
# (CTRL+C to exit)
|
||||
```
|
||||
|
||||
To stop all containers:
|
||||
```console
|
||||
user@host:szuru$ docker-compose down
|
||||
### 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`.*
|
||||
|
||||
|
||||
### Additional Features
|
||||
|
||||
1. **CLI-level administrative tools**
|
||||
|
|
|
@ -10,6 +10,12 @@ BUILD_INFO=latest
|
|||
# otherwise the port specified here will be publicly accessible
|
||||
PORT=8080
|
||||
|
||||
# How many waitress threads to start
|
||||
# 4 is the default amount of threads. If you experience performance
|
||||
# degradation with a large number of posts, increasing this may
|
||||
# improve performance, since waitress is most likely clogging up with Tasks.
|
||||
THREADS=4
|
||||
|
||||
# URL base to run szurubooru under
|
||||
# See "Additional Features" section in INSTALL.md
|
||||
BASE_URL=/
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
##
|
||||
## Use this as a template to set up docker-compose, or as guide to set up other
|
||||
## orchestration services
|
||||
version: '2'
|
||||
#version: '2'
|
||||
|
||||
services:
|
||||
|
||||
server:
|
||||
image: szurubooru/server:latest
|
||||
# image: szurubooru/server:latest
|
||||
build: server
|
||||
depends_on:
|
||||
- sql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
## These should be the names of the dependent containers listed below,
|
||||
## 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_PORT: 5432
|
||||
#LOG_SQL: 0 (1 for verbose SQL logs)
|
||||
THREADS:
|
||||
volumes:
|
||||
- "${MOUNT_DATA}:/data"
|
||||
- "./server/config.yaml:/opt/app/config.yaml"
|
||||
|
||||
client:
|
||||
image: szurubooru/client:latest
|
||||
# image: szurubooru/client:latest
|
||||
build: client
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- server
|
||||
environment:
|
||||
|
@ -43,5 +48,7 @@ services:
|
|||
environment:
|
||||
POSTGRES_USER:
|
||||
POSTGRES_PASSWORD:
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
volumes:
|
||||
- "${MOUNT_SQL}:/var/lib/postgresql/data"
|
||||
|
|
|
@ -7,8 +7,13 @@ WORKDIR /opt/app
|
|||
RUN apk --no-cache add \
|
||||
python3 \
|
||||
python3-dev \
|
||||
ffmpeg \
|
||||
py3-pip \
|
||||
build-base \
|
||||
libheif \
|
||||
libheif-dev \
|
||||
libavif \
|
||||
libavif-dev \
|
||||
ffmpeg \
|
||||
# from requirements.txt:
|
||||
py3-yaml \
|
||||
py3-psycopg2 \
|
||||
|
@ -18,26 +23,21 @@ RUN apk --no-cache add \
|
|||
py3-pillow \
|
||||
py3-pynacl \
|
||||
py3-tz \
|
||||
py3-pyrfc3339 \
|
||||
build-base \
|
||||
&& apk --no-cache add \
|
||||
libheif \
|
||||
libavif \
|
||||
libheif-dev \
|
||||
libavif-dev \
|
||||
&& pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
alembic \
|
||||
py3-pyrfc3339
|
||||
RUN pip3 install --no-cache-dir --disable-pip-version-check \
|
||||
"alembic>=0.8.5" \
|
||||
"coloredlogs==5.0" \
|
||||
youtube_dl \
|
||||
pillow-avif-plugin \
|
||||
pyheif-pillow-opener \
|
||||
&& apk --no-cache del py3-pip
|
||||
"pyheif==0.6.1" \
|
||||
"heif-image-plugin>=0.3.2" \
|
||||
yt-dlp \
|
||||
"pillow-avif-plugin~=1.1.0"
|
||||
RUN apk --no-cache del py3-pip
|
||||
|
||||
COPY ./ /opt/app/
|
||||
RUN rm -rf /opt/app/szurubooru/tests
|
||||
|
||||
|
||||
FROM prereqs as testing
|
||||
FROM --platform=$BUILDPLATFORM prereqs as testing
|
||||
WORKDIR /opt/app
|
||||
|
||||
RUN apk --no-cache add \
|
||||
|
@ -83,6 +83,9 @@ ARG PORT=6666
|
|||
ENV PORT=${PORT}
|
||||
EXPOSE ${PORT}
|
||||
|
||||
ARG THREADS=4
|
||||
ENV THREADS=${THREADS}
|
||||
|
||||
VOLUME ["/data/"]
|
||||
|
||||
ARG DOCKER_REPO
|
||||
|
|
|
@ -103,6 +103,7 @@ privileges:
|
|||
'posts:edit:content': power
|
||||
'posts:edit:flags': regular
|
||||
'posts:edit:notes': regular
|
||||
'posts:edit:desc': regular
|
||||
'posts:edit:relations': regular
|
||||
'posts:edit:safety': power
|
||||
'posts:edit:source': regular
|
||||
|
@ -115,6 +116,7 @@ privileges:
|
|||
'posts:favorite': regular
|
||||
'posts:bulk-edit:tags': power
|
||||
'posts:bulk-edit:safety': power
|
||||
'posts:bulk-edit:delete': power
|
||||
|
||||
'tags:create': regular
|
||||
'tags:edit:names': power
|
||||
|
|
|
@ -4,5 +4,5 @@ cd /opt/app
|
|||
|
||||
alembic upgrade head
|
||||
|
||||
echo "Starting szurubooru API on port ${PORT}"
|
||||
exec waitress-serve-3 --port ${PORT} szurubooru.facade:app
|
||||
echo "Starting szurubooru API on port ${PORT} - Running on ${THREADS} threads"
|
||||
exec waitress-serve-3 --port ${PORT} --threads ${THREADS} szurubooru.facade:app
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
docker build \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg SOURCE_COMMIT \
|
||||
--build-arg DOCKER_REPO \
|
||||
-f $DOCKERFILE_PATH -t $IMAGE_NAME .
|
|
@ -1,19 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
add_tag() {
|
||||
echo "Also tagging image as ${DOCKER_REPO}:${1}"
|
||||
docker tag $IMAGE_NAME $DOCKER_REPO:$1
|
||||
docker push $DOCKER_REPO:$1
|
||||
}
|
||||
|
||||
CLOSEST_VER=$(git describe --tags --abbrev=0)
|
||||
CLOSEST_MAJOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f1)
|
||||
CLOSEST_MINOR_VER=$(echo ${CLOSEST_VER} | cut -d'.' -f2)
|
||||
|
||||
add_tag "${CLOSEST_MAJOR_VER}-edge"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}-edge"
|
||||
|
||||
if git describe --exact-match --abbrev=0 2> /dev/null; then
|
||||
add_tag "${CLOSEST_MAJOR_VER}"
|
||||
add_tag "${CLOSEST_MAJOR_VER}.${CLOSEST_MINOR_VER}"
|
||||
fi
|
|
@ -1,8 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
docker run --rm \
|
||||
-t $(docker build --target testing -q .) \
|
||||
--color=no szurubooru/
|
||||
|
||||
exit $?
|
|
@ -1,14 +1,15 @@
|
|||
alembic>=0.8.5
|
||||
pyyaml>=3.11
|
||||
psycopg2-binary>=2.6.1
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
coloredlogs==5.0
|
||||
certifi>=2017.11.5
|
||||
coloredlogs==5.0
|
||||
heif-image-plugin==0.3.2
|
||||
numpy>=1.8.2
|
||||
pillow-avif-plugin~=1.1.0
|
||||
pillow>=4.3.0
|
||||
psycopg2-binary>=2.6.1
|
||||
pyheif==0.6.1
|
||||
pynacl>=1.2.1
|
||||
pytz>=2018.3
|
||||
pyRFC3339>=1.0
|
||||
pillow-avif-plugin>=1.1.0
|
||||
pyheif-pillow-opener>=0.1.0
|
||||
youtube_dl
|
||||
pytz>=2018.3
|
||||
pyyaml>=3.11
|
||||
SQLAlchemy>=1.0.12, <1.4
|
||||
yt-dlp
|
||||
|
|
|
@ -91,6 +91,15 @@ def reset_filenames() -> None:
|
|||
rename_in_dir("posts/custom-thumbnails/")
|
||||
|
||||
|
||||
def regenerate_thumbnails() -> None:
|
||||
for post in db.session.query(model.Post).all():
|
||||
print("Generating tumbnail for post %d ..." % post.post_id, end="\r")
|
||||
try:
|
||||
postfuncs.generate_post_thumbnail(post)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser_top = ArgumentParser(
|
||||
description="Collection of CLI commands for an administrator to use",
|
||||
|
@ -114,6 +123,12 @@ def main() -> None:
|
|||
help="reset and rename the content and thumbnail "
|
||||
"filenames in case of a lost/changed secret key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--regenerate-thumbnails",
|
||||
action="store_true",
|
||||
help="regenerate the thumbnails for posts if the "
|
||||
"thumbnail files are missing",
|
||||
)
|
||||
command = parser_top.parse_args()
|
||||
|
||||
try:
|
||||
|
@ -123,6 +138,8 @@ def main() -> None:
|
|||
check_audio()
|
||||
elif command.reset_filenames:
|
||||
reset_filenames()
|
||||
elif command.regenerate_thumbnails:
|
||||
regenerate_thumbnails()
|
||||
except errors.BaseError as e:
|
||||
print(e, file=stderr)
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ def create_post(
|
|||
source = ctx.get_param_as_string("contentUrl", default="")
|
||||
relations = ctx.get_param_as_int_list("relations", 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", default=posts.get_default_flags(content)
|
||||
)
|
||||
|
@ -85,6 +86,7 @@ def create_post(
|
|||
posts.update_post_source(post, source)
|
||||
posts.update_post_relations(post, relations)
|
||||
posts.update_post_notes(post, notes)
|
||||
posts.update_post_desc(post, desc)
|
||||
posts.update_post_flags(post, flags)
|
||||
if ctx.has_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"):
|
||||
auth.verify_privilege(ctx.user, "posts:edit: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"):
|
||||
auth.verify_privilege(ctx.user, "posts:edit: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
|
||||
|
||||
|
||||
def _docker_config() -> Dict:
|
||||
def _container_config() -> Dict:
|
||||
if "TEST_ENVIRONMENT" not in os.environ:
|
||||
for key in ["POSTGRES_USER", "POSTGRES_PASSWORD", "POSTGRES_HOST"]:
|
||||
if key not in os.environ:
|
||||
|
@ -33,7 +33,7 @@ def _docker_config() -> Dict:
|
|||
"show_sql": int(os.getenv("LOG_SQL", 0)),
|
||||
"data_url": os.getenv("DATA_URL", "data/"),
|
||||
"data_dir": "/data/",
|
||||
"database": "postgres://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
"database": "postgresql://%(user)s:%(pass)s@%(host)s:%(port)d/%(db)s"
|
||||
% {
|
||||
"user": os.getenv("POSTGRES_USER"),
|
||||
"pass": os.getenv("POSTGRES_PASSWORD"),
|
||||
|
@ -49,6 +49,15 @@ def _file_config(filename: str) -> Dict:
|
|||
return yaml.load(handle.read(), Loader=yaml.SafeLoader) or {}
|
||||
|
||||
|
||||
def _running_inside_container() -> bool:
|
||||
env = os.environ.keys()
|
||||
return (
|
||||
os.path.exists("/.dockerenv")
|
||||
or "KUBERNETES_SERVICE_HOST" in env
|
||||
or "container" in env # set by lxc/podman
|
||||
)
|
||||
|
||||
|
||||
def _read_config() -> Dict:
|
||||
ret = _file_config("config.yaml.dist")
|
||||
if os.path.isfile("config.yaml"):
|
||||
|
@ -57,8 +66,8 @@ def _read_config() -> Dict:
|
|||
logger.warning(
|
||||
"'config.yaml' should be a file, not a directory, skipping"
|
||||
)
|
||||
if os.path.exists("/.dockerenv"):
|
||||
ret = _merge(ret, _docker_config())
|
||||
if _running_inside_container():
|
||||
ret = _merge(ret, _container_config())
|
||||
return ret
|
||||
|
||||
|
||||
|
|
|
@ -4,12 +4,10 @@ from datetime import datetime
|
|||
from io import BytesIO
|
||||
from typing import Any, Callable, List, Optional, Set, Tuple
|
||||
|
||||
import HeifImagePlugin
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
import pillow_avif
|
||||
import pyheif
|
||||
from pyheif_pillow_opener import register_heif_opener
|
||||
register_heif_opener()
|
||||
from PIL import Image
|
||||
|
||||
from szurubooru import config, errors
|
||||
|
||||
|
@ -44,7 +42,7 @@ def _preprocess_image(content: bytes) -> NpMatrix:
|
|||
try:
|
||||
img = Image.open(BytesIO(content))
|
||||
return np.asarray(img.convert("L"), dtype=np.uint8)
|
||||
except IOError:
|
||||
except (IOError, ValueError):
|
||||
raise errors.ProcessingError(
|
||||
"Unable to generate a signature hash " "for this image."
|
||||
)
|
||||
|
|
|
@ -6,6 +6,9 @@ import shlex
|
|||
import subprocess
|
||||
from io import BytesIO
|
||||
from typing import List
|
||||
|
||||
import HeifImagePlugin
|
||||
import pillow_avif
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from szurubooru import errors
|
||||
|
@ -17,7 +20,7 @@ logger = logging.getLogger(__name__)
|
|||
def convert_heif_to_png(content: bytes) -> bytes:
|
||||
img = PILImage.open(BytesIO(content))
|
||||
img_byte_arr = BytesIO()
|
||||
img.save(img_byte_arr, format='PNG')
|
||||
img.save(img_byte_arr, format="PNG")
|
||||
return img_byte_arr.getvalue()
|
||||
|
||||
|
||||
|
@ -96,7 +99,7 @@ class Image:
|
|||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=white:s=%dx%d" % (self.width, self.height),
|
||||
"color=black:s=%dx%d" % (self.width, self.height),
|
||||
"-i",
|
||||
"{path}",
|
||||
"-f",
|
||||
|
@ -276,10 +279,10 @@ class Image:
|
|||
proc = subprocess.Popen(
|
||||
cli,
|
||||
stdout=subprocess.PIPE,
|
||||
stdin=subprocess.PIPE,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
out, err = proc.communicate(input=self.content)
|
||||
out, err = proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
logger.warning(
|
||||
"Failed to execute ffmpeg command (cli=%r, err=%r)",
|
||||
|
|
|
@ -36,9 +36,12 @@ def get_mime_type(content: bytes) -> str:
|
|||
if content[0:4] == b"\x1A\x45\xDF\xA3":
|
||||
return "video/webm"
|
||||
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypmp42", b"ftypM4V "):
|
||||
if content[4:12] in (b"ftypisom", b"ftypiso5", b"ftypiso6", b"ftypmp42", b"ftypM4V "):
|
||||
return "video/mp4"
|
||||
|
||||
if content[4:12] == b"ftypqt ":
|
||||
return "video/quicktime"
|
||||
|
||||
return "application/octet-stream"
|
||||
|
||||
|
||||
|
@ -54,6 +57,7 @@ def get_extension(mime_type: str) -> Optional[str]:
|
|||
"image/heif": "heif",
|
||||
"image/heic": "heic",
|
||||
"video/mp4": "mp4",
|
||||
"video/quicktime": "mov",
|
||||
"video/webm": "webm",
|
||||
"application/octet-stream": "dat",
|
||||
}
|
||||
|
@ -65,7 +69,12 @@ def is_flash(mime_type: str) -> bool:
|
|||
|
||||
|
||||
def is_video(mime_type: str) -> bool:
|
||||
return mime_type.lower() in ("application/ogg", "video/mp4", "video/webm")
|
||||
return mime_type.lower() in (
|
||||
"application/ogg",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/webm",
|
||||
)
|
||||
|
||||
|
||||
def is_image(mime_type: str) -> bool:
|
||||
|
@ -88,6 +97,7 @@ def is_animated_gif(content: bytes) -> bool:
|
|||
and len(re.findall(pattern, content)) > 1
|
||||
)
|
||||
|
||||
|
||||
def is_heif(mime_type: str) -> bool:
|
||||
return mime_type.lower() in (
|
||||
"image/heif",
|
||||
|
|
|
@ -39,13 +39,20 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
|||
length_tally = 0
|
||||
try:
|
||||
with urllib.request.urlopen(request) as handle:
|
||||
while (chunk := handle.read(_dl_chunk_size)) :
|
||||
while chunk := handle.read(_dl_chunk_size):
|
||||
length_tally += len(chunk)
|
||||
if length_tally > config.config["max_dl_filesize"]:
|
||||
raise DownloadTooLargeError(url)
|
||||
raise DownloadTooLargeError(
|
||||
"Download target exceeds maximum. (%d)"
|
||||
% (config.config["max_dl_filesize"]),
|
||||
extra_fields={"URL": url},
|
||||
)
|
||||
content_buffer += chunk
|
||||
except urllib.error.HTTPError as ex:
|
||||
raise DownloadError(url) from ex
|
||||
raise DownloadError(
|
||||
"Download target returned HTTP %d. (%s)" % (ex.code, ex.reason),
|
||||
extra_fields={"URL": url},
|
||||
) from ex
|
||||
|
||||
if (
|
||||
youtube_dl_error
|
||||
|
@ -57,7 +64,7 @@ def download(url: str, use_video_downloader: bool = False) -> bytes:
|
|||
|
||||
|
||||
def _get_youtube_dl_content_url(url: str) -> str:
|
||||
cmd = ["youtube-dl", "--format", "best", "--no-playlist"]
|
||||
cmd = ["yt-dlp", "--format", "best", "--no-playlist"]
|
||||
if config.config["user_agent"]:
|
||||
cmd.extend(["--user-agent", config.config["user_agent"]])
|
||||
cmd.extend(["--get-url", url])
|
||||
|
@ -69,7 +76,8 @@ def _get_youtube_dl_content_url(url: str) -> str:
|
|||
)
|
||||
except subprocess.CalledProcessError:
|
||||
raise errors.ThirdPartyError(
|
||||
"Could not extract content location from %s" % (url)
|
||||
"Could not extract content location from URL.",
|
||||
extra_fields={"URL": url},
|
||||
) from None
|
||||
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@ class PostSerializer(serialization.BaseSerializer):
|
|||
"favoritedBy": self.serialize_favorited_by,
|
||||
"hasCustomThumbnail": self.serialize_has_custom_thumbnail,
|
||||
"notes": self.serialize_notes,
|
||||
"desc": self.serialize_desc,
|
||||
"comments": self.serialize_comments,
|
||||
"pools": self.serialize_pools,
|
||||
}
|
||||
|
@ -328,6 +329,9 @@ class PostSerializer(serialization.BaseSerializer):
|
|||
key=lambda x: x["polygon"],
|
||||
)
|
||||
|
||||
def serialize_desc(self) -> Any:
|
||||
return self.post.desc
|
||||
|
||||
def serialize_comments(self) -> Any:
|
||||
return [
|
||||
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:
|
||||
assert post
|
||||
target_flags = []
|
||||
|
|
|
@ -29,6 +29,7 @@ def upgrade():
|
|||
sa.Column("image_width", sa.Integer(), nullable=True),
|
||||
sa.Column("image_height", sa.Integer(), nullable=True),
|
||||
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_score", 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")
|
||||
|
||||
|
||||
class PostRelation(Base):
|
||||
__tablename__ = "post_relation"
|
||||
|
||||
|
@ -222,6 +221,7 @@ class Post(Base):
|
|||
canvas_width = sa.Column("image_width", sa.Integer)
|
||||
canvas_height = sa.Column("image_height", sa.Integer)
|
||||
mime_type = sa.Column("mime-type", sa.Unicode(32), nullable=False)
|
||||
desc = sa.Column("description", sa.UnicodeText, nullable=True, default="")
|
||||
|
||||
# foreign tables
|
||||
user = sa.orm.relationship("User")
|
||||
|
|
|
@ -122,6 +122,34 @@ def _pool_filter(
|
|||
)(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):
|
||||
def __init__(self) -> None:
|
||||
self.user = None # type: Optional[model.User]
|
||||
|
@ -349,6 +377,7 @@ class PostSearchConfig(BaseSearchConfig):
|
|||
),
|
||||
),
|
||||
(["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(
|
||||
config_injector, context_factory, tag_factory, user_factory
|
||||
config_injector, context_factory, tag_factory, user_factory, type
|
||||
):
|
||||
tag = tag_factory(names=["tag"])
|
||||
db.session.add(tag)
|
||||
|
@ -165,16 +166,7 @@ def test_trying_to_create_tags_without_privileges(
|
|||
with pytest.raises(errors.AuthError):
|
||||
api.tag_api.update_tag(
|
||||
context_factory(
|
||||
params={"suggestions": ["tag1", "tag2"], "version": 1},
|
||||
user=user_factory(rank=model.User.RANK_REGULAR),
|
||||
),
|
||||
{"tag_name": "tag"},
|
||||
)
|
||||
db.session.rollback()
|
||||
with pytest.raises(errors.AuthError):
|
||||
api.tag_api.update_tag(
|
||||
context_factory(
|
||||
params={"implications": ["tag1", "tag2"], "version": 1},
|
||||
params={type: ["tag1", "tag2"], "version": 1},
|
||||
user=user_factory(rank=model.User.RANK_REGULAR),
|
||||
),
|
||||
{"tag_name": "tag"},
|
||||
|
|
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)
|
||||
|
||||
|
||||
@pytest.yield_fixture(scope="function", autouse=True)
|
||||
def session(query_logger, postgresql_db):
|
||||
@pytest.fixture(scope="function", autouse=True)
|
||||
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
|
||||
postgresql_db.create_table(*model.Base.metadata.sorted_tables)
|
||||
try:
|
||||
yield postgresql_db.session
|
||||
finally:
|
||||
postgresql_db.reset_db()
|
||||
db.session = old_db_session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -7,6 +7,7 @@ from szurubooru.func import mime
|
|||
"input_path,expected_mime_type",
|
||||
[
|
||||
("mp4.mp4", "video/mp4"),
|
||||
("mov.mov", "video/quicktime"),
|
||||
("webm.webm", "video/webm"),
|
||||
("flash.swf", "application/x-shockwave-flash"),
|
||||
("png.png", "image/png"),
|
||||
|
@ -35,6 +36,7 @@ def test_get_mime_type_for_empty_file():
|
|||
[
|
||||
("video/mp4", "mp4"),
|
||||
("video/webm", "webm"),
|
||||
("video/quicktime", "mov"),
|
||||
("application/x-shockwave-flash", "swf"),
|
||||
("image/png", "png"),
|
||||
("image/jpeg", "jpg"),
|
||||
|
@ -70,6 +72,8 @@ def test_is_flash(input_mime_type, expected_state):
|
|||
("VIDEO/WEBM", True),
|
||||
("video/mp4", True),
|
||||
("VIDEO/MP4", True),
|
||||
("video/quicktime", True),
|
||||
("VIDEO/QUICKTIME", True),
|
||||
("video/anything_else", False),
|
||||
("application/ogg", True),
|
||||
("not a video", False),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
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():
|
||||
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
||||
|
||||
|
@ -62,6 +67,9 @@ def test_download():
|
|||
assert actual_content == expected_content
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"url",
|
||||
[
|
||||
|
@ -74,6 +82,9 @@ def test_too_large_download(url):
|
|||
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(
|
||||
"url,expected_sha1",
|
||||
[
|
||||
|
@ -96,6 +107,9 @@ def test_content_download(url, 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():
|
||||
url = "http://info.cern.ch/hypertext/WWW/TheProject.html"
|
||||
with pytest.raises(errors.ThirdPartyError):
|
||||
|
@ -108,11 +122,13 @@ def test_no_webhooks(config_injector):
|
|||
assert len(res) == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"TEST_NET" not in os.environ, reason="Network tests skipped by default."
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"webhook,status_code",
|
||||
[
|
||||
("https://postman-echo.com/post", 200),
|
||||
("http://localhost/", 400),
|
||||
("https://postman-echo.com/get", 400),
|
||||
],
|
||||
)
|
||||
|
@ -121,6 +137,9 @@ def test_single_webhook(config_injector, webhook, 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):
|
||||
config_injector(
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import pytest # noqa: F401
|
||||
|
||||
from szurubooru import db, model
|
||||
from szurubooru.func import snapshots, users
|
||||
|
@ -144,46 +144,6 @@ def test_create(tag_factory, user_factory):
|
|||
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):
|
||||
tag = tag_factory(names=["dummy"])
|
||||
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")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["name", "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": ".*"})
|
||||
for name in ["name", "NAME"]:
|
||||
category = tag_category_factory(name="name")
|
||||
db.session.add(category)
|
||||
db.session.flush()
|
||||
tag_categories.update_category_name(category, name)
|
||||
assert category.name == name
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
|
||||
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]*$"})
|
||||
for name in list("aA"):
|
||||
tag = tag_factory(names=["a"])
|
||||
db.session.add(tag)
|
||||
db.session.flush()
|
||||
tags.update_tag_names(tag, [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):
|
||||
|
@ -533,7 +532,6 @@ def test_update_tag_names_changing_primary_name(config_injector, tag_factory):
|
|||
db.session.flush()
|
||||
db.session.refresh(tag)
|
||||
assert [tag_name.name for tag_name in tag.names] == ["b", "a"]
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("attempt", ["name", "NAME", "alias", "ALIAS"])
|
||||
|
|
|
@ -136,8 +136,6 @@ def test_escaping(
|
|||
)
|
||||
db.session.flush()
|
||||
|
||||
if db_driver and db.session.get_bind().driver != db_driver:
|
||||
pytest.xfail()
|
||||
if expected_pool_names is None:
|
||||
with pytest.raises(errors.SearchError):
|
||||
executor.execute(input, offset=0, limit=100)
|
||||
|
|
|
@ -863,3 +863,55 @@ def test_tumbleweed(
|
|||
db.session.flush()
|
||||
verify_unpaged("special:tumbleweed", [4])
|
||||
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()
|
||||
|
||||
if db_driver and db.session.get_bind().driver != db_driver:
|
||||
pytest.xfail()
|
||||
if expected_tag_names is None:
|
||||
with pytest.raises(errors.SearchError):
|
||||
executor.execute(input, offset=0, limit=100)
|
||||
|
|
Reference in a new issue