Compare commits

...

182 commits
2.4 ... master

Author SHA1 Message Date
48d5dfb4e6 added description column to DB and desc field 2025-02-08 18:30:05 +01:00
541fec20ca Merge pull request 'adding-description---server-side' (#1) from adding-description---server-side into master
Reviewed-on: #1
2025-02-08 15:53:27 +01:00
be280acb17 beginning of description view 2025-02-08 15:52:15 +01:00
sol
bc7c2c7867 change cost defaults 2025-02-08 16:12:41 +02:00
2e7547d3bc added api end points and database models, readded transparency color 2025-02-08 11:52:43 +01:00
39bb53528c made transparency grid transparent
Some checks failed
Build Docker containers / Build and push client/ Docker container (push) Has been cancelled
Build Docker containers / Build and push server/ Docker container (push) Has been cancelled
Run unit tests / Run pytest for server/ (push) Failing after 32s
2025-02-07 21:23:26 +01:00
5ef62b21a0 personalized
Some checks failed
Build Docker containers / Build and push client/ Docker container (push) Has been cancelled
Build Docker containers / Build and push server/ Docker container (push) Has been cancelled
Run unit tests / Run pytest for server/ (push) Has been cancelled
2025-01-13 21:14:55 +01:00
anbosuki
61b9f81e39 Fixed the google search option in the post details view 2024-11-17 16:48:24 +01:00
Zak B. Elep
b721865931 server/config: generalize container support
Allow running in Kubernetes, podman, and LXC, besides plain docker-compose,
without having to fake out /.dockerenv in non-Docker environments.
2024-11-10 15:44:39 +01:00
Neo
46e3295003
Upload from clipboard (#414)
client/upload: upload from clipboard

Co-authored-by: Eva <evauwu@riseup.net>
2024-09-29 14:54:53 +02:00
Zak B. Elep
031131506e client/css: fix comment word-break
`break-all` makes it hard to read actual comments.
2024-09-29 13:48:06 +02:00
Neo
d102578b54
Merge pull request #647 from po5/null-checks
client: add null checks
2024-04-27 21:23:16 +02:00
Neo
6edb25d87b
Merge pull request #641 from po5/mobile
Mobile improvements
2024-04-26 22:56:58 +02:00
Neo
93fc15f2a4
Merge pull request #642 from po5/better-links 2024-04-26 22:37:54 +02:00
Neo
4f9d46e1c2
Merge branch 'master' into better-links 2024-04-26 22:16:37 +02:00
Eva
b72e81850d client: add null checks 2024-03-28 13:31:48 +01:00
Eva
c1c695f082 client/css: stack bulk tagging toggles horizontally on mobile 2024-03-21 22:26:49 +01:00
Eva
4b6b231fc8 client/posts: reorder elements in mobile layout
Navigation is always right below the image, and comments are always
at the very bottom, to minimize scrolling for common actions.
2024-03-21 22:26:28 +01:00
Eva
6b0c3cfc7f client/html: allow mobile browsers to zoom in 2024-03-21 22:23:45 +01:00
Eva
4ec8cb3ba2 client/css: constrain thumbnails to parent to prevent overextended links 2024-03-21 22:19:46 +01:00
Eva
8d971234a2 client/views: better pool name fallback 2024-03-21 22:16:05 +01:00
Eva
a16bb198ab client/views: more thorough link fallbacks
Prevents a bunch of errors that can happen when a resource is deleted.
2024-03-21 21:53:11 +01:00
Eva
3f182a66ad client/posts: fix overextended tag link 2024-03-21 21:52:52 +01:00
Eva
b52363e82d client/posts: fix overextended download link 2024-03-21 21:52:49 +01:00
Eva
3bf45e4c0a client/users: fix overextended avatar links 2024-03-21 21:52:39 +01:00
hujle
5596f53744 posts page ugly horizontal bar fix
fixes ugly horizontal scrollbar appearing when a post with extremely wide image is present in the posts list
2024-02-29 20:56:27 +01:00
neobooru
da425afc49 Pin pillow-avif-plugin to compatible version range 2024-02-21 17:47:27 +01:00
Xnoe
d7394d672f Fix Pool Search 2024-02-21 01:27:00 +01:00
neobooru
190d795426 doc: fix small error in pool API docs 2023-12-05 21:31:23 +01:00
ewof
7c92ceaf6a fix overflow on comments, prevents ugly unnecesary horizontal scroll 2023-11-05 12:27:03 +01:00
Neo
9e00f37464
Merge pull request #597 from zakame/use-yt-dlp
server/net: use yt-dlp instead of youtube-dl
2023-11-05 12:22:03 +01:00
Zak B. Elep
59c497e168 doc: update for yt-dlp 2023-08-17 20:58:09 +08:00
Zak B. Elep
c292b96f06 server/net: use yt-dlp instead of youtube-dl
youtube-dl no longer even gets URLs properly, so switch to yt-dlp as a
drop-in replacement for it.
2023-08-17 20:41:50 +08:00
neobooru
7a82e9d581 tests/server: post category filter 2023-07-05 12:22:11 +00:00
neobooru
4806bbe0ed server: post category filter 2023-07-05 12:22:11 +00:00
Yochyo
c2fdc2d070 docs (tag categories): order is required when creating tag category 2023-06-26 20:49:48 +02:00
Yochyo
ffdf115714 docs (api): change micro post attribute name to id 2023-06-26 20:49:48 +02:00
Shyam Sunder
782f069031 client/upload: fix thumbnail width in post uploads
Fixes regression caused by 648121d7
2023-04-17 19:50:40 -04:00
Shyam Sunder
81f7ae8034 client: fix post flow view on webkit browsers
Merge branch 'SediSocks-master'
2023-04-17 12:30:21 -04:00
Shyam Sunder
648121d7c3 client+server: add quicktime video support
Merge branch 'skybldev-upstream'
2023-04-17 12:21:26 -04:00
Shyam Sunder
42524503b9 client/tests: add unit tests for quicktime videos 2023-04-17 12:01:20 -04:00
skybldev
8a03015349 client+server: added quicktime upload support 2023-04-17 11:36:44 -04:00
Shyam Sunder
2165b59158 client: merge dependabot version bumps
Merge remote-tracking branches:
- 'project/dependabot/npm_and_yarn/client/cookiejar-2.1.4'
- 'project/dependabot/npm_and_yarn/client/decode-uri-component-0.2.2'
- 'project/dependabot/npm_and_yarn/client/jpeg-js-0.4.4'
- 'project/dependabot/npm_and_yarn/client/minimist-1.2.6'
- project/dependabot/npm_and_yarn/client/qs-6.11.0'
- 'project/dependabot/npm_and_yarn/client/shell-quote-1.7.3'
- 'project/dependabot/npm_and_yarn/client/terser-4.8.1'
2023-04-17 11:30:47 -04:00
Shyam Sunder
244a0f0b6c server/test: skip network tests by default 2023-04-17 10:31:35 -04:00
Shyam Sunder
da3b4790ad server+client: bump versions in pre-commit 2023-04-17 10:31:35 -04:00
SediSocks
196f92593c
fix flow view on webkit browsers 2023-03-13 19:53:02 +00:00
neobooru
d7d2a151a8 client: workaround for #545, but not a fix 2023-01-24 22:19:24 +01:00
dependabot[bot]
75635bbc43
build(deps): bump cookiejar from 2.1.2 to 2.1.4 in /client
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.2 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

---
updated-dependencies:
- dependency-name: cookiejar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-23 20:36:57 +00:00
Neo
e3062b1c77
client: add bulk delete feature (#459)
This introduces a new privilege 'posts:bulk-edit:delete' which by default is given to power users.
2023-01-19 18:44:31 +01:00
dependabot[bot]
e950fe7ea5
build(deps): bump qs from 6.5.2 to 6.11.0 in /client
Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.11.0.
- [Release notes](https://github.com/ljharb/qs/releases)
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.11.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-07 17:36:52 +00:00
dependabot[bot]
86f50ec742
build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 in /client
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-07 15:29:20 +00:00
w1kl4s
8088ff3bbe support ftypiso6 file signature 2022-09-13 19:18:22 +02:00
dependabot[bot]
da71c672dd
build(deps-dev): bump terser from 3.7.7 to 4.8.1 in /client
Bumps [terser](https://github.com/terser/terser) from 3.7.7 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 01:23:35 +00:00
dependabot[bot]
42bb364dd0
build(deps): bump shell-quote from 1.6.1 to 1.7.3 in /client
Bumps [shell-quote](https://github.com/substack/node-shell-quote) from 1.6.1 to 1.7.3.
- [Release notes](https://github.com/substack/node-shell-quote/releases)
- [Changelog](https://github.com/substack/node-shell-quote/blob/master/CHANGELOG.md)
- [Commits](https://github.com/substack/node-shell-quote/compare/1.6.1...1.7.3)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-21 21:38:21 +00:00
dependabot[bot]
5b43c5bebd
build(deps): bump jpeg-js from 0.4.0 to 0.4.4 in /client
Bumps [jpeg-js](https://github.com/eugeneware/jpeg-js) from 0.4.0 to 0.4.4.
- [Release notes](https://github.com/eugeneware/jpeg-js/releases)
- [Commits](https://github.com/eugeneware/jpeg-js/compare/v0.4.0...v0.4.4)

---
updated-dependencies:
- dependency-name: jpeg-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-06-17 01:45:34 +00:00
Luna
6c3b50d287 doc: add GET /post/<id>/around to API.md 2022-06-10 01:49:07 +02:00
neobooru
6075ae9326 all: add .gitattributes
This forces shell scripts to always have LF line endings. By default Windows uses CRLF which breaks the docker build, because docker-start.sh doesn't have the correct line endings. Adding this file should fix that.
2022-05-02 13:04:07 +02:00
dependabot[bot]
70f2164dc6 build(deps): bump minimist from 1.2.5 to 1.2.6 in /client
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-31 18:56:34 -04:00
Shyam Sunder
1b9ce79f4e client+server: only trigger autobuild on master branch pushes 2022-03-31 18:54:08 -04:00
dependabot[bot]
7e5d48b6e8
build(deps): bump minimist from 1.2.5 to 1.2.6 in /client
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-31 22:45:47 +00:00
Shyam Sunder
e746f09911 server: fix build error due to broken pip requirements
Pinned pyheif to v0.6.1
2022-03-31 18:43:37 -04:00
Shyam Sunder
6088e89ea1 server/szuru-admin: Add thumbnail regeneration script
Closes #467
2022-03-30 23:04:16 -04:00
Skybbles
79d0efc25b doc: added BuildKit flags fix to INSTALL.md
Added this because recently, there have been more problems with `docker-compose build` where it errors:

    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

Recent Docker versions have switched to using `buildx` (BuildKit) to build containers, but that needs to be enabled, either in `daemon.json` or through an environment variable. But since we are using Docker Compose, it doesn't pass it to Docker; so the environment variable needs to be set. At least that's what I've heard and figured out sweat_smile My explanation might be very wrong - but it works :)
2022-03-30 22:47:03 -04:00
Maksymilian Babarowski
929071ea1a doc: fix external link in README.md 2022-03-30 22:44:32 -04:00
Shyam Sunder
514b846781 client/js/markdown: fix processing of inline markdown 2022-02-16 09:09:21 -05:00
Shyam Sunder
b2582b7b0f client: update dependencies 2022-02-14 18:31:15 -05:00
noirscape
82541536af Make waitress thread count configurable.
This should fix most scaling problems without needing to start
more server instances. By default, waitress maintains at most
4 threads. This works fine if the database is small (sub 100k posts)
but causes a large Task queue depth to occur if the database is larger.

Letting users increase the amount of threads means that one server instance
is able to handle more requests without locking up the rest of the site.

This adds a new environment variable to .env, THREADS, which can be used to
configure the amount of threads to start and is by default set to 4
(the default amount used by waitress).
2022-02-14 17:33:23 -05:00
dependabot[bot]
8ad9457b24
build(deps): bump path-parse from 1.0.6 to 1.0.7 in /client
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-08 15:00:00 +00:00
Shyam Sunder
6de0a74257 server/config: fix deprecated database string format 2022-02-08 09:58:56 -05:00
Shyam Sunder
a22485afda server/func/images: upgrade to heif-image-plugin 2022-02-08 09:58:33 -05:00
dependabot[bot]
e2419a30ba
build(deps): bump cached-path-relative from 1.0.2 to 1.1.0 in /client
Bumps [cached-path-relative](https://github.com/ashaffer/cached-path-relative) from 1.0.2 to 1.1.0.
- [Release notes](https://github.com/ashaffer/cached-path-relative/releases)
- [Commits](https://github.com/ashaffer/cached-path-relative/commits)

---
updated-dependencies:
- dependency-name: cached-path-relative
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-27 14:29:29 +00:00
neobooru
d5a6609f75 client: remove URL rewriting from the markdown handler 2022-01-26 20:29:31 +00:00
Shyam Sunder
106dcc4135 server/func/images: Do not pass file content to ffmpeg stdin 2022-01-16 11:07:46 -05:00
dependabot[bot]
a14ead1842
build(deps): bump marked from 0.7.0 to 4.0.10 in /client
Bumps [marked](https://github.com/markedjs/marked) from 0.7.0 to 4.0.10.
- [Release notes](https://github.com/markedjs/marked/releases)
- [Changelog](https://github.com/markedjs/marked/blob/master/.releaserc.json)
- [Commits](https://github.com/markedjs/marked/compare/v0.7.0...v4.0.10)

---
updated-dependencies:
- dependency-name: marked
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-15 01:05:54 +00:00
Shyam Sunder
780b7dc6fd client/upload: restore option to pause upload chain on error 2021-11-29 20:06:20 -05:00
Shyam Sunder
9f95e9eb90 client: linting 2021-11-29 18:44:20 -05:00
Shyam Sunder
9b3123a815 server: fix python docstring formatting 2021-11-29 18:39:34 -05:00
Shyam Sunder
f3aa0eb801 dev/pre-commit: update versions for pre-commit hooks 2021-11-29 18:34:17 -05:00
Shyam Sunder
98c0941c97 client/docker: Do not pin LTS version of Node
See: https://github.com/npm/cli/wiki/Support-Policy#long-term-support-lts
2021-11-29 18:09:56 -05:00
skybldev
a5fbaae4b3 updated build files
-  is no longer valid as per https://github.com/npm/cli/wiki/Support-Policy#long-term-support-lts
- updated pre-commit config to use latest repos
2021-11-28 10:07:04 -05:00
Shyam Sunder
d699979d35 client+server: cleanup GitHub actions workflow names
Also run unit test action on push
2021-09-23 12:49:32 -04:00
Shyam Sunder
d083084407 server/tests: use transactional db for faster unit tests
* `test_modify_saves_non_empty_diffs` needs non-transactional
  db, so moved to seperate file
* Replaced incompatable usage of `db.session.rollback()`
  with parametrerized function calls
* xfail conditionals for search removed, as we can no longer
  get current driver with binds
* Also remove usage of deprecated `pytest.yield_fixture`
2021-09-23 12:24:56 -04:00
Shyam Sunder
ad9d3599bc server/net: return more useful error messages 2021-09-22 22:08:07 -04:00
Shyam Sunder
c3b81371d8 client+server/docker: fix ARM build platform issue 2021-09-19 12:03:32 -04:00
Shyam Sunder
c64983002e client+server/docker: build ARM images for Docker Hub 2021-09-19 11:39:40 -04:00
Shyam Sunder
4f57f49ebe client+server: migrate to GitHub actions 2021-09-19 11:01:47 -04:00
Shyam Sunder
f58079e12e client/upload: force enable 'upload anonymously' for anon users
Fixes #425
2021-09-13 14:24:07 -04:00
Shyam Sunder
be0c867d25 client/upload: add QoL features for bulk uploads
* Continue uploading remaining posts in an upload list even
when one fails

* Allow option to continue uploading even when similar posts are found

Closes #400
2021-09-13 13:28:34 -04:00
Shyam Sunder
f5338ca508 Fix style 2021-09-13 13:26:57 -04:00
Shyam Sunder
e4a253fd25 client+server: fixed style errors 2021-09-13 13:25:37 -04:00
Ben Klein
414106a477
client/css: dark mode contrast fixes (#388)
* client/css: fix dark mode pagination header bg

* client/css/post-main-view: dark uses box-shadow

* client/css: animate compact-tags updates

* client: tag input animations fixed

* client/css: darken darktheme success bg

* client/css: dark tag background colors

* client/css/tag-input-control: dark suggest header

* client/css: darktheme mobile site-name in nav
2021-07-05 13:24:04 +02:00
neobooru
fa4997fbb9 server: fix issue where no video files could be uploaded 2021-06-07 00:37:30 +02:00
neobooru
3cabe790a7 client/build: update builder image Node.js version to LTS
Fixes #412
The older stylus version throws some warnings in Node.js LTS. The new one doesn't.
2021-06-04 17:12:21 +02:00
neobooru
f497dca92f server: update docker image base to alpine:3.13
We do this so that we don't have to use 'edge' packages, which aren't (always) ABI compatible
2021-06-01 18:20:51 +02:00
neobooru
5ea9e27e48 Merge branch 'avif'
Merges PR #399
2021-06-01 16:57:29 +02:00
dependabot[bot]
027e83a7e7 build(deps-dev): bump underscore from 1.9.1 to 1.12.1 in /client
Bumps [underscore](https://github.com/jashkenas/underscore) from 1.9.1 to 1.12.1.
- [Release notes](https://github.com/jashkenas/underscore/releases)
- [Commits](https://github.com/jashkenas/underscore/compare/1.9.1...1.12.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-14 19:10:19 +00:00
neobooru
dc46ed7929
Merge pull request #404 from Ruin0x11/bmp-support
Support BMP format uploads
2021-05-14 14:43:37 +00:00
Ruin0x11
a6886ddb89
Improve compilation speed for development builds (#402)
* Improve incremental build times
* Live-reloading in development mode
2021-05-14 14:39:40 +00:00
Ruin0x11
a2b68925ac Support BMP format uploads 2021-05-09 01:29:36 -07:00
Ruin0x11
516b3a51a7 Option to always upload similar posts instead of confirming every time 2021-05-07 23:24:38 -07:00
Ruin0x11
f4ca435657 If one post fails to upload, don't prevent the rest from uploading 2021-05-07 23:02:59 -07:00
Ruin0x11
2949431d9a Add libheif/libavif to Dockerfile dependencies 2021-05-07 22:25:59 -07:00
Ruin0x11
1be2d95bb1 Add HEIF formats to allowed extensions text 2021-05-07 21:37:21 -07:00
Ruin0x11
7e27df835c Add AVIF/HEIF/HEIC upload support 2021-05-07 21:20:42 -07:00
Ruin0x11
169593ea36 Add AVIF/HEIC detection
ffmpeg doesn't support HEIC decoding yet...
2021-05-07 14:36:58 -07:00
Shyam Sunder
ca77149597 client: escape periods in tag names
Merges PR #390
2021-04-22 13:43:21 -04:00
nothink (Satoshi Ishii)
535aa0d8fe
Suppressed the use of SQLAlchemy 1.4 2021-04-20 22:52:29 +09:00
neobooru
4ce72fa712 client/tags: escape dots in search term and don't allow '.' and '..' as tags 2021-04-12 10:42:58 +02:00
neobooru
7c37734fec client: rename escapeColons to escapeTagName and also escape dots 2021-04-10 15:10:39 +02:00
Shyam Sunder
545b5828b5 server/func/mime: support ftypM4V file signature 2021-03-30 09:52:49 -04:00
dependabot[bot]
7b54551b8e
build(deps): bump elliptic from 6.5.3 to 6.5.4 in /client
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.3...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 16:49:06 +00:00
Shyam Sunder
8fa84abdc4 client/posts: provide link for danbooru image search 2021-01-08 11:03:38 -05:00
Shyam Sunder
b9451bef4a client/posts/edit: maintain post editing state for arrow key nav
Fixes #373
2021-01-08 10:21:56 -05:00
Shyam Sunder
2b9a4ab786 server/net: prevent youtube-dl errors when downloading image links 2021-01-07 08:28:22 -05:00
Shyam Sunder
c732e62844 server/net: fix error handling 2021-01-06 10:37:59 -05:00
Shyam Sunder
c7461c7f65 server/net: improve youtube-dl functionality, enforce size limits 2021-01-05 17:05:57 -05:00
Shyam Sunder
2dfd1c2192 server/search: add MD5-based search 2021-01-05 13:51:39 -05:00
Shyam Sunder
2bdb072296 server/posts: store and provide MD5 checksums 2021-01-05 13:20:01 -05:00
dependabot[bot]
7515b8e605 build(deps): bump dompurify from 2.0.11 to 2.0.17 in /client
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.0.11 to 2.0.17.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.0.11...2.0.17)

Signed-off-by: dependabot[bot] <support@github.com>
2020-12-19 16:34:36 -05:00
Shyam Sunder
b3dbf1f0c6 doc/install (WIP): new install script 2020-12-19 16:32:46 -05:00
Shyam Sunder
bc69505382 doc/install: applied formatting fixes to script 2020-12-19 16:32:29 -05:00
ShockDW
05823e5dec
Update install.sh 2020-12-13 15:05:55 -06:00
ShockDW
4a23b615fc
Update install.sh - added Daemon access check
Added Docker daemon access check, updated output with colorized ERROR, OK, and NOTICE flags for readability.
2020-12-13 15:04:02 -06:00
ShockDW
e1390633ff
Add files via upload 2020-12-06 02:56:39 -06:00
ShockDW
ed9b6c1f48
Add files via upload
Updated install.sh to remove logic relevant to deprecated elastisearch dependency, unnecessary root check;  various other fixes.
2020-12-06 00:39:59 -06:00
Shyam Sunder
58678b4504 server/func/mailer: Attempt to manually start TLS for SMTP
Fixes #365
2020-12-02 14:01:43 -05:00
ShockDW
cdb3b7dddc add install.sh to automate deployment 2020-12-02 00:21:50 -06:00
ShockDW
ec0e9f29c7 add install.sh to automate deployment 2020-12-01 22:49:37 -06:00
Shyam Sunder
5945271166 client/css: generate transparency grid via pure CSS 2020-10-12 16:07:49 -04:00
Shyam Sunder
a302b2c4a4 server: enable large file support in database 2020-10-11 12:50:21 -04:00
Shyam Sunder
143f633eaa server/func/webhooks: call webhooks asynchronously 2020-10-06 11:55:09 -04:00
Shyam Sunder
eaa6107a6c client/posts: support content aware post flow option 2020-09-27 20:11:56 -04:00
Shyam Sunder
afe4c5c847 client/tag-categories: sort by order on tag-category edit page 2020-09-25 00:02:12 -04:00
Shyam Sunder
697bd45420 server/tag-categories: sort responses by order 2020-09-24 22:50:28 -04:00
Shyam Sunder
a896c1a5a7 client+server/tag-categories: add ordering feature 2020-09-24 13:47:39 -04:00
Shyam Sunder
d4f72de8c2 server/tests: fix failing tests 2020-09-24 19:09:54 +02:00
neobooru
b5d2e447fc docs: update tag category api 2020-09-23 13:49:20 +02:00
neobooru
d2b6ecef4d server+client: update tag category api + fix formatting 2020-09-23 13:48:47 +02:00
neobooru
368372e36d server/tests: fix failing tests 2020-09-20 12:07:42 +02:00
neobooru
06ad8b1882 client+server: add tag category ordering feature
Fixes  #209
2020-09-19 22:55:17 +02:00
Shyam Sunder
1ef0419dc2 server/pools: serialize pools as micro resource within post resources
Fixes #348
2020-09-19 10:29:09 -04:00
neobooru
802051399f doc/api: add pools to post resource 2020-09-18 18:27:21 +02:00
Shyam Sunder
0dd427755b client+server: fix linter issues due to updated pre-commit hooks 2020-09-01 14:07:39 -04:00
Shyam Sunder
67a5dd7c18 dev/pre-commit: add additional checks
- Expand scope of python autoformatting
- Check for mixed line endings
- Enforce no tabs for indentation
2020-09-01 14:07:39 -04:00
Shyam Sunder
4ab6aa5c85 doc/install: fix typo 2020-09-01 11:06:59 -04:00
Shyam Sunder
f5111483af client/html/help: fix typo 2020-08-28 14:59:33 -04:00
Shyam Sunder
e656a3c46a server/docker: unify test and main Dockerfiles 2020-08-28 14:43:10 -04:00
Shyam Sunder
c004eb36c2 client/css: implement dark theme option 2020-08-26 13:19:56 -04:00
Shyam Sunder
1bbcaf11f7 client/posts: add tag implications when autocompleting mass tag inputs
Closes #334. This solution should function similar to single post
tagging. Implications are automatically added but this also allows
for them to review and manually remove any unwanted implications.
2020-08-23 13:11:19 -04:00
Shyam Sunder
3e69edc117 dev/pre-commit: move pytest hook to 'push' stage 2020-08-22 22:08:52 -04:00
Shyam Sunder
74c97efdef client/search: fix autocomplete for composite queries
Fixes #342
2020-08-22 10:17:59 -04:00
Shyam Sunder
4595f9a2aa server: API support for webhooks
Closes #339
2020-08-13 22:41:43 -04:00
Shyam Sunder
b74492974d doc/developer-utils: added helper script for easily creating szurubooru migrations 2020-08-13 12:38:43 -04:00
dependabot[bot]
3edc07b7f8 client/build: bump elliptic from 6.4.0 to 6.5.3
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.0 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.0...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-13 11:53:04 -04:00
dependabot[bot]
9189842524 client/build: bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-16 14:38:53 -04:00
Ben Klein
800a79f95f client/css/snapshot-list-view: use alpha for dark
using alpha and an is-dark check to support dark color schemes in the
history page
2020-07-08 17:45:21 -04:00
Shyam Sunder
13e2888ae4 client/js/views: fix pool links for deleted pools
Fixes #333
2020-07-08 17:28:20 -04:00
Shyam Sunder
b037ce80c3 client/css: make add/remove button for mass tag larger
Fixes #322
2020-06-24 22:37:40 -04:00
Shyam Sunder
0137cf383a client/markdown: use DOMPurify over marked.js sanitizer
See markedjs/marked#1232
2020-06-23 13:24:59 -04:00
Shyam Sunder
342ca9ccba client/build: fix npm audit 2020-06-23 12:58:44 -04:00
Shyam Sunder
d420609f97 client/pools: inherit option to show underscores as spaces 2020-06-23 12:36:26 -04:00
Shyam Sunder
029c112011 client/html: fix upload error when pool input is disabled 2020-06-22 16:44:41 -04:00
Shyam Sunder
b8c5b27195 client/html: hide 'pools' in navbar if user doesn't have privileges 2020-06-22 15:47:57 -04:00
Shyam Sunder
018e3df31d client/html: fixed pool summary view 2020-06-22 12:48:54 -04:00
Shyam Sunder
57193b5715 client+server: implement code autoformatting using prettier and black 2020-06-06 08:58:23 -04:00
Shyam Sunder
c06aaa63af dev: add pre-commit hooks for pytest and docker building 2020-06-05 12:47:23 -04:00
Shyam Sunder
454685755b dev: added pre-commit hooks for code style consistency
See #325
2020-06-05 11:10:05 -04:00
Shyam Sunder
c0d0c4c894 client+server: normalize trailing newlines 2020-06-05 10:54:32 -04:00
Shyam Sunder
4f46619b91 doc: clean up 2020-06-05 10:29:52 -04:00
Shyam Sunder
e7610db054 client/docker: enforce waitress' max upload limitations on nginx proxy
This ensures that both NGINX and Waitress are using the same max upload
request body. See #327
2020-06-05 10:07:55 -04:00
Shyam Sunder
ea623449e7 server: format code to flake8 2020-06-05 10:02:18 -04:00
Shyam Sunder
c5358f7f83 client+server: add post pools feature 2020-06-04 21:01:28 -04:00
Shyam Sunder
4329b1620f client/js: format code to ESLint 2020-06-04 19:02:33 -04:00
Shyam Sunder
b0f1b8c230 fix python lint issues 2020-06-03 11:55:50 -04:00
Ruin0x11
1be947e946 PR fixes 2020-06-02 17:43:18 -07:00
Ruin0x11
7bcefeb347 Add pool information to API.md 2020-05-04 19:45:09 -07:00
Ruin0x11
5ca21f9e7f Add pool tests 2020-05-04 19:12:54 -07:00
Ruin0x11
6b8e3f251f Implement pool merging 2020-05-04 15:15:51 -07:00
Ruin0x11
ffba010ae4 Implement updating pools of a post from details sidebar 2020-05-04 14:44:16 -07:00
Ruin0x11
8795279a73 Add pool input box in post details 2020-05-04 02:20:23 -07:00
Ruin0x11
e6bf102bc0 Add list of posts to pools 2020-05-04 00:09:33 -07:00
Ruin0x11
d59ecb8e23 Add pool CRUD operations/pages 2020-05-03 19:53:28 -07:00
404 changed files with 28772 additions and 12263 deletions

5
.gitattributes vendored Normal file
View 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
View 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
View 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
View file

@ -13,3 +13,6 @@ server/**/lib/
server/**/bin/
server/**/pyvenv.cfg
__pycache__/
data/
sql/

62
.pre-commit-config.yaml Normal file
View file

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

View file

@ -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))
@ -20,6 +20,7 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
- Tag suggestions
- Tag implications (adding a tag automatically adds another)
- Tag aliases
- Pools and pool categories
- Duplicate detection
- Post rating and favoriting; comment rating
- Polished UI
@ -31,7 +32,8 @@ scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
It is recommended that you use Docker for deployment.
[See installation instructions.](doc/INSTALL.md)
Users who wish to avoid using Docker may find the [old installation instructions](doc/LEGACY_INSTALL.md) helpful.
More installation resources, as well as related projects can be found on the
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
## Screenshots

12
client/.eslintrc.yml Normal file
View file

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

4
client/.prettierrc.yml Normal file
View file

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

View file

@ -1,4 +1,4 @@
FROM node:9 as builder
FROM --platform=$BUILDPLATFORM node:lts as builder
WORKDIR /opt/app
COPY package.json package-lock.json ./
@ -11,7 +11,7 @@ ARG CLIENT_BUILD_ARGS=""
RUN BASE_URL="__BASEURL__" node build.js --gzip ${CLIENT_BUILD_ARGS}
FROM scratch as approot
FROM --platform=$BUILDPLATFORM scratch as approot
COPY docker-start.sh /
@ -22,7 +22,7 @@ WORKDIR /var/www
COPY --from=builder /opt/app/public/ .
FROM nginx:alpine
FROM nginx:alpine as release
RUN apk --no-cache add dumb-init
COPY --from=approot / /

View file

@ -4,28 +4,30 @@
// -------------------------------------------------
const webapp_icons = [
{name: 'android-chrome-192x192.png', size: 192},
{name: 'android-chrome-512x512.png', size: 512},
{name: 'apple-touch-icon.png', size: 180},
{name: 'mstile-150x150.png', size: 150}
{ name: 'android-chrome-192x192.png', size: 192 },
{ name: 'android-chrome-512x512.png', size: 512 },
{ name: 'apple-touch-icon.png', size: 180 },
{ name: 'mstile-150x150.png', size: 150 }
];
const webapp_splash_screens = [
{w: 640, h: 1136, center: 320},
{w: 750, h: 1294, center: 375},
{w: 1125, h: 2436, center: 565},
{w: 1242, h: 2148, center: 625},
{w: 1536, h: 2048, center: 770},
{w: 1668, h: 2224, center: 820},
{w: 2048, h: 2732, center: 1024}
{ w: 640, h: 1136, center: 320 },
{ w: 750, h: 1294, center: 375 },
{ w: 1125, h: 2436, center: 565 },
{ w: 1242, h: 2148, center: 625 },
{ w: 1536, h: 2048, center: 770 },
{ w: 1668, h: 2224, center: 820 },
{ w: 2048, h: 2732, center: 1024 }
];
const external_js = [
'underscore',
'superagent',
'mousetrap',
'dompurify',
'js-cookie',
'marked',
'mousetrap',
'nprogress',
'superagent',
'underscore',
];
const app_manifest = {
@ -35,7 +37,7 @@ const app_manifest = {
src: baseUrl() + 'img/android-chrome-192x192.png',
type: 'image/png',
sizes: '192x192'
},
},
{
src: baseUrl() + 'img/android-chrome-512x512.png',
type: 'image/png',
@ -55,6 +57,11 @@ const glob = require('glob');
const path = require('path');
const util = require('util');
const execSync = require('child_process').execSync;
const browserify = require('browserify');
const chokidar = require('chokidar');
const WebSocket = require('ws');
var PrettyError = require('pretty-error');
var pe = new PrettyError();
function readTextFile(path) {
return fs.readFileSync(path, 'utf-8');
@ -110,7 +117,7 @@ function bundleHtml() {
(match, number) => { return placeholders[number]; });
const functionText = underscore.template(
templateText, {variable: 'ctx'}).source;
templateText, { variable: 'ctx' }).source;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
}
@ -129,7 +136,7 @@ function bundleCss() {
let css = '';
for (const file of glob.sync('./css/**/*.styl')) {
css += stylus.render(readTextFile(file), {filename: file});
css += stylus.render(readTextFile(file), { filename: file });
}
fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
if (process.argv.includes('--gzip')) {
@ -146,59 +153,69 @@ function bundleCss() {
console.info('Bundled CSS');
}
function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), { compress: { unused: false } }).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
function bundleVendorJs(compress) {
let b = browserify();
for (let lib of external_js) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled vendor JS');
});
}
function bundleAppJs(b, compress, callback) {
const file = './public/js/app.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled app JS');
callback();
});
}
function bundleJs() {
const browserify = require('browserify');
function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), {compress: {unused: false}}).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
}
callback();
});
}
if (!process.argv.includes('--no-vendor-js')) {
let b = browserify();
for (let lib of external_js) {
b.require(lib);
}
if (!process.argv.includes('--no-transpile')) {
b.add(require.resolve('babel-polyfill'));
}
const file = './public/js/vendor.min.js';
writeJsBundle(b, file, true, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled vendor JS');
});
bundleVendorJs(true);
}
if (!process.argv.includes('--no-app-js')) {
let b = browserify({debug: process.argv.includes('--debug')});
let watchify = require('watchify');
let b = browserify({ debug: process.argv.includes('--debug') });
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = !process.argv.includes('--debug');
const file = './public/js/app.min.js';
writeJsBundle(b, file, compress, () => {
if (process.argv.includes('--gzip')) {
gzipFile(file);
}
console.info('Bundled app JS');
});
bundleAppJs(b, compress, () => { });
}
}
const environment = process.argv.includes('--watch') ? "development" : "production";
function bundleConfig() {
function getVersion() {
let build_info = process.env.BUILD_INFO;
@ -214,9 +231,10 @@ function bundleConfig() {
}
const config = {
meta: {
version: getVersion(),
buildDate: new Date().toUTCString()
}
version: getVersion(),
buildDate: new Date().toUTCString()
},
environment: environment
};
fs.writeFileSync('./js/.config.autogen.json', JSON.stringify(config));
@ -225,7 +243,6 @@ function bundleConfig() {
function bundleBinaryAssets() {
fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
fs.copyFileSync('./img/transparency_grid.png', './public/img/transparency_grid.png');
console.info('Copied images');
fs.copyFileSync('./fonts/open_sans.woff2', './public/fonts/open_sans.woff2')
@ -254,31 +271,31 @@ function bundleWebAppFiles() {
Promise.all(webapp_icons.map(icon => {
return Jimp.read('./img/app.png')
.then(file => {
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name));
});
.then(file => {
file.resize(icon.size, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.write(path.join('./public/img/', icon.name));
});
}))
.then(() => {
console.info('Generated webapp icons');
});
.then(() => {
console.info('Generated webapp icons');
});
Promise.all(webapp_splash_screens.map(dim => {
return Jimp.read('./img/splash.png')
.then(file => {
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF)
.contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/',
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
});
.then(file => {
file.resize(dim.center, Jimp.AUTO, Jimp.RESIZE_BEZIER)
.background(0xFFFFFFFF)
.contain(dim.w, dim.center,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.contain(dim.w, dim.h,
Jimp.HORIZONTAL_ALIGN_CENTER | Jimp.VERTICAL_ALIGN_MIDDLE)
.write(path.join('./public/img/',
'apple-touch-startup-image-' + dim.w + 'x' + dim.h + '.png'));
});
}))
.then(() => {
console.info('Generated splash screens');
});
.then(() => {
console.info('Generated splash screens');
});
}
function makeOutputDirs() {
@ -297,18 +314,111 @@ function makeOutputDirs() {
}
}
function watch() {
let wss = new WebSocket.Server({ port: 8080 });
const liveReload = !process.argv.includes('--no-live-reload');
function emitReload() {
if (liveReload) {
console.log("Requesting live reload.")
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send("reload");
}
});
}
}
chokidar.watch('./fonts/**/*').on('change', () => {
try {
bundleBinaryAssets();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./img/**/*').on('change', () => {
try {
bundleWebAppFiles();
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./html/**/*.tpl').on('change', () => {
try {
bundleHtml();
} catch (e) {
console.error(pe.render(e));
}
});
chokidar.watch('./css/**/*.styl').on('change', () => {
try {
bundleCss()
emitReload();
} catch (e) {
console.error(pe.render(e));
}
});
bundleBinaryAssets();
bundleWebAppFiles();
bundleCss();
bundleHtml();
bundleVendorJs(true);
let watchify = require('watchify');
let b = browserify({
debug: process.argv.includes('--debug'),
entries: ['js/main.js'],
cache: {},
packageCache: {},
});
b.plugin(watchify);
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = false;
function bundle(id) {
console.info("Rebundling app JS...");
let start = new Date();
bundleAppJs(b, compress, () => {
let end = new Date() - start;
console.info('Rebundled in %ds.', end / 1000)
emitReload();
});
}
b.on('update', bundle);
bundle();
}
// -------------------------------------------------
console.log("Building for '" + environment + "' environment.");
makeOutputDirs();
bundleConfig();
bundleBinaryAssets();
bundleWebAppFiles();
if (!process.argv.includes('--no-html')) {
bundleHtml();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
if (process.argv.includes('--watch')) {
watch();
} else {
if (!process.argv.includes('--no-binary-assets')) {
bundleBinaryAssets();
}
if (!process.argv.includes('--no-web-app-files')) {
bundleWebAppFiles();
}
if (!process.argv.includes('--no-html')) {
bundleHtml();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
}
}

View file

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

View file

@ -1,5 +1,7 @@
@import colors
$comment-header-background-color = $top-navigation-color
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
$comment-border-color = #DDD
.comment-container
@ -81,7 +83,7 @@ $comment-border-color = #DDD
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-tab-text-color)
color: mix($main-color, $inactive-link-color)
i
margin-right: 0.3em
@ -112,8 +114,23 @@ $comment-border-color = #DDD
.messages
margin: 1em 0
.darktheme .comment-container .comment header
background: $comment-header-background-color-darktheme
nav.edit
ul
li
&.active
background: $window-color-darktheme
border-bottom: 1px solid $window-color-darktheme
.edit, .delete, .score-container a, .nickname a
&:not(.inactive)
color: mix($main-color, $inactive-link-color-darktheme)
.comment-content
p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside
margin: 1em 0

View file

@ -1,5 +1,6 @@
@import colors
$comment-border-color = $top-navigation-color
$comment-border-color-darktheme = $top-navigation-color-darktheme
.global-comment-list
text-align: left
@ -46,3 +47,8 @@ $comment-border-color = $top-navigation-color
.comments-container
width: 100%
.darktheme .global-comment-list
&>ul
&>li
border-top: 3px solid $comment-border-color-darktheme

View file

@ -26,6 +26,10 @@ form:not(.horizontal)
font-size: 80%
line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal
display: inline-block
margin-bottom: 1em
@ -167,6 +171,16 @@ input[type=time]
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
input[type=date],
input[type=time]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
/*
@ -204,6 +218,21 @@ input[type=number]
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
select,
textarea,
input[type=text],
input[type=email],
input[type=password],
input[type=number]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
input[readonly],
input[readonly]+.radio,
input[readonly]+.checkbox,
@ -242,8 +271,9 @@ form.show-validation .input
outline: 0
border: 2px solid $input-good-border-color
background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/*
* Buttons
@ -310,6 +340,10 @@ input::-moz-focus-inner
button
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper
cursor: default
opacity: .5
@ -319,8 +353,6 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active
border-color: $main-color
.autocomplete
position: absolute
z-index: 10
@ -345,6 +377,10 @@ input[type=file]:focus+.file-dropper,
.disabled
color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete
display: none

View file

@ -1,6 +1,11 @@
@import colors
@import mixins
$active-tab-text-color = $text-color
$active-tab-text-color-darktheme = $text-color-darktheme
$inactive-tab-text-color = $inactive-link-color
$inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */
@font-face
font-family: 'Open Sans';
@ -28,6 +33,10 @@ body
@media (max-width: 1200px)
font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3
font-weight: normal
margin-bottom: 1em
@ -62,6 +71,11 @@ a
.vim-nav-hint
position: absolute
visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append
margin-left: 1em
@ -102,12 +116,19 @@ form .fa-question-circle-o
>*:last-child
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
hr
border: 0
border-top: 1px solid $line-color
margin: 1em 0
padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav
ul
list-style-type: none
@ -205,6 +226,24 @@ nav
@media (max-width: 1000px)
display: none
.darktheme nav
&.buttons
ul
li:not(.active) a
color: $inactive-tab-text-color-darktheme
li:hover:not(.active) a
color: $active-tab-text-color-darktheme
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
@ -229,6 +268,18 @@ a .access-key
border: 1px solid $message-success-border-color
background: $message-success-background-color
.darktheme .messages
.message
&.info
border: 1px solid darken($message-info-border-color, 30%)
background: darken($message-info-background-color, 60%)
&.error
border: 1px solid darken($message-error-border-color, 30%)
background: darken($message-error-background-color, 60%)
&.success
border: 1px solid darken($message-success-border-color, 30%)
background: darken($message-success-background-color, 80%)
.thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle
@ -239,9 +290,14 @@ a .access-key
width: 20px
height: 20px
&.empty
background-image: url('../img/transparency_grid.png')
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
background-repeat: repeat
background-size: initial
background-size: 20px 20px
img
opacity: 0
width: 100%

View file

@ -22,3 +22,11 @@
line-height: 2em
.expander-content
padding: 0.5em 0.5em 2em 0.5em
.darktheme .expander
header
background: $active-tab-background-color-darktheme
a
color: mix($text-color-darktheme, $inactive-link-color-darktheme)
i
color: $inactive-link-color-darktheme

View file

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

View file

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

View file

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

View file

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

33
client/css/pool-view.styl Normal file
View file

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

View file

@ -1,6 +1,14 @@
@import colors
.post-container
.post-content.transparency-grid img
background: url('../img/transparency_grid.png')
background-image:
linear-gradient(45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(-45deg, $transparency-grid-square-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, $transparency-grid-square-color 75%),
linear-gradient(-45deg, transparent 75%, $transparency-grid-square-color 75%)
background-size: 20px 20px
background-position: 0 0, 0 10px, 10px -10px, -10px 0px
text-align: center
.post-content

View file

@ -70,7 +70,7 @@
height: 1em
text-align: center
line-height: 1em
font-size: 1.6em
font-size: 2.2em
&.tagged
background: rgba(0, 230, 0, 0.7)
&:after
@ -114,12 +114,36 @@
&[data-disabled]
background: rgba(200, 200, 200, 0.7)
.delete-flipper
display: inline-block
padding: 0.5em
box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.delete
background: rgba(255, 0, 0, 0.7)
&:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail
background-position: 50% 30%
width: 100%
height: 100%
outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags
.thumbnail
@ -134,6 +158,22 @@
.thumbnail
outline: 4px solid $main-color !important
.post-flow
ul
li
min-width: inherit
width: inherit
&:not(.flexbox-dummy)
height: 14vw
.thumbnail
outline-offset: -1px
.thumbnail-wrapper.no-tags
.thumbnail
outline: 2px solid $post-thumbnail-no-tags-border-color
&:hover a, a:active, a:focus
.thumbnail
outline: 2px solid $main-color !important
.post-list-header
white-space: nowrap
text-align: left
@ -147,6 +187,9 @@
vertical-align: top
@media (max-width: 1000px)
display: block
&.bulk-edit-tags:not(.opened), &.bulk-edit-safety:not(.opened)
float: left
margin-right: 1em
input
margin-bottom: 0.25em
margin-right: 0.25em
@ -182,7 +225,7 @@
.hint
display: none
input[name=tag]
width: 12em
width: 24em
@media (max-width: 1000px)
display: block
width: 100%
@ -198,7 +241,19 @@
.append
@media (max-width: 1000px)
margin-left: 0
.bulk-edit-delete
&.opened
.start
@media (max-width: 1000px)
margin-left: 0
&:not(.opened)
.start
display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety
margin-right: 0.25em
&.safety-safe

View file

@ -7,47 +7,61 @@
>.sidebar
margin-right: 1em
min-width: 20em
max-width: 20em
min-width: 21em
max-width: 21em
line-height: 160%
a:active
border: 0
outline: 0
nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
article
flex: 1 0 33%
a
display: inline-block
width: 100%
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
>.sidebar>nav.buttons, >.content nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
article
flex: 1 0 33%
a
display: inline-block
width: 100%
padding: 0.3em 0
text-align: center
@media (max-width: 800px)
margin-top: 2em
vertical-align: middle
transition: background 0.2s linear, box-shadow 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 0.6em
margin-bottom: 0.6em
>.content
width: 100%
.post-container
margin-bottom: 2em
margin-bottom: 0.6em
.post-content
margin: 0
.after-mobile-controls
width: 100%
.darktheme .post-view
>.sidebar, >.content
nav.buttons
article
a:not(.inactive):hover
background: unset
box-shadow: inset 0 0 0 0.3em $main-color
@media (max-width: 800px)
.post-view
flex-wrap: wrap
>.after-mobile-controls
order: 3
>.sidebar
order: 2
min-width: 100%
@ -105,7 +119,6 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
li

View file

@ -1,5 +1,6 @@
@import colors
$upload-header-background-color = $top-navigation-color
$upload-header-background-color-darktheme = $top-navigation-color-darktheme
$upload-border-color = #DDD
$cancel-button-color = tomato
@ -12,8 +13,12 @@ $cancel-button-color = tomato
&.inactive input[type=submit],
&.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit],
&.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel
display: none
@ -38,6 +43,12 @@ $cancel-button-color = tomato
.skip-duplicates
margin-left: 1em
.always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
margin-top: 1em
@ -51,6 +62,14 @@ $cancel-button-color = tomato
margin: 0 0 1.2em 0
padding-left: 13em
img
width: 100%
height: 100%
video
width: 100%
height: 100%
&>.thumbnail-wrapper
float: left
width: 12em
@ -149,3 +168,15 @@ $cancel-button-color = tomato
color: $inactive-link-color
&:last-child .move-down
color: $inactive-link-color
.darktheme &:first-child .move-up
color: $inactive-link-color-darktheme
.darktheme &:last-child .move-down
color: $inactive-link-color-darktheme
.darktheme #post-upload .uploadables-container .uploadable-container
.uploadable header
background: $upload-header-background-color-darktheme
&:first-child .move-up
color: $inactive-link-color-darktheme
&:last-child .move-down
color: $inactive-link-color-darktheme

View file

@ -31,16 +31,34 @@ $snapshot-merged-background-color = #FEC
div.operation-created
background: $snapshot-created-background-color
&+.details
background: lighten($snapshot-created-background-color, 50%)
background: alpha(@background, 50%)
div.operation-modified
background: $snapshot-modified-background-color
&+.details
background: lighten($snapshot-modified-background-color, 50%)
background: alpha(@background, 50%)
div.operation-deleted
background: $snapshot-deleted-background-color
&+.details
background: lighten($snapshot-deleted-background-color, 50%)
background: alpha(@background, 50%)
div.operation-merged
background: $snapshot-merged-background-color
&+.details
background: lighten($snapshot-merged-background-color, 50%)
background: alpha(@background, 50%)
.darktheme .snapshot-list ul li
div.operation-created
background: darken($snapshot-created-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-modified
background: darken($snapshot-modified-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-deleted
background: darken($snapshot-deleted-background-color, 80%)
&+.details
background: alpha(@background, 50%)
div.operation-merged
background: darken($snapshot-merged-background-color, 80%)
&+.details
background: alpha(@background, 50%)

View file

@ -27,4 +27,3 @@
display: none
form
width: auto

View file

@ -46,7 +46,7 @@ div.tag-input
.wrapper
margin-left: 0.5em
background: $tag-suggestions-background-color
background: $window-color
border: 1px solid $tag-suggestions-border-color
width: 15em
word-break: break-all
@ -62,7 +62,7 @@ div.tag-input
max-height: 20em
padding: 0.5em 1em 0 1em
li:last-child
border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0)
border-bottom: 0.5em solid alpha($window-color, 0)
li
margin: 0
font-size: 90%
@ -86,6 +86,12 @@ div.tag-input
font-size: 90%
unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags
width: 100%
margin: 0.5em 0 0 0
@ -103,18 +109,30 @@ ul.compact-tags
a:focus
outline: 0
box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication
background: $implied-tag-background-color
color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new
background: $new-tag-background-color
color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i
padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color
@ -125,3 +143,19 @@ div.tag-input, ul.compact-tags
margin-left: 0.7em
.remove-tag
margin-right: 0.5em
.darktheme
div.tag-input .tag-suggestions
.buttons a
color: $inactive-link-color-darktheme
.wrapper
background: $window-color-darktheme
ul li:last-child
border-bottom: 0.5em solid alpha($window-color-darktheme, 0)
p
background: darken($tag-suggestions-header-color, 80%)
.append
color: $inactive-link-color-darktheme
div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color-darktheme

View file

@ -40,6 +40,13 @@
.implications, .suggestions
display: none
.darktheme .tag-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.tag-list-header
label
display: none !important
@ -54,3 +61,7 @@
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -21,10 +21,15 @@
.details
font-size: 90%
line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail
width: 3em
height: 3em
margin: 0.25em 0.6em 0 0
.darktheme .user-list
ul li
background: $top-navigation-color-darktheme
.user-list-header
label
@ -40,3 +45,7 @@
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .user-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -2,10 +2,10 @@
# Integrate environment variables
sed -i "s|__BACKEND__|${BACKEND_HOST}|" \
/etc/nginx/nginx.conf
/etc/nginx/nginx.conf
sed -i "s|__BASEURL__|${BASE_URL:-/}|g" \
/var/www/index.htm \
/var/www/manifest.json
/var/www/index.htm \
/var/www/manifest.json
# Start server
exec nginx
exec nginx

View file

@ -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 .

View file

@ -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

View file

@ -4,6 +4,7 @@
--><li data-name='posts'><a href='<%- ctx.formatClientLink('help', 'search', 'posts') %>'>Posts</a></li><!--
--><li data-name='users'><a href='<%- ctx.formatClientLink('help', 'search', 'users') %>'>Users</a></li><!--
--><li data-name='tags'><a href='<%- ctx.formatClientLink('help', 'search', 'tags') %>'>Tags</a></li><!--
--><li data-name='pools'><a href='<%- ctx.formatClientLink('help', 'search', 'pools') %>'>Pools</li><!--
--></ul><!--
--></nav>

View file

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

View file

@ -20,15 +20,15 @@
</tr>
<tr>
<td><code>uploader</code></td>
<td>uploaded by given use (accepts wildcards)r</td>
<td>uploaded by given user (accepts wildcards)</td>
</tr>
<tr>
<td><code>upload</code></td>
<td>alias of <code>upload</code></td>
<td>alias of <code>uploader</code></td>
</tr>
<tr>
<td><code>submit</code></td>
<td>alias of <code>upload</code></td>
<td>alias of <code>uploader</code></td>
</tr>
<tr>
<td><code>comment</code></td>
@ -42,6 +42,10 @@
<td><code>source</code></td>
<td>having given source URL (accepts wildcards)</td>
</tr>
<tr>
<td><code>pool</code></td>
<td>belonging to the pool with the given ID</td>
</tr>
<tr>
<td><code>tag-count</code></td>
<td>having given number of tags</td>
@ -79,9 +83,17 @@
<td>having given flag. <code>&lt;value&gt;</code> can be either <code>loop</code> or <code>sound</code>.</td>
</tr>
<tr>
<td><code>content-checksum</code></td>
<td><code>sha1</code></td>
<td>having given SHA1 checksum</td>
</tr>
<tr>
<td><code>md5</code></td>
<td>having given MD5 checksum</td>
</tr>
<tr>
<td><code>content-checksum</code></td>
<td>alias of <code>sha1</code></td>
</tr>
<tr>
<td><code>file-size</code></td>
<td>having given file size (in bytes)</td>

View file

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

View file

@ -2,7 +2,7 @@
<html>
<head>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>

18
client/html/pool.tpl Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

50
client/html/pool_edit.tpl Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,6 +73,12 @@
</section>
<% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPostNotes) { %>
<section class='notes'>
<a href class='add'>Add a note</a>

View file

@ -29,6 +29,7 @@
<span class='vim-nav-hint'>Next post &gt;</span>
</a>
</article>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<article class='edit-post'>
<% if (ctx.editMode) { %>
<a href='<%= ctx.getPostUrl(ctx.post.id, ctx.parameters) %>'>
@ -36,16 +37,13 @@
<span class='vim-nav-hint'>Back to view mode</span>
</a>
<% } else { %>
<% if (ctx.canEditPosts || ctx.canDeletePosts || ctx.canFeaturePosts) { %>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<% } else { %>
<a class='inactive'>
<% } %>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
<a href='<%= ctx.getPostEditUrl(ctx.post.id, ctx.parameters) %>'>
<i class='fa fa-pencil'></i>
<span class='vim-nav-hint'>Edit post</span>
</a>
<% } %>
</article>
<% } %>
</nav>
<div class='sidebar-container'></div>
@ -54,13 +52,16 @@
<div class='content'>
<div class='post-container'></div>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
<div class='after-mobile-controls'>
<div class='description'></div>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canCreateComments) { %>
<h2>Add comment</h2>
<div class='comment-form-container'></div>
<% } %>
<% if (ctx.canListComments) { %>
<div class='comments-container'></div>
<% } %>
</div>
</div>
</div>

View file

@ -36,8 +36,13 @@
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] +
' (' +

View file

@ -9,11 +9,16 @@
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %>
</a>
}[ctx.post.mimeType] %><!--
--></a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
@ -52,7 +57,8 @@
<section class='search'>
Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -91,12 +97,12 @@
--></a><!--
--><% } %><!--
--><% if (ctx.canListPosts) { %><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!--
--><%- ctx.getPrettyTagName(tag.names[0]) %>&#32;<!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--

View file

@ -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>

View file

@ -61,6 +61,7 @@
text: 'Upload anonymously',
name: 'anonymous',
checked: ctx.uploadable.anonymous,
readonly: ctx.uploadable.forceAnonymous,
}) %>
</div>
<% } %>

View file

@ -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>

View file

@ -1,4 +1,4 @@
<div class='post-list'>
<% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %>
<% if (ctx.response.results.length) { %>
<ul>
<% for (let post of ctx.response.results) { %>
@ -50,6 +50,10 @@
<% } %>
</span>
<% } %>
<% if (ctx.canBulkDelete && ctx.parameters && ctx.parameters.delete) { %>
<a href class='delete-flipper'>
</a>
<% } %>
</span>
</li>
<% } %>

View file

@ -22,6 +22,15 @@
}) %>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Use dark theme',
name: 'dark-theme',
checked: ctx.browsingSettings.darkTheme,
}) %>
<p class='hint'>Changing this setting will require you to refresh the page for it to apply.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Upscale small posts',
@ -38,6 +47,15 @@
<p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Use post flow',
name: 'post-flow',
checked: ctx.browsingSettings.postFlow,
}) %>
<p class='hint'>Use a content-aware flow for thumbnails on the post search page.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Enable transparency grid',
@ -66,8 +84,8 @@
<li>
<%= ctx.makeCheckbox({
text: 'Display underscores as spaces in tags',
name: 'tag-underscores-as-spaces',
text: 'Display underscores as spaces',
name: 'underscores-as-spaces',
checked: ctx.browsingSettings.tagUnderscoresAsSpaces,
}) %>
<p class='hint'>Display all underscores as if they were spaces. This is only a visual change, which means that you'll still have to use underscores when searching or editing tags.</p>

View file

@ -1,5 +1,5 @@
<div class='content-wrapper' id='tag'>
<h1><%- ctx.getPrettyTagName(ctx.tag.names[0]) %></h1>
<h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1>
<nav class='buttons'><!--
--><ul><!--
--><li data-name='summary'><a href='<%- ctx.formatClientLink('tag', ctx.tag.names[0]) %>'>Summary</a></li><!--

View file

@ -7,6 +7,7 @@
<tr>
<th class='name'>Category name</th>
<th class='color'>CSS color</th>
<th class='order'>Order</th>
<th class='usages'>Usages</th>
</tr>
</thead>
@ -21,7 +22,7 @@
<div class='messages'></div>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canDelete) { %>
<% if (ctx.canCreate || ctx.canEditName || ctx.canEditColor || ctx.canEditOrder || ctx.canDelete) { %>
<div class='buttons'>
<input type='submit' class='save' value='Save changes'>
</div>

View file

@ -17,6 +17,13 @@
<%- ctx.tagCategory.color %>
<% } %>
</td>
<td class='order'>
<% if (ctx.canEditOrder) { %>
<%= ctx.makeNumericInput({value: ctx.tagCategory.order}) %>
<% } else { %>
<%- ctx.tagCategory.order %>
<% } %>
</td>
<td class='usages'>
<% if (ctx.tagCategory.name) { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>

View file

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

View file

@ -36,6 +36,6 @@
<section class='description'>
<hr/>
<%= ctx.makeMarkdown(ctx.tag.description || 'This tag has no description yet.') %>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeColons(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
<p>This tag has <a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(ctx.tag.names[0])}) %>'><%- ctx.tag.postCount %> usage(s)</a>.</p>
</section>
</div>

View file

@ -3,35 +3,35 @@
<table>
<thead>
<th class='names'>
<% if (ctx.query == 'sort:name' || !ctx.query) { %>
<% if (ctx.parameters.query == 'sort:name' || !ctx.parameters.query) { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:name'}) %>'>Tag name(s)</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:name'}) %>'>Tag name(s)</a>
<% } %>
</th>
<th class='implications'>
<% if (ctx.query == 'sort:implication-count') { %>
<% if (ctx.parameters.query == 'sort:implication-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:implication-count'}) %>'>Implications</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:implication-count'}) %>'>Implications</a>
<% } %>
</th>
<th class='suggestions'>
<% if (ctx.query == 'sort:suggestion-count') { %>
<% if (ctx.parameters.query == 'sort:suggestion-count') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:suggestion-count'}) %>'>Suggestions</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:suggestion-count'}) %>'>Suggestions</a>
<% } %>
</th>
<th class='usages'>
<% if (ctx.query == 'sort:usages') { %>
<% if (ctx.parameters.query == 'sort:usages') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:usages'}) %>'>Usages</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:usages'}) %>'>Usages</a>
<% } %>
</th>
<th class='creation-time'>
<% if (ctx.query == 'sort:creation-time') { %>
<% if (ctx.parameters.query == 'sort:creation-time') { %>
<a href='<%- ctx.formatClientLink('tags', {query: '-sort:creation-time'}) %>'>Created on</a>
<% } else { %>
<a href='<%- ctx.formatClientLink('tags', {query: 'sort:creation-time'}) %>'>Created on</a>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 B

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const cookies = require('js-cookie');
const request = require('superagent');
const events = require('./events.js');
const progress = require('./util/progress.js');
const uri = require('./util/uri.js');
const cookies = require("js-cookie");
const request = require("superagent");
const events = require("./events.js");
const progress = require("./util/progress.js");
const uri = require("./util/uri.js");
let fileTokens = {};
let remoteConfig = null;
@ -18,22 +18,22 @@ class Api extends events.EventTarget {
this.token = null;
this.cache = {};
this.allRanks = [
'anonymous',
'restricted',
'regular',
'power',
'moderator',
'administrator',
'nobody',
"anonymous",
"restricted",
"regular",
"power",
"moderator",
"administrator",
"nobody",
];
this.rankNames = new Map([
['anonymous', 'Anonymous'],
['restricted', 'Restricted user'],
['regular', 'Regular user'],
['power', 'Power user'],
['moderator', 'Moderator'],
['administrator', 'Administrator'],
['nobody', 'Nobody'],
["anonymous", "Anonymous"],
["restricted", "Restricted user"],
["regular", "Regular user"],
["power", "Power user"],
["moderator", "Moderator"],
["administrator", "Administrator"],
["nobody", "Nobody"],
]);
}
@ -43,11 +43,12 @@ class Api extends events.EventTarget {
resolve(this.cache[url]);
});
}
return this._wrappedRequest(url, request.get, {}, {}, options)
.then(response => {
return this._wrappedRequest(url, request.get, {}, {}, options).then(
(response) => {
this.cache[url] = response;
return Promise.resolve(response);
});
}
);
}
post(url, data, files, options) {
@ -67,10 +68,9 @@ class Api extends events.EventTarget {
fetchConfig() {
if (remoteConfig === null) {
return this.get(uri.formatApiLink('info'))
.then(response => {
remoteConfig = response.config;
});
return this.get(uri.formatApiLink("info")).then((response) => {
remoteConfig = response.config;
});
} else {
return Promise.resolve();
}
@ -84,6 +84,10 @@ class Api extends events.EventTarget {
return remoteConfig.tagNameRegex;
}
getPoolNameRegex() {
return remoteConfig.poolNameRegex;
}
getPasswordRegex() {
return remoteConfig.passwordRegex;
}
@ -111,7 +115,8 @@ class Api extends events.EventTarget {
continue;
}
const rankIndex = this.allRanks.indexOf(
remoteConfig.privileges[p]);
remoteConfig.privileges[p]
);
if (minViableRank === null || rankIndex < minViableRank) {
minViableRank = rankIndex;
}
@ -119,17 +124,16 @@ class Api extends events.EventTarget {
if (minViableRank === null) {
throw `Bad privilege name: ${lookup}`;
}
let myRank = this.user !== null ?
this.allRanks.indexOf(this.user.rank) :
0;
let myRank =
this.user !== null ? this.allRanks.indexOf(this.user.rank) : 0;
return myRank >= minViableRank;
}
loginFromCookies() {
const auth = cookies.getJSON('auth');
return auth && auth.user && auth.token ?
this.loginWithToken(auth.user, auth.token, true) :
Promise.resolve();
const auth = cookies.getJSON("auth");
return auth && auth.user && auth.token
? this.loginWithToken(auth.user, auth.token, true)
: Promise.resolve();
}
loginWithToken(userName, token, doRemember) {
@ -137,63 +141,74 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => {
this.userName = userName;
this.token = token;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
this.get("/user/" + userName + "?bump-login=true").then(
(response) => {
const options = {};
if (doRemember) {
options.expires = 365;
}
cookies.set(
'auth',
{'user': userName, 'token': token},
options);
"auth",
{ user: userName, token: token },
options
);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
}, error => {
this.dispatchEvent(new CustomEvent("login"));
},
(error) => {
reject(error);
this.logout();
});
}
);
});
}
createToken(userName, options) {
let userTokenRequest = {
enabled: true,
note: 'Web Login Token'
note: "Web Login Token",
};
if (typeof options.expires !== 'undefined') {
userTokenRequest.expirationTime = new Date().addDays(options.expires).toISOString()
if (typeof options.expires !== "undefined") {
userTokenRequest.expirationTime = new Date()
.addDays(options.expires)
.toISOString();
}
return new Promise((resolve, reject) => {
this.post('/user-token/' + userName, userTokenRequest)
.then(response => {
this.post("/user-token/" + userName, userTokenRequest).then(
(response) => {
cookies.set(
'auth',
{'user': userName, 'token': response.token},
options);
"auth",
{ user: userName, token: response.token },
options
);
this.userName = userName;
this.token = response.token;
this.userPassword = null;
}, error => {
},
(error) => {
reject(error);
});
}
);
});
}
deleteToken(userName, userToken) {
return new Promise((resolve, reject) => {
this.delete('/user-token/' + userName + '/' + userToken, {})
.then(response => {
this.delete("/user-token/" + userName + "/" + userToken, {}).then(
(response) => {
const options = {};
cookies.set(
'auth',
{'user': userName, 'token': null},
options);
"auth",
{ user: userName, token: null },
options
);
resolve();
}, error => {
},
(error) => {
reject(error);
});
}
);
});
}
@ -202,8 +217,8 @@ class Api extends events.EventTarget {
return new Promise((resolve, reject) => {
this.userName = userName;
this.userPassword = userPassword;
this.get('/user/' + userName + '?bump-login=true')
.then(response => {
this.get("/user/" + userName + "?bump-login=true").then(
(response) => {
const options = {};
if (doRemember) {
options.expires = 365;
@ -211,22 +226,26 @@ class Api extends events.EventTarget {
this.createToken(this.userName, options);
this.user = response;
resolve();
this.dispatchEvent(new CustomEvent('login'));
}, error => {
this.dispatchEvent(new CustomEvent("login"));
},
(error) => {
reject(error);
this.logout();
});
}
);
});
}
logout() {
let self = this;
this.deleteToken(this.userName, this.token)
.then(response => {
this.deleteToken(this.userName, this.token).then(
(response) => {
self._logout();
}, error => {
},
(error) => {
self._logout();
});
}
);
}
_logout() {
@ -234,17 +253,19 @@ class Api extends events.EventTarget {
this.userName = null;
this.userPassword = null;
this.token = null;
this.dispatchEvent(new CustomEvent('logout'));
this.dispatchEvent(new CustomEvent("logout"));
}
forget() {
cookies.remove('auth');
cookies.remove("auth");
}
isLoggedIn(user) {
if (user) {
return this.userName !== null &&
this.userName.toLowerCase() === user.name.toLowerCase();
return (
this.userName !== null &&
this.userName.toLowerCase() === user.name.toLowerCase()
);
} else {
return this.userName !== null;
}
@ -255,8 +276,7 @@ class Api extends events.EventTarget {
}
_getFullUrl(url) {
const fullUrl =
('api/' + url).replace(/([^:])\/+/g, '$1/');
const fullUrl = ("api/" + url).replace(/([^:])\/+/g, "$1/");
const matches = fullUrl.match(/^([^?]*)\??(.*)$/);
const baseUrl = matches[1];
const request = matches[2];
@ -281,7 +301,7 @@ class Api extends events.EventTarget {
const file = files[key];
const fileId = this._getFileId(file);
if (fileTokens[fileId]) {
data[key + 'Token'] = fileTokens[fileId];
data[key + "Token"] = fileTokens[fileId];
} else {
promise = promise
.then(() => {
@ -289,33 +309,40 @@ class Api extends events.EventTarget {
abortFunction = () => uploadPromise.abort();
return uploadPromise;
})
.then(token => {
.then((token) => {
abortFunction = () => {};
fileTokens[fileId] = token;
data[key + 'Token'] = token;
data[key + "Token"] = token;
return Promise.resolve();
});
}
}
}
promise = promise.then(
() => {
promise = promise
.then(() => {
let requestPromise = this._rawRequest(
url, requestFactory, data, {}, options);
url,
requestFactory,
data,
{},
options
);
abortFunction = () => requestPromise.abort();
return requestPromise;
})
.catch(error => {
if (error.response && error.response.name ===
'MissingOrExpiredRequiredFileError') {
.catch((error) => {
if (
error.response &&
error.response.name === "MissingOrExpiredRequiredFileError"
) {
for (let key of Object.keys(files)) {
const file = files[key];
const fileId = this._getFileId(file);
fileTokens[fileId] = null;
}
error.message =
'The uploaded file has expired; ' +
'please resend the form to reupload.';
"The uploaded file has expired; " +
"please resend the form to reupload.";
}
return Promise.reject(error);
});
@ -327,13 +354,17 @@ class Api extends events.EventTarget {
let abortFunction = () => {};
let returnedPromise = new Promise((resolve, reject) => {
let uploadPromise = this._rawRequest(
'uploads', request.post, {}, {content: file}, options);
"uploads",
request.post,
{},
{ content: file },
options
);
abortFunction = () => uploadPromise.abort();
return uploadPromise.then(
response => {
abortFunction = () => {};
return resolve(response.token);
}, reject);
return uploadPromise.then((response) => {
abortFunction = () => {};
return resolve(response.token);
}, reject);
});
returnedPromise.abort = () => abortFunction();
return returnedPromise;
@ -348,7 +379,7 @@ class Api extends events.EventTarget {
let returnedPromise = new Promise((resolve, reject) => {
let req = requestFactory(fullUrl);
req.set('Accept', 'application/json');
req.set("Accept", "application/json");
if (query) {
req.query(query);
@ -358,7 +389,7 @@ class Api extends events.EventTarget {
for (let key of Object.keys(files)) {
const value = files[key];
if (value.constructor === String) {
data[key + 'Url'] = value;
data[key + "Url"] = value;
} else {
req.attach(key, value || new Blob());
}
@ -367,9 +398,9 @@ class Api extends events.EventTarget {
if (data) {
if (files && Object.keys(files).length) {
req.attach('metadata', new Blob([JSON.stringify(data)]));
req.attach("metadata", new Blob([JSON.stringify(data)]));
} else {
req.set('Content-Type', 'application/json');
req.set("Content-Type", "application/json");
req.send(data);
}
}
@ -377,19 +408,29 @@ class Api extends events.EventTarget {
try {
if (this.userName && this.token) {
req.auth = null;
req.set('Authorization', 'Token '
+ new Buffer(this.userName + ":" + this.token).toString('base64'))
// eslint-disable-next-line no-undef
req.set(
"Authorization",
"Token " +
new Buffer(
this.userName + ":" + this.token
).toString("base64")
);
} else if (this.userName && this.userPassword) {
req.auth(
this.userName,
encodeURIComponent(this.userPassword)
.replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode('0x' + p1);
}));
encodeURIComponent(this.userPassword).replace(
/%([0-9A-F]{2})/g,
(match, p1) => {
return String.fromCharCode("0x" + p1);
}
)
);
}
} catch (e) {
reject(
new Error('Authentication error (malformed credentials)'));
new Error("Authentication error (malformed credentials)")
);
}
if (!options.noProgress) {
@ -397,10 +438,11 @@ class Api extends events.EventTarget {
}
abortFunction = () => {
req.abort(); // does *NOT* call the callback passed in .end()
req.abort(); // does *NOT* call the callback passed in .end()
progress.done();
reject(
new Error('The request was aborted due to user cancel.'));
new Error("The request was aborted due to user cancel.")
);
};
req.end((error, response) => {
@ -409,7 +451,8 @@ class Api extends events.EventTarget {
if (error) {
if (response && response.body) {
error = new Error(
response.body.description || 'Unknown error');
response.body.description || "Unknown error"
);
error.response = response.body;
}
reject(error);

View file

@ -1,4 +1,4 @@
'use strict';
"use strict";
const config = require('./.config.autogen.json');
const config = require("./.config.autogen.json");
module.exports = config;

View file

@ -1,36 +1,40 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const tags = require('../tags.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const LoginView = require('../views/login_view.js');
const router = require("../router.js");
const api = require("../api.js");
const tags = require("../tags.js");
const pools = require("../pools.js");
const uri = require("../util/uri.js");
const topNavigation = require("../models/top_navigation.js");
const LoginView = require("../views/login_view.js");
class LoginController {
constructor() {
api.forget();
topNavigation.activate('login');
topNavigation.setTitle('Login');
topNavigation.activate("login");
topNavigation.setTitle("Login");
this._loginView = new LoginView();
this._loginView.addEventListener('submit', e => this._evtLogin(e));
this._loginView.addEventListener("submit", (e) => this._evtLogin(e));
}
_evtLogin(e) {
this._loginView.clearMessages();
this._loginView.disableForm();
api.forget();
api.login(e.detail.name, e.detail.password, e.detail.remember)
.then(() => {
api.login(e.detail.name, e.detail.password, e.detail.remember).then(
() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged in');
ctx.controller.showSuccess("Logged in");
// reload tag category color map, this is required when `tag_categories:list` has a permission other than anonymous
tags.refreshCategoryColorMap();
}, error => {
pools.refreshCategoryColorMap();
},
(error) => {
this._loginView.showError(error.message);
this._loginView.enableForm();
});
}
);
}
}
@ -39,15 +43,15 @@ class LogoutController {
api.forget();
api.logout();
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Logged out');
ctx.controller.showSuccess("Logged out");
}
}
module.exports = router => {
router.enter(['login'], (ctx, next) => {
module.exports = (router) => {
router.enter(["login"], (ctx, next) => {
ctx.controller = new LoginController();
});
router.enter(['logout'], (ctx, next) => {
router.enter(["logout"], (ctx, next) => {
ctx.controller = new LogoutController();
});
};

View file

@ -1,19 +1,19 @@
'use strict';
"use strict";
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const EmptyView = require("../views/empty_view.js");
class BasePostController {
constructor(ctx) {
if (!api.hasPrivilege('posts:view')) {
if (!api.hasPrivilege("posts:view")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.');
this._view.showError("You don't have privileges to view posts.");
return;
}
topNavigation.activate('posts');
topNavigation.setTitle('Post #' + ctx.parameters.id.toString());
topNavigation.activate("posts");
topNavigation.setTitle("Post #" + ctx.parameters.id.toString());
}
}

View file

@ -1,49 +1,55 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const CommentsPageView = require('../views/comments_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const PostList = require("../models/post_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const CommentsPageView = require("../views/comments_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = ['id', 'comments', 'commentCount', 'thumbnailUrl'];
const fields = ["id", "comments", "commentCount", "thumbnailUrl"];
class CommentsController {
constructor(ctx) {
if (!api.hasPrivilege('comments:list')) {
if (!api.hasPrivilege("comments:list")) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to view comments.');
"You don't have privileges to view comments."
);
return;
}
topNavigation.activate('comments');
topNavigation.setTitle('Listing comments');
topNavigation.activate("comments");
topNavigation.setTitle("Listing comments");
this._pageController = new PageController();
this._pageController.run({
parameters: ctx.parameters,
defaultLimit: 10,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('comments', parameters);
const parameters = Object.assign({}, ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("comments", parameters);
},
requestPage: (offset, limit) => {
return PostList.search(
'sort:comment-date comment-count-min:1',
offset, limit, fields);
"sort:comment-date comment-count-min:1",
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canViewPosts: api.hasPrivilege("posts:view"),
});
const view = new CommentsPageView(pageCtx);
view.addEventListener('submit', e => this._evtUpdate(e));
view.addEventListener('score', e => this._evtScore(e));
view.addEventListener('delete', e => this._evtDelete(e));
view.addEventListener("submit", (e) => this._evtUpdate(e));
view.addEventListener("score", (e) => this._evtScore(e));
view.addEventListener("delete", (e) => this._evtDelete(e));
return view;
},
});
@ -52,25 +58,27 @@ class CommentsController {
_evtUpdate(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(error => {
e.detail.target.showError(error.message);
// TODO: enable form
});
e.detail.comment.save().catch((error) => {
e.detail.target.showError(error.message);
// TODO: enable form
});
}
_evtScore(e) {
e.detail.comment.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.comment
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtDelete(e) {
e.detail.comment.delete()
.catch(error => window.alert(error.message));
e.detail.comment
.delete()
.catch((error) => window.alert(error.message));
}
};
}
module.exports = router => {
router.enter(['comments'],
(ctx, next) => { new CommentsController(ctx); });
module.exports = (router) => {
router.enter(["comments"], (ctx, next) => {
new CommentsController(ctx);
});
};

View file

@ -1,24 +1,24 @@
'use strict';
"use strict";
const topNavigation = require('../models/top_navigation.js');
const HelpView = require('../views/help_view.js');
const topNavigation = require("../models/top_navigation.js");
const HelpView = require("../views/help_view.js");
class HelpController {
constructor(section, subsection) {
topNavigation.activate('help');
topNavigation.setTitle('Help');
topNavigation.activate("help");
topNavigation.setTitle("Help");
this._helpView = new HelpView(section, subsection);
}
}
module.exports = router => {
router.enter(['help'], (ctx, next) => {
module.exports = (router) => {
router.enter(["help"], (ctx, next) => {
new HelpController();
});
router.enter(['help', ':section'], (ctx, next) => {
router.enter(["help", ":section"], (ctx, next) => {
new HelpController(ctx.parameters.section);
});
router.enter(['help', ':section', ':subsection'], (ctx, next) => {
router.enter(["help", ":section", ":subsection"], (ctx, next) => {
new HelpController(ctx.parameters.section, ctx.parameters.subsection);
});
};

View file

@ -1,26 +1,27 @@
'use strict';
"use strict";
const api = require('../api.js');
const config = require('../config.js');
const Info = require('../models/info.js');
const topNavigation = require('../models/top_navigation.js');
const HomeView = require('../views/home_view.js');
const api = require("../api.js");
const config = require("../config.js");
const Info = require("../models/info.js");
const topNavigation = require("../models/top_navigation.js");
const HomeView = require("../views/home_view.js");
class HomeController {
constructor() {
topNavigation.activate('home');
topNavigation.setTitle('Home');
topNavigation.activate("home");
topNavigation.setTitle("Home");
this._homeView = new HomeView({
name: api.getName(),
version: config.meta.version,
buildDate: config.meta.buildDate,
canListSnapshots: api.hasPrivilege('snapshots:list'),
canListPosts: api.hasPrivilege('posts:list'),
canListSnapshots: api.hasPrivilege("snapshots:list"),
canListPosts: api.hasPrivilege("posts:list"),
isDevelopmentMode: config.environment == "development",
});
Info.get()
.then(info => {
Info.get().then(
(info) => {
this._homeView.setStats({
diskUsage: info.diskUsage,
postCount: info.postCount,
@ -31,7 +32,8 @@ class HomeController {
featuringTime: info.featuringTime,
});
},
error => this._homeView.showError(error.message));
(error) => this._homeView.showError(error.message)
);
}
showSuccess(message) {
@ -41,9 +43,9 @@ class HomeController {
showError(message) {
this._homeView.showError(message);
}
};
}
module.exports = router => {
module.exports = (router) => {
router.enter([], (ctx, next) => {
ctx.controller = new HomeController();
});

View file

@ -1,17 +1,17 @@
'use strict';
"use strict";
const topNavigation = require('../models/top_navigation.js');
const NotFoundView = require('../views/not_found_view.js');
const topNavigation = require("../models/top_navigation.js");
const NotFoundView = require("../views/not_found_view.js");
class NotFoundController {
constructor(path) {
topNavigation.activate('');
topNavigation.setTitle('Not found');
topNavigation.activate("");
topNavigation.setTitle("Not found");
this._notFoundView = new NotFoundView(path);
}
};
}
module.exports = router => {
module.exports = (router) => {
router.enter(null, (ctx, next) => {
ctx.controller = new NotFoundController(ctx.canonicalPath);
});

View file

@ -1,8 +1,8 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const EndlessPageView = require('../views/endless_page_view.js');
const ManualPageView = require('../views/manual_page_view.js');
const settings = require("../models/settings.js");
const EndlessPageView = require("../views/endless_page_view.js");
const ManualPageView = require("../views/manual_page_view.js");
class PageController {
constructor(ctx) {

View file

@ -1,19 +1,20 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const topNavigation = require('../models/top_navigation.js');
const PasswordResetView = require('../views/password_reset_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const topNavigation = require("../models/top_navigation.js");
const PasswordResetView = require("../views/password_reset_view.js");
class PasswordResetController {
constructor() {
topNavigation.activate('login');
topNavigation.setTitle('Password reminder');
topNavigation.activate("login");
topNavigation.setTitle("Password reminder");
this._passwordResetView = new PasswordResetView();
this._passwordResetView.addEventListener(
'submit', e => this._evtReset(e));
this._passwordResetView.addEventListener("submit", (e) =>
this._evtReset(e)
);
}
_evtReset(e) {
@ -21,15 +22,20 @@ class PasswordResetController {
this._passwordResetView.disableForm();
api.forget();
api.logout();
api.get(uri.formatApiLink('password-reset', e.detail.userNameOrEmail))
.then(() => {
api.get(
uri.formatApiLink("password-reset", e.detail.userNameOrEmail)
).then(
() => {
this._passwordResetView.showSuccess(
'E-mail has been sent. To finish the procedure, ' +
'please click the link it contains.');
}, error => {
"E-mail has been sent. To finish the procedure, " +
"please click the link it contains."
);
},
(error) => {
this._passwordResetView.showError(error.message);
this._passwordResetView.enableForm();
});
}
);
}
}
@ -38,26 +44,30 @@ class PasswordResetFinishController {
api.forget();
api.logout();
let password = null;
api.post(uri.formatApiLink('password-reset', name), {token: token})
.then(response => {
api.post(uri.formatApiLink("password-reset", name), { token: token })
.then((response) => {
password = response.password;
return api.login(name, password, false);
}).then(() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('New password: ' + password);
}, error => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
});
})
.then(
() => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("New password: " + password);
},
(error) => {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showError(error.message);
}
);
}
}
module.exports = router => {
router.enter(['password-reset'], (ctx, next) => {
module.exports = (router) => {
router.enter(["password-reset"], (ctx, next) => {
ctx.controller = new PasswordResetController();
});
router.enter(['password-reset', ':descriptor'], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(':', 2);
router.enter(["password-reset", ":descriptor"], (ctx, next) => {
const [name, token] = ctx.parameters.descriptor.split(":", 2);
ctx.controller = new PasswordResetFinishController(name, token);
});
};

View file

@ -0,0 +1,69 @@
"use strict";
const api = require("../api.js");
const pools = require("../pools.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolCategoriesView = require("../views/pool_categories_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCategoriesController {
constructor() {
if (!api.hasPrivilege("poolCategories:list")) {
this._view = new EmptyView();
this._view.showError(
"You don't have privileges to view pool categories."
);
return;
}
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
PoolCategoryList.get().then(
(response) => {
this._poolCategories = response.results;
this._view = new PoolCategoriesView({
poolCategories: this._poolCategories,
canEditName: api.hasPrivilege("poolCategories:edit:name"),
canEditColor: api.hasPrivilege(
"poolCategories:edit:color"
),
canDelete: api.hasPrivilege("poolCategories:delete"),
canCreate: api.hasPrivilege("poolCategories:create"),
canSetDefault: api.hasPrivilege(
"poolCategories:setDefault"
),
});
this._view.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._poolCategories.save().then(
() => {
pools.refreshCategoryColorMap();
this._view.enableForm();
this._view.showSuccess("Changes saved.");
},
(error) => {
this._view.enableForm();
this._view.showError(error.message);
}
);
}
}
module.exports = (router) => {
router.enter(["pool-categories"], (ctx, next) => {
ctx.controller = new PoolCategoriesController(ctx, next);
});
};

View file

@ -0,0 +1,176 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const Post = require("../models/post.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const PoolView = require("../views/pool_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolController {
constructor(ctx, section) {
if (!api.hasPrivilege("pools:view")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to view pools.");
return;
}
Promise.all([
PoolCategoryList.get(),
Pool.get(ctx.parameters.id),
]).then(
(responses) => {
const [poolCategoriesResponse, pool] = responses;
topNavigation.activate("pools");
topNavigation.setTitle("Pool #" + pool.names[0]);
this._name = ctx.parameters.name;
pool.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolView({
pool: pool,
section: section,
canEditAnything: api.hasPrivilege("pools:edit"),
canEditNames: api.hasPrivilege("pools:edit:names"),
canEditCategory: api.hasPrivilege("pools:edit:category"),
canEditDescription: api.hasPrivilege(
"pools:edit:description"
),
canEditPosts: api.hasPrivilege("pools:edit:posts"),
canMerge: api.hasPrivilege("pools:merge"),
canDelete: api.hasPrivilege("pools:delete"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtChange(e) {
misc.enableExitConfirmation();
}
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._name !== e.detail.pool.names[0]) {
router.replace(
uri.formatClientLink("pool", e.detail.pool.id, section),
null,
false
);
}
}
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined && e.detail.names !== null) {
e.detail.pool.names = e.detail.names;
}
if (e.detail.category !== undefined && e.detail.category !== null) {
e.detail.pool.category = e.detail.category;
}
if (e.detail.description !== undefined && e.detail.description !== null) {
e.detail.pool.description = e.detail.description;
}
if (e.detail.posts !== undefined && e.detail.posts !== null) {
e.detail.pool.posts.clear();
for (let postId of e.detail.posts) {
e.detail.pool.posts.add(
Post.fromResponse({ id: parseInt(postId) })
);
}
}
e.detail.pool.save().then(
() => {
this._view.showSuccess("Pool saved.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.merge(e.detail.targetPoolId, e.detail.addAlias).then(
() => {
this._view.showSuccess("Pool merged.");
this._view.enableForm();
router.replace(
uri.formatClientLink(
"pool",
e.detail.targetPoolId,
"merge"
),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool deleted.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = (router) => {
router.enter(["pool", ":id", "edit"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "edit");
});
router.enter(["pool", ":id", "merge"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "merge");
});
router.enter(["pool", ":id", "delete"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "delete");
});
router.enter(["pool", ":id"], (ctx, next) => {
ctx.controller = new PoolController(ctx, "summary");
});
};

View file

@ -0,0 +1,65 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const PoolCategoryList = require("../models/pool_category_list.js");
const PoolCreateView = require("../views/pool_create_view.js");
const EmptyView = require("../views/empty_view.js");
class PoolCreateController {
constructor(ctx) {
if (!api.hasPrivilege("pools:create")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to create pools.");
return;
}
PoolCategoryList.get().then(
(poolCategoriesResponse) => {
const categories = {};
for (let category of poolCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new PoolCreateView({
canCreate: api.hasPrivilege("pools:create"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("submit", (e) =>
this._evtCreate(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtCreate(e) {
this._view.clearMessages();
this._view.disableForm();
api.post(uri.formatApiLink("pool"), e.detail).then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink("pools"));
ctx.controller.showSuccess("Pool created.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = (router) => {
router.enter(["pool", "create"], (ctx, next) => {
ctx.controller = new PoolCreateController(ctx, "create");
});
};

View file

@ -0,0 +1,121 @@
"use strict";
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const PoolList = require("../models/pool_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PoolsHeaderView = require("../views/pools_header_view.js");
const PoolsPageView = require("../views/pools_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
"id",
"names",
"posts",
"creationTime",
"postCount",
"category",
];
class PoolListController {
constructor(ctx) {
this._pageController = new PageController();
if (!api.hasPrivilege("pools:list")) {
this._view = new EmptyView();
this._view.showError("You don't have privileges to view pools.");
return;
}
this._ctx = ctx;
topNavigation.activate("pools");
topNavigation.setTitle("Listing pools");
this._headerView = new PoolsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canCreate: api.hasPrivilege("pools:create"),
canEditPoolCategories: api.hasPrivilege("poolCategories:edit"),
});
this._headerView.addEventListener(
"submit",
(e) => this._evtSubmit(e),
);
this._headerView.addEventListener(
"navigate",
(e) => this._evtNavigate(e)
);
this._syncPageController();
}
showSuccess(message) {
this._pageController.showSuccess(message);
}
showError(message) {
this._pageController.showError(message);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.pool.save().then(
() => {
this._installView(e.detail.pool, "edit");
this._view.showSuccess("Pool created.");
router.replace(
uri.formatClientLink("pool", e.detail.pool.id, "edit"),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink("pools", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
_syncPageController() {
this._pageController.run({
parameters: this._ctx.parameters,
defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("pools", parameters);
},
requestPage: (offset, limit) => {
return PoolList.search(
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: (pageCtx) => {
return new PoolsPageView(pageCtx);
},
});
}
}
module.exports = (router) => {
router.enter(["pools"], (ctx, next) => {
ctx.controller = new PoolListController(ctx);
});
};

View file

@ -1,28 +1,33 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const settings = require('../models/settings.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const PostDetailView = require('../views/post_detail_view.js');
const BasePostController = require('./base_post_controller.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const settings = require("../models/settings.js");
const Post = require("../models/post.js");
const PostList = require("../models/post_list.js");
const PostDetailView = require("../views/post_detail_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
class PostDetailController extends BasePostController {
constructor(ctx, section) {
super(ctx);
Post.get(ctx.parameters.id).then(post => {
this._id = ctx.parameters.id;
post.addEventListener('change', e => this._evtSaved(e, section));
this._installView(post, section);
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
Post.get(ctx.parameters.id).then(
(post) => {
this._id = ctx.parameters.id;
post.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
this._installView(post, section);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
showSuccess(message) {
@ -33,56 +38,68 @@ class PostDetailController extends BasePostController {
this._view = new PostDetailView({
post: post,
section: section,
canMerge: api.hasPrivilege('posts:merge'),
canMerge: api.hasPrivilege("posts:merge"),
});
this._view.addEventListener('select', e => this._evtSelect(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener("select", (e) => this._evtSelect(e));
this._view.addEventListener("merge", (e) => this._evtMerge(e));
}
_evtSelect(e) {
this._view.clearMessages();
this._view.disableForm();
Post.get(e.detail.postId).then(post => {
this._view.selectPost(post);
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
Post.get(e.detail.postId).then(
(post) => {
this._view.selectPost(post);
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtSaved(e, section) {
misc.disableExitConfirmation();
if (this._id !== e.detail.post.id) {
router.replace(
uri.formatClientLink('post', e.detail.post.id, section),
null, false);
uri.formatClientLink("post", e.detail.post.id, section),
null,
false
);
}
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.post.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(() => {
this._installView(e.detail.post, 'merge');
this._view.showSuccess('Post merged.');
router.replace(
uri.formatClientLink(
'post', e.detail.targetPost.id, 'merge'),
null, false);
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.post
.merge(e.detail.targetPost.id, e.detail.useOldContent)
.then(
() => {
this._installView(e.detail.post, "merge");
this._view.showSuccess("Post merged.");
router.replace(
uri.formatClientLink(
"post",
e.detail.targetPost.id,
"merge"
),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(
['post', ':id', 'merge'],
(ctx, next) => {
ctx.controller = new PostDetailController(ctx, 'merge');
});
module.exports = (router) => {
router.enter(["post", ":id", "merge"], (ctx, next) => {
ctx.controller = new PostDetailController(ctx, "merge");
});
};

View file

@ -1,47 +1,68 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const settings = require('../models/settings.js');
const uri = require('../util/uri.js');
const PostList = require('../models/post_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const PostsHeaderView = require('../views/posts_header_view.js');
const PostsPageView = require('../views/posts_page_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const settings = require("../models/settings.js");
const uri = require("../util/uri.js");
const PostList = require("../models/post_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const PostsHeaderView = require("../views/posts_header_view.js");
const PostsPageView = require("../views/posts_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
'id', 'thumbnailUrl', 'type', 'safety',
'score', 'favoriteCount', 'commentCount', 'tags', 'version'];
"id",
"thumbnailUrl",
"type",
"safety",
"score",
"favoriteCount",
"commentCount",
"tags",
"version",
];
class PostListController {
constructor(ctx) {
if (!api.hasPrivilege('posts:list')) {
this._pageController = new PageController();
if (!api.hasPrivilege("posts:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view posts.');
this._view.showError("You don't have privileges to view posts.");
return;
}
topNavigation.activate('posts');
topNavigation.setTitle('Listing posts');
this._ctx = ctx;
this._pageController = new PageController();
topNavigation.activate("posts");
topNavigation.setTitle("Listing posts");
this._headerView = new PostsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
enableSafety: api.safetyEnabled(),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety: api.hasPrivilege('posts:bulk-edit:safety'),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege("posts:bulk-edit:safety"),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: {
tags: this._bulkEditTags
tags: this._bulkEditTags,
},
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
if (this._headerView._bulkDeleteEditor) {
this._headerView._bulkDeleteEditor.addEventListener(
"deleteSelectedPosts",
(e) => {
this._evtDeleteSelectedPosts(e);
}
);
}
this._postsMarkedForDeletion = [];
this._syncPageController();
}
@ -50,34 +71,67 @@ class PostListController {
}
get _bulkEditTags() {
return (this._ctx.parameters.tag || '').split(/\s+/).filter(s => s);
return (this._ctx.parameters.tag || "").split(/\s+/).filter((s) => s);
}
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('posts', e.detail.parameters));
uri.formatClientLink("posts", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
_evtTag(e) {
Promise.all(
this._bulkEditTags.map(tag =>
e.detail.post.tags.addByName(tag)))
this._bulkEditTags.map((tag) => e.detail.post.tags.addByName(tag))
)
.then(e.detail.post.save())
.catch(error => window.alert(error.message));
.catch((error) => window.alert(error.message));
}
_evtUntag(e) {
for (let tag of this._bulkEditTags) {
e.detail.post.tags.removeByName(tag);
}
e.detail.post.save().catch(error => window.alert(error.message));
e.detail.post.save().catch((error) => window.alert(error.message));
}
_evtChangeSafety(e) {
e.detail.post.safety = e.detail.safety;
e.detail.post.save().catch(error => window.alert(error.message));
e.detail.post.save().catch((error) => window.alert(error.message));
}
_evtMarkForDeletion(e) {
const postId = e.detail;
// Add or remove post from delete list
if (e.detail.delete) {
this._postsMarkedForDeletion.push(e.detail.post);
} else {
this._postsMarkedForDeletion = this._postsMarkedForDeletion.filter(
(x) => x.id != e.detail.post.id
);
}
}
_evtDeleteSelectedPosts(e) {
if (this._postsMarkedForDeletion.length == 0) return;
if (
confirm(
`Are you sure you want to delete ${this._postsMarkedForDeletion.length} posts?`
)
) {
Promise.all(
this._postsMarkedForDeletion.map((post) => post.delete())
)
.catch((error) => window.alert(error.message))
.then(() => {
this._postsMarkedForDeletion = [];
this._headerView._navigate();
});
}
}
_syncPageController() {
@ -85,37 +139,51 @@ class PostListController {
parameters: this._ctx.parameters,
defaultLimit: parseInt(settings.get().postsPerPage),
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('posts', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("posts", parameters);
},
requestPage: (offset, limit) => {
return PostList.search(
this._ctx.parameters.query, offset, limit, fields);
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canBulkEditTags: api.hasPrivilege('posts:bulk-edit:tags'),
canBulkEditSafety:
api.hasPrivilege('posts:bulk-edit:safety'),
canViewPosts: api.hasPrivilege("posts:view"),
canBulkEditTags: api.hasPrivilege("posts:bulk-edit:tags"),
canBulkEditSafety: api.hasPrivilege(
"posts:bulk-edit:safety"
),
canBulkDelete: api.hasPrivilege("posts:bulk-edit:delete"),
bulkEdit: {
tags: this._bulkEditTags,
markedForDeletion: this._postsMarkedForDeletion,
},
postFlow: settings.get().postFlow,
});
const view = new PostsPageView(pageCtx);
view.addEventListener('tag', e => this._evtTag(e));
view.addEventListener('untag', e => this._evtUntag(e));
view.addEventListener(
'changeSafety', e => this._evtChangeSafety(e));
view.addEventListener("tag", (e) => this._evtTag(e));
view.addEventListener("untag", (e) => this._evtUntag(e));
view.addEventListener("changeSafety", (e) =>
this._evtChangeSafety(e)
);
view.addEventListener("markForDeletion", (e) =>
this._evtMarkForDeletion(e)
);
return view;
},
});
}
}
module.exports = router => {
router.enter(
['posts'],
(ctx, next) => { ctx.controller = new PostListController(ctx); });
module.exports = (router) => {
router.enter(["posts"], (ctx, next) => {
ctx.controller = new PostListController(ctx);
});
};

View file

@ -1,16 +1,16 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const settings = require('../models/settings.js');
const Comment = require('../models/comment.js');
const Post = require('../models/post.js');
const PostList = require('../models/post_list.js');
const PostMainView = require('../views/post_main_view.js');
const BasePostController = require('./base_post_controller.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const settings = require("../models/settings.js");
const Comment = require("../models/comment.js");
const Post = require("../models/post.js");
const PostList = require("../models/post_list.js");
const PostMainView = require("../views/post_main_view.js");
const BasePostController = require("./base_post_controller.js");
const EmptyView = require("../views/empty_view.js");
class PostMainController extends BasePostController {
constructor(ctx, editMode) {
@ -18,77 +18,110 @@ class PostMainController extends BasePostController {
let parameters = ctx.parameters;
Promise.all([
Post.get(ctx.parameters.id),
PostList.getAround(
ctx.parameters.id,
parameters ? parameters.query : null),
]).then(responses => {
const [post, aroundResponse] = responses;
Post.get(ctx.parameters.id),
PostList.getAround(
ctx.parameters.id,
parameters ? parameters.query : null
),
]).then(
(responses) => {
const [post, aroundResponse] = responses;
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
if (parameters.query) {
ctx.state.parameters = parameters;
const url = editMode ?
uri.formatClientLink('post', ctx.parameters.id, 'edit') :
uri.formatClientLink('post', ctx.parameters.id);
router.replace(url, ctx.state, false);
// remove junk from query, but save it into history so that it can
// be still accessed after history navigation / page refresh
if (parameters.query) {
ctx.state.parameters = parameters;
const url = editMode
? uri.formatClientLink(
"post",
ctx.parameters.id,
"edit"
)
: uri.formatClientLink("post", ctx.parameters.id);
router.replace(url, ctx.state, false);
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev
? aroundResponse.prev.id
: null,
nextPostId: aroundResponse.next
? aroundResponse.next.id
: null,
canEditPosts: api.hasPrivilege("posts:edit"),
canDeletePosts: api.hasPrivilege("posts:delete"),
canFeaturePosts: api.hasPrivilege("posts:feature"),
canListComments: api.hasPrivilege("comments:list"),
canCreateComments: api.hasPrivilege("comments:create"),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
"favorite",
(e) => this._evtFavoritePost(e)
);
this._view.sidebarControl.addEventListener(
"unfavorite",
(e) => this._evtUnfavoritePost(e)
);
this._view.sidebarControl.addEventListener("score", (e) =>
this._evtScorePost(e)
);
this._view.sidebarControl.addEventListener(
"fitModeChange",
(e) => this._evtFitModeChange(e)
);
this._view.sidebarControl.addEventListener("change", (e) =>
this._evtPostChange(e)
);
this._view.sidebarControl.addEventListener("submit", (e) =>
this._evtUpdatePost(e)
);
this._view.sidebarControl.addEventListener(
"feature",
(e) => this._evtFeaturePost(e)
);
this._view.sidebarControl.addEventListener("delete", (e) =>
this._evtDeletePost(e)
);
this._view.sidebarControl.addEventListener("merge", (e) =>
this._evtMergePost(e)
);
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener("change", (e) =>
this._evtCommentChange(e)
);
this._view.commentControl.addEventListener("submit", (e) =>
this._evtCreateComment(e)
);
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
"submit",
(e) => this._evtUpdateComment(e)
);
this._view.commentListControl.addEventListener(
"score",
(e) => this._evtScoreComment(e)
);
this._view.commentListControl.addEventListener(
"delete",
(e) => this._evtDeleteComment(e)
);
}
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._post = post;
this._view = new PostMainView({
post: post,
editMode: editMode,
prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
canEditPosts: api.hasPrivilege('posts:edit'),
canDeletePosts: api.hasPrivilege('posts:delete'),
canFeaturePosts: api.hasPrivilege('posts:feature'),
canListComments: api.hasPrivilege('comments:list'),
canCreateComments: api.hasPrivilege('comments:create'),
parameters: parameters,
});
if (this._view.sidebarControl) {
this._view.sidebarControl.addEventListener(
'favorite', e => this._evtFavoritePost(e));
this._view.sidebarControl.addEventListener(
'unfavorite', e => this._evtUnfavoritePost(e));
this._view.sidebarControl.addEventListener(
'score', e => this._evtScorePost(e));
this._view.sidebarControl.addEventListener(
'fitModeChange', e => this._evtFitModeChange(e));
this._view.sidebarControl.addEventListener(
'change', e => this._evtPostChange(e));
this._view.sidebarControl.addEventListener(
'submit', e => this._evtUpdatePost(e));
this._view.sidebarControl.addEventListener(
'feature', e => this._evtFeaturePost(e));
this._view.sidebarControl.addEventListener(
'delete', e => this._evtDeletePost(e));
this._view.sidebarControl.addEventListener(
'merge', e => this._evtMergePost(e));
}
if (this._view.commentControl) {
this._view.commentControl.addEventListener(
'change', e => this._evtCommentChange(e));
this._view.commentControl.addEventListener(
'submit', e => this._evtCreateComment(e));
}
if (this._view.commentListControl) {
this._view.commentListControl.addEventListener(
'submit', e => this._evtUpdateComment(e));
this._view.commentListControl.addEventListener(
'score', e => this._evtScoreComment(e));
this._view.commentListControl.addEventListener(
'delete', e => this._evtDeleteComment(e));
}
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtFitModeChange(e) {
@ -100,65 +133,74 @@ class PostMainController extends BasePostController {
_evtFeaturePost(e) {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
e.detail.post.feature()
.then(() => {
this._view.sidebarControl.showSuccess('Post featured.');
e.detail.post.feature().then(
() => {
this._view.sidebarControl.showSuccess("Post featured.");
this._view.sidebarControl.enableForm();
}, error => {
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtMergePost(e) {
router.show(uri.formatClientLink('post', e.detail.post.id, 'merge'));
router.show(uri.formatClientLink("post", e.detail.post.id, "merge"));
}
_evtDeletePost(e) {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
e.detail.post.delete()
.then(() => {
e.detail.post.delete().then(
() => {
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Post deleted.');
}, error => {
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Post deleted.");
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtUpdatePost(e) {
this._view.sidebarControl.disableForm();
this._view.sidebarControl.clearMessages();
const post = e.detail.post;
if (e.detail.safety !== undefined) {
if (e.detail.safety !== undefined && e.detail.safety !== null) {
post.safety = e.detail.safety;
}
if (e.detail.flags !== undefined) {
if (e.detail.flags !== undefined && e.detail.flags !== null) {
post.flags = e.detail.flags;
}
if (e.detail.relations !== undefined) {
if (e.detail.relations !== undefined && e.detail.relations !== null) {
post.relations = e.detail.relations;
}
if (e.detail.content !== undefined) {
if (e.detail.content !== undefined && e.detail.content !== null) {
post.newContent = e.detail.content;
}
if (e.detail.thumbnail !== undefined) {
if (e.detail.thumbnail !== undefined && e.detail.thumbnail !== null) {
post.newThumbnail = e.detail.thumbnail;
}
if (e.detail.source !== undefined) {
if (e.detail.source !== undefined && e.detail.source !== null) {
post.source = e.detail.source;
}
post.save()
.then(() => {
this._view.sidebarControl.showSuccess('Post saved.');
if (e.detail.desc !== undefined && e.detail.desc !== null) {
post.desc = e.detail.desc;
}
post.save().then(
() => {
this._view.sidebarControl.showSuccess("Post saved.");
this._view.sidebarControl.enableForm();
misc.disableExitConfirmation();
}, error => {
},
(error) => {
this._view.sidebarControl.showError(error.message);
this._view.sidebarControl.enableForm();
});
}
);
}
_evtPostChange(e) {
@ -173,79 +215,82 @@ class PostMainController extends BasePostController {
this._view.commentControl.disableForm();
const comment = Comment.create(this._post.id);
comment.text = e.detail.text;
comment.save()
.then(() => {
comment.save().then(
() => {
this._post.comments.add(comment);
this._view.commentControl.exitEditMode();
this._view.commentControl.enableForm();
misc.disableExitConfirmation();
}, error => {
},
(error) => {
this._view.commentControl.showError(error.message);
this._view.commentControl.enableForm();
});
}
);
}
_evtUpdateComment(e) {
// TODO: disable form
e.detail.comment.text = e.detail.text;
e.detail.comment.save()
.catch(error => {
e.detail.target.showError(error.message);
// TODO: enable form
});
e.detail.comment.save().catch((error) => {
e.detail.target.showError(error.message);
// TODO: enable form
});
}
_evtScoreComment(e) {
e.detail.comment.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.comment
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtDeleteComment(e) {
e.detail.comment.delete()
.catch(error => window.alert(error.message));
e.detail.comment
.delete()
.catch((error) => window.alert(error.message));
}
_evtScorePost(e) {
if (!api.hasPrivilege('posts:score')) {
if (!api.hasPrivilege("posts:score")) {
return;
}
e.detail.post.setScore(e.detail.score)
.catch(error => window.alert(error.message));
e.detail.post
.setScore(e.detail.score)
.catch((error) => window.alert(error.message));
}
_evtFavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) {
if (!api.hasPrivilege("posts:favorite")) {
return;
}
e.detail.post.addToFavorites()
.catch(error => window.alert(error.message));
e.detail.post
.addToFavorites()
.catch((error) => window.alert(error.message));
}
_evtUnfavoritePost(e) {
if (!api.hasPrivilege('posts:favorite')) {
if (!api.hasPrivilege("posts:favorite")) {
return;
}
e.detail.post.removeFromFavorites()
.catch(error => window.alert(error.message));
e.detail.post
.removeFromFavorites()
.catch((error) => window.alert(error.message));
}
}
module.exports = router => {
router.enter(['post', ':id', 'edit'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
});
router.enter(
['post', ':id'],
(ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, false);
});
module.exports = (router) => {
router.enter(["post", ":id", "edit"], (ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, true);
});
router.enter(["post", ":id"], (ctx, next) => {
// restore parameters from history state
if (ctx.state.parameters) {
Object.assign(ctx.parameters, ctx.state.parameters);
}
ctx.controller = new PostMainController(ctx, false);
});
};

View file

@ -1,40 +1,40 @@
'use strict';
"use strict";
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const progress = require('../util/progress.js');
const topNavigation = require('../models/top_navigation.js');
const Post = require('../models/post.js');
const Tag = require('../models/tag.js');
const PostUploadView = require('../views/post_upload_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const router = require("../router.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const progress = require("../util/progress.js");
const topNavigation = require("../models/top_navigation.js");
const Post = require("../models/post.js");
const Tag = require("../models/tag.js");
const PostUploadView = require("../views/post_upload_view.js");
const EmptyView = require("../views/empty_view.js");
const genericErrorMessage =
'One of the posts needs your attention; ' +
"One or more posts needs your attention; " +
'click "resume upload" when you\'re ready.';
class PostUploadController {
constructor() {
this._lastCancellablePromise = null;
if (!api.hasPrivilege('posts:create')) {
if (!api.hasPrivilege("posts:create")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to upload posts.');
this._view.showError("You don't have privileges to upload posts.");
return;
}
topNavigation.activate('upload');
topNavigation.setTitle('Upload');
topNavigation.activate("upload");
topNavigation.setTitle("Upload");
this._view = new PostUploadView({
canUploadAnonymously: api.hasPrivilege('posts:create:anonymous'),
canViewPosts: api.hasPrivilege('posts:view'),
canUploadAnonymously: api.hasPrivilege("posts:create:anonymous"),
canViewPosts: api.hasPrivilege("posts:view"),
enableSafety: api.safetyEnabled(),
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtSubmit(e));
this._view.addEventListener('cancel', e => this._evtCancel(e));
this._view.addEventListener("change", (e) => this._evtChange(e));
this._view.addEventListener("submit", (e) => this._evtSubmit(e));
this._view.addEventListener("cancel", (e) => this._evtCancel(e));
}
_evtChange(e) {
@ -55,89 +55,130 @@ class PostUploadController {
_evtSubmit(e) {
this._view.disableForm();
this._view.clearMessages();
let anyFailures = false;
e.detail.uploadables.reduce(
(promise, uploadable) =>
promise.then(() => this._uploadSinglePost(
uploadable, e.detail.skipDuplicates)),
Promise.resolve())
.then(() => {
e.detail.uploadables
.reduce(
(promise, uploadable) =>
promise.then(() =>
this._uploadSinglePost(
uploadable,
e.detail.skipDuplicates,
e.detail.alwaysUploadSimilar
).catch((error) => {
anyFailures = true;
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes =
error.similarPosts;
this._view.updateUploadable(
error.uploadable
);
this._view.showInfo(
error.message,
error.uploadable
);
} else {
this._view.showError(
error.message,
error.uploadable
);
}
} else {
this._view.showError(
error.message,
uploadable
);
}
if (e.detail.pauseRemainOnError) {
return Promise.reject();
}
})
),
Promise.resolve()
)
.then(() => {
if (anyFailures) {
return Promise.reject();
}
})
.then(
() => {
this._view.clearMessages();
misc.disableExitConfirmation();
const ctx = router.show(uri.formatClientLink('posts'));
ctx.controller.showSuccess('Posts uploaded.');
}, error => {
if (error.uploadable) {
if (error.similarPosts) {
error.uploadable.lookalikes = error.similarPosts;
this._view.updateUploadable(error.uploadable);
this._view.showInfo(genericErrorMessage);
this._view.showInfo(
error.message, error.uploadable);
} else {
this._view.showError(genericErrorMessage);
this._view.showError(
error.message, error.uploadable);
}
} else {
this._view.showError(error.message);
}
const ctx = router.show(uri.formatClientLink("posts"));
ctx.controller.showSuccess("Posts uploaded.");
},
(error) => {
this._view.showError(genericErrorMessage);
this._view.enableForm();
});
}
);
}
_uploadSinglePost(uploadable, skipDuplicates) {
_uploadSinglePost(uploadable, skipDuplicates, alwaysUploadSimilar) {
progress.start();
let reverseSearchPromise = Promise.resolve();
if (!uploadable.lookalikesConfirmed) {
reverseSearchPromise =
Post.reverseSearch(uploadable.url || uploadable.file);
reverseSearchPromise = Post.reverseSearch(
uploadable.url || uploadable.file
);
}
this._lastCancellablePromise = reverseSearchPromise;
return reverseSearchPromise.then(searchResult => {
if (searchResult) {
// notify about exact duplicate
if (searchResult.exactPost) {
if (skipDuplicates) {
this._view.removeUploadable(uploadable);
return Promise.resolve();
} else {
let error = new Error('Post already uploaded ' +
`(@${searchResult.exactPost.id})`);
return reverseSearchPromise
.then((searchResult) => {
if (searchResult) {
// notify about exact duplicate
if (searchResult.exactPost) {
if (skipDuplicates) {
this._view.removeUploadable(uploadable);
return Promise.resolve();
} else {
let error = new Error(
"Post already uploaded " +
`(@${searchResult.exactPost.id})`
);
error.uploadable = uploadable;
return Promise.reject(error);
}
}
// notify about similar posts
if (
searchResult.similarPosts.length &&
!alwaysUploadSimilar
) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
"posts.\nYou can resume or discard this upload."
);
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
}
}
// notify about similar posts
if (searchResult.similarPosts.length) {
let error = new Error(
`Found ${searchResult.similarPosts.length} similar ` +
'posts.\nYou can resume or discard this upload.');
error.uploadable = uploadable;
error.similarPosts = searchResult.similarPosts;
return Promise.reject(error);
}
}
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous)
.then(() => {
// no duplicates, proceed with saving
let post = this._uploadableToPost(uploadable);
let savePromise = post.save(uploadable.anonymous).then(() => {
this._view.removeUploadable(uploadable);
return Promise.resolve();
});
this._lastCancellablePromise = savePromise;
return savePromise;
}).then(result => {
progress.done();
return Promise.resolve(result);
}, error => {
error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
});
this._lastCancellablePromise = savePromise;
return savePromise;
})
.then(
(result) => {
progress.done();
return Promise.resolve(result);
},
(error) => {
error.uploadable = uploadable;
progress.done();
return Promise.reject(error);
}
);
}
_uploadableToPost(uploadable) {
@ -153,13 +194,15 @@ class PostUploadController {
post.newContent = uploadable.url || uploadable.file;
// if uploadable.source is ever going to be a valid field (e.g when setting source directly in the upload window)
// you'll need to change the line below to `post.source = uploadable.source || uploadable.url;`
if (uploadable.url) post.source = uploadable.url;
if (uploadable.url) {
post.source = uploadable.url;
}
return post;
}
}
module.exports = router => {
router.enter(['upload'], (ctx, next) => {
module.exports = (router) => {
router.enter(["upload"], (ctx, next) => {
ctx.controller = new PostUploadController();
});
};

View file

@ -1,28 +1,28 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const topNavigation = require('../models/top_navigation.js');
const SettingsView = require('../views/settings_view.js');
const settings = require("../models/settings.js");
const topNavigation = require("../models/top_navigation.js");
const SettingsView = require("../views/settings_view.js");
class SettingsController {
constructor() {
topNavigation.activate('settings');
topNavigation.setTitle('Browsing settings');
topNavigation.activate("settings");
topNavigation.setTitle("Browsing settings");
this._view = new SettingsView({
settings: settings.get(),
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
this._view.addEventListener("submit", (e) => this._evtSubmit(e));
}
_evtSubmit(e) {
this._view.clearMessages();
settings.save(e.detail);
this._view.showSuccess('Settings saved.');
this._view.showSuccess("Settings saved.");
}
};
}
module.exports = router => {
router.enter(['settings'], (ctx, next) => {
module.exports = (router) => {
router.enter(["settings"], (ctx, next) => {
ctx.controller = new SettingsController();
});
};

View file

@ -1,41 +1,43 @@
'use strict';
"use strict";
const api = require('../api.js');
const uri = require('../util/uri.js');
const SnapshotList = require('../models/snapshot_list.js');
const PageController = require('../controllers/page_controller.js');
const topNavigation = require('../models/top_navigation.js');
const SnapshotsPageView = require('../views/snapshots_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const uri = require("../util/uri.js");
const SnapshotList = require("../models/snapshot_list.js");
const PageController = require("../controllers/page_controller.js");
const topNavigation = require("../models/top_navigation.js");
const SnapshotsPageView = require("../views/snapshots_page_view.js");
const EmptyView = require("../views/empty_view.js");
class SnapshotsController {
constructor(ctx) {
if (!api.hasPrivilege('snapshots:list')) {
if (!api.hasPrivilege("snapshots:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view history.');
this._view.showError("You don't have privileges to view history.");
return;
}
topNavigation.activate('');
topNavigation.setTitle('History');
topNavigation.activate("");
topNavigation.setTitle("History");
this._pageController = new PageController();
this._pageController.run({
parameters: ctx.parameters,
defaultLimit: 25,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('history', parameters);
const parameters = Object.assign({}, ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("history", parameters);
},
requestPage: (offset, limit) => {
return SnapshotList.search('', offset, limit);
return SnapshotList.search("", offset, limit);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewPosts: api.hasPrivilege('posts:view'),
canViewUsers: api.hasPrivilege('users:view'),
canViewTags: api.hasPrivilege('tags:view'),
canViewPosts: api.hasPrivilege("posts:view"),
canViewUsers: api.hasPrivilege("users:view"),
canViewTags: api.hasPrivilege("tags:view"),
});
return new SnapshotsPageView(pageCtx);
},
@ -43,7 +45,8 @@ class SnapshotsController {
}
}
module.exports = router => {
router.enter(['history'],
(ctx, next) => { ctx.controller = new SnapshotsController(ctx); });
module.exports = (router) => {
router.enter(["history"], (ctx, next) => {
ctx.controller = new SnapshotsController(ctx);
});
};

View file

@ -1,57 +1,68 @@
'use strict';
"use strict";
const api = require('../api.js');
const tags = require('../tags.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagCategoriesView = require('../views/tag_categories_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const tags = require("../tags.js");
const TagCategoryList = require("../models/tag_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const TagCategoriesView = require("../views/tag_categories_view.js");
const EmptyView = require("../views/empty_view.js");
class TagCategoriesController {
constructor() {
if (!api.hasPrivilege('tagCategories:list')) {
if (!api.hasPrivilege("tagCategories:list")) {
this._view = new EmptyView();
this._view.showError(
'You don\'t have privileges to view tag categories.');
"You don't have privileges to view tag categories."
);
return;
}
topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
TagCategoryList.get().then(response => {
this._tagCategories = response.results;
this._view = new TagCategoriesView({
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege('tagCategories:edit:name'),
canEditColor: api.hasPrivilege('tagCategories:edit:color'),
canDelete: api.hasPrivilege('tagCategories:delete'),
canCreate: api.hasPrivilege('tagCategories:create'),
canSetDefault: api.hasPrivilege('tagCategories:setDefault'),
});
this._view.addEventListener('submit', e => this._evtSubmit(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
topNavigation.activate("tags");
topNavigation.setTitle("Listing tags");
TagCategoryList.get().then(
(response) => {
this._tagCategories = response.results;
this._view = new TagCategoriesView({
tagCategories: this._tagCategories,
canEditName: api.hasPrivilege("tagCategories:edit:name"),
canEditColor: api.hasPrivilege("tagCategories:edit:color"),
canEditOrder: api.hasPrivilege("tagCategories:edit:order"),
canDelete: api.hasPrivilege("tagCategories:delete"),
canCreate: api.hasPrivilege("tagCategories:create"),
canSetDefault: api.hasPrivilege(
"tagCategories:setDefault"
),
});
this._view.addEventListener("submit", (e) =>
this._evtSubmit(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
);
}
_evtSubmit(e) {
this._view.clearMessages();
this._view.disableForm();
this._tagCategories.save()
.then(() => {
this._tagCategories.save().then(
() => {
tags.refreshCategoryColorMap();
this._view.enableForm();
this._view.showSuccess('Changes saved.');
}, error => {
this._view.showSuccess("Changes saved.");
},
(error) => {
this._view.enableForm();
this._view.showError(error.message);
});
}
);
}
}
module.exports = router => {
router.enter(['tag-categories'], (ctx, next) => {
module.exports = (router) => {
router.enter(["tag-categories"], (ctx, next) => {
ctx.controller = new TagCategoriesController(ctx, next);
});
};

View file

@ -1,63 +1,80 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const misc = require('../util/misc.js');
const uri = require('../util/uri.js');
const Tag = require('../models/tag.js');
const TagCategoryList = require('../models/tag_category_list.js');
const topNavigation = require('../models/top_navigation.js');
const TagView = require('../views/tag_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Tag = require("../models/tag.js");
const TagCategoryList = require("../models/tag_category_list.js");
const topNavigation = require("../models/top_navigation.js");
const TagView = require("../views/tag_view.js");
const EmptyView = require("../views/empty_view.js");
class TagController {
constructor(ctx, section) {
if (!api.hasPrivilege('tags:view')) {
if (!api.hasPrivilege("tags:view")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.');
this._view.showError("You don't have privileges to view tags.");
return;
}
Promise.all([
TagCategoryList.get(),
Tag.get(ctx.parameters.name),
]).then(responses => {
const [tagCategoriesResponse, tag] = responses;
]).then(
(responses) => {
const [tagCategoriesResponse, tag] = responses;
topNavigation.activate('tags');
topNavigation.setTitle('Tag #' + tag.names[0]);
topNavigation.activate("tags");
topNavigation.setTitle("Tag #" + tag.names[0]);
this._name = ctx.parameters.name;
tag.addEventListener('change', e => this._evtSaved(e, section));
this._name = ctx.parameters.name;
tag.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const categories = {};
for (let category of tagCategoriesResponse.results) {
categories[category.name] = category.name;
const categories = {};
for (let category of tagCategoriesResponse.results) {
categories[category.name] = category.name;
}
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege("tags:edit"),
canEditNames: api.hasPrivilege("tags:edit:names"),
canEditCategory: api.hasPrivilege("tags:edit:category"),
canEditImplications: api.hasPrivilege(
"tags:edit:implications"
),
canEditSuggestions: api.hasPrivilege(
"tags:edit:suggestions"
),
canEditDescription: api.hasPrivilege(
"tags:edit:description"
),
canMerge: api.hasPrivilege("tags:merge"),
canDelete: api.hasPrivilege("tags:delete"),
categories: categories,
escapeTagName: uri.escapeTagName,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("merge", (e) => this._evtMerge(e));
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
this._view = new TagView({
tag: tag,
section: section,
canEditAnything: api.hasPrivilege('tags:edit'),
canEditNames: api.hasPrivilege('tags:edit:names'),
canEditCategory: api.hasPrivilege('tags:edit:category'),
canEditImplications: api.hasPrivilege('tags:edit:implications'),
canEditSuggestions: api.hasPrivilege('tags:edit:suggestions'),
canEditDescription: api.hasPrivilege('tags:edit:description'),
canMerge: api.hasPrivilege('tags:merge'),
canDelete: api.hasPrivilege('tags:delete'),
categories: categories,
escapeColons: uri.escapeColons,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('merge', e => this._evtMerge(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
_evtChange(e) {
@ -68,75 +85,88 @@ class TagController {
misc.disableExitConfirmation();
if (this._name !== e.detail.tag.names[0]) {
router.replace(
uri.formatClientLink('tag', e.detail.tag.names[0], section),
null, false);
uri.formatClientLink("tag", e.detail.tag.names[0], section),
null,
false
);
}
}
_evtUpdate(e) {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.names !== undefined) {
if (e.detail.names !== undefined && e.detail.names !== null) {
e.detail.tag.names = e.detail.names;
}
if (e.detail.category !== undefined) {
if (e.detail.category !== undefined && e.detail.category !== null) {
e.detail.tag.category = e.detail.category;
}
if (e.detail.description !== undefined) {
if (e.detail.description !== undefined && e.detail.description !== null) {
e.detail.tag.description = e.detail.description;
}
e.detail.tag.save().then(() => {
this._view.showSuccess('Tag saved.');
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.tag.save().then(
() => {
this._view.showSuccess("Tag saved.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtMerge(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag
.merge(e.detail.targetTagName, e.detail.addAlias)
.then(() => {
this._view.showSuccess('Tag merged.');
e.detail.tag.merge(e.detail.targetTagName, e.detail.addAlias).then(
() => {
this._view.showSuccess("Tag merged.");
this._view.enableForm();
router.replace(
uri.formatClientLink(
'tag', e.detail.targetTagName, 'merge'),
null, false);
}, error => {
"tag",
e.detail.targetTagName,
"merge"
),
null,
false
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
e.detail.tag.delete()
.then(() => {
const ctx = router.show(uri.formatClientLink('tags'));
ctx.controller.showSuccess('Tag deleted.');
}, error => {
e.detail.tag.delete().then(
() => {
const ctx = router.show(uri.formatClientLink("tags"));
ctx.controller.showSuccess("Tag deleted.");
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
module.exports = router => {
router.enter(['tag', ':name', 'edit'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'edit');
module.exports = (router) => {
router.enter(["tag", ":name", "edit"], (ctx, next) => {
ctx.controller = new TagController(ctx, "edit");
});
router.enter(['tag', ':name', 'merge'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'merge');
router.enter(["tag", ":name", "merge"], (ctx, next) => {
ctx.controller = new TagController(ctx, "merge");
});
router.enter(['tag', ':name', 'delete'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'delete');
router.enter(["tag", ":name", "delete"], (ctx, next) => {
ctx.controller = new TagController(ctx, "delete");
});
router.enter(['tag', ':name'], (ctx, next) => {
ctx.controller = new TagController(ctx, 'summary');
router.enter(["tag", ":name"], (ctx, next) => {
ctx.controller = new TagController(ctx, "summary");
});
};

View file

@ -1,44 +1,47 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const TagList = require('../models/tag_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const TagsHeaderView = require('../views/tags_header_view.js');
const TagsPageView = require('../views/tags_page_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const TagList = require("../models/tag_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const TagsHeaderView = require("../views/tags_header_view.js");
const TagsPageView = require("../views/tags_page_view.js");
const EmptyView = require("../views/empty_view.js");
const fields = [
'names',
'suggestions',
'implications',
'creationTime',
'usages',
'category'];
"names",
"suggestions",
"implications",
"creationTime",
"usages",
"category",
];
class TagListController {
constructor(ctx) {
if (!api.hasPrivilege('tags:list')) {
this._pageController = new PageController();
if (!api.hasPrivilege("tags:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view tags.');
this._view.showError("You don't have privileges to view tags.");
return;
}
topNavigation.activate('tags');
topNavigation.setTitle('Listing tags');
this._ctx = ctx;
this._pageController = new PageController();
topNavigation.activate("tags");
topNavigation.setTitle("Listing tags");
this._headerView = new TagsHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
canEditTagCategories: api.hasPrivilege('tagCategories:edit'),
canEditTagCategories: api.hasPrivilege("tagCategories:edit"),
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._syncPageController();
}
@ -53,7 +56,8 @@ class TagListController {
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('tags', e.detail.parameters));
uri.formatClientLink("tags", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -63,23 +67,29 @@ class TagListController {
parameters: this._ctx.parameters,
defaultLimit: 50,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset: offset, limit: limit});
return uri.formatClientLink('tags', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("tags", parameters);
},
requestPage: (offset, limit) => {
return TagList.search(
this._ctx.parameters.query, offset, limit, fields);
this._ctx.parameters.query,
offset,
limit,
fields
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
return new TagsPageView(pageCtx);
},
});
}
}
module.exports = router => {
router.enter(
['tags'],
(ctx, next) => { ctx.controller = new TagListController(ctx); });
module.exports = (router) => {
router.enter(["tags"], (ctx, next) => {
ctx.controller = new TagListController(ctx);
});
};

View file

@ -1,19 +1,20 @@
'use strict';
"use strict";
const api = require('../api.js');
const topNavigation = require('../models/top_navigation.js');
const TopNavigationView = require('../views/top_navigation_view.js');
const api = require("../api.js");
const topNavigation = require("../models/top_navigation.js");
const TopNavigationView = require("../views/top_navigation_view.js");
class TopNavigationController {
constructor() {
api.fetchConfig().then(() => {
this._topNavigationView = new TopNavigationView();
topNavigation.addEventListener(
'activate', e => this._evtActivate(e));
topNavigation.addEventListener("activate", (e) =>
this._evtActivate(e)
);
api.addEventListener('login', e => this._evtAuthChange(e));
api.addEventListener('logout', e => this._evtAuthChange(e));
api.addEventListener("login", (e) => this._evtAuthChange(e));
api.addEventListener("logout", (e) => this._evtAuthChange(e));
this._render();
});
@ -28,37 +29,41 @@ class TopNavigationController {
}
_updateNavigationFromPrivileges() {
topNavigation.get('account').url = 'user/' + api.userName;
topNavigation.get('account').imageUrl =
api.user ? api.user.avatarUrl : null;
topNavigation.get("account").url = "user/" + api.userName;
topNavigation.get("account").imageUrl = api.user
? api.user.avatarUrl
: null;
topNavigation.showAll();
if (!api.hasPrivilege('posts:list')) {
topNavigation.hide('posts');
if (!api.hasPrivilege("posts:list")) {
topNavigation.hide("posts");
}
if (!api.hasPrivilege('posts:create')) {
topNavigation.hide('upload');
if (!api.hasPrivilege("posts:create")) {
topNavigation.hide("upload");
}
if (!api.hasPrivilege('comments:list')) {
topNavigation.hide('comments');
if (!api.hasPrivilege("comments:list")) {
topNavigation.hide("comments");
}
if (!api.hasPrivilege('tags:list')) {
topNavigation.hide('tags');
if (!api.hasPrivilege("tags:list")) {
topNavigation.hide("tags");
}
if (!api.hasPrivilege('users:list')) {
topNavigation.hide('users');
if (!api.hasPrivilege("users:list")) {
topNavigation.hide("users");
}
if (!api.hasPrivilege("pools:list")) {
topNavigation.hide("pools");
}
if (api.isLoggedIn()) {
if (!api.hasPrivilege('users:create:any')) {
topNavigation.hide('register');
if (!api.hasPrivilege("users:create:any")) {
topNavigation.hide("register");
}
topNavigation.hide('login');
topNavigation.hide("login");
} else {
if (!api.hasPrivilege('users:create:self')) {
topNavigation.hide('register');
if (!api.hasPrivilege("users:create:self")) {
topNavigation.hide("register");
}
topNavigation.hide('account');
topNavigation.hide('logout');
topNavigation.hide("account");
topNavigation.hide("logout");
}
}
@ -66,10 +71,11 @@ class TopNavigationController {
this._updateNavigationFromPrivileges();
this._topNavigationView.render({
items: topNavigation.getAll(),
name: api.getName()
name: api.getName(),
});
this._topNavigationView.activate(
topNavigation.activeItem ? topNavigation.activeItem.key : '');
topNavigation.activeItem ? topNavigation.activeItem.key : ""
);
}
}

View file

@ -1,23 +1,25 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const misc = require('../util/misc.js');
const views = require('../util/views.js');
const User = require('../models/user.js');
const UserToken = require('../models/user_token.js');
const topNavigation = require('../models/top_navigation.js');
const UserView = require('../views/user_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const misc = require("../util/misc.js");
const views = require("../util/views.js");
const User = require("../models/user.js");
const UserToken = require("../models/user_token.js");
const topNavigation = require("../models/top_navigation.js");
const UserView = require("../views/user_view.js");
const EmptyView = require("../views/empty_view.js");
class UserController {
constructor(ctx, section) {
const userName = ctx.parameters.name;
if (!api.hasPrivilege('users:view') &&
!api.isLoggedIn({name: userName})) {
if (
!api.hasPrivilege("users:view") &&
!api.isLoggedIn({ name: userName })
) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.');
this._view.showError("You don't have privileges to view users.");
return;
}
@ -25,100 +27,128 @@ class UserController {
this._errorMessages = [];
let userTokenPromise = Promise.resolve([]);
if (section === 'list-tokens') {
userTokenPromise = UserToken.get(userName)
.then(userTokens => {
return userTokens.map(token => {
token.isCurrentAuthToken = api.isCurrentAuthToken(token);
if (section === "list-tokens") {
userTokenPromise = UserToken.get(userName).then(
(userTokens) => {
return userTokens.map((token) => {
token.isCurrentAuthToken =
api.isCurrentAuthToken(token);
return token;
});
}, error => {
},
(error) => {
return [];
});
}
);
}
topNavigation.setTitle('User ' + userName);
Promise.all([
userTokenPromise,
User.get(userName)
]).then(responses => {
const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? 'self' : 'any';
topNavigation.setTitle("User " + userName);
Promise.all([userTokenPromise, User.get(userName)]).then(
(responses) => {
const [userTokens, user] = responses;
const isLoggedIn = api.isLoggedIn(user);
const infix = isLoggedIn ? "self" : "any";
this._name = userName;
user.addEventListener('change', e => this._evtSaved(e, section));
this._name = userName;
user.addEventListener("change", (e) =>
this._evtSaved(e, section)
);
const myRankIndex = api.user ?
api.allRanks.indexOf(api.user.rank) :
0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === 'anonymous') {
continue;
const myRankIndex = api.user
? api.allRanks.indexOf(api.user.rank)
: 0;
let ranks = {};
for (let [rankIdx, rankIdentifier] of api.allRanks.entries()) {
if (rankIdentifier === "anonymous") {
continue;
}
if (rankIdx > myRankIndex) {
continue;
}
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
}
if (rankIdx > myRankIndex) {
continue;
if (isLoggedIn) {
topNavigation.activate("account");
} else {
topNavigation.activate("users");
}
ranks[rankIdentifier] = api.rankNames.get(rankIdentifier);
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(
`users:edit:${infix}:pass`
),
canEditEmail: api.hasPrivilege(
`users:edit:${infix}:email`
),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(
`users:edit:${infix}:avatar`
),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
canListTokens: api.hasPrivilege(
`userTokens:list:${infix}`
),
canCreateToken: api.hasPrivilege(
`userTokens:create:${infix}`
),
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
canDeleteToken: api.hasPrivilege(
`userTokens:delete:${infix}`
),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener("change", (e) =>
this._evtChange(e)
);
this._view.addEventListener("submit", (e) =>
this._evtUpdate(e)
);
this._view.addEventListener("delete", (e) =>
this._evtDelete(e)
);
this._view.addEventListener("create-token", (e) =>
this._evtCreateToken(e)
);
this._view.addEventListener("delete-token", (e) =>
this._evtDeleteToken(e)
);
this._view.addEventListener("update-token", (e) =>
this._evtUpdateToken(e)
);
for (let message of this._successMessages) {
this.showSuccess(message);
}
for (let message of this._errorMessages) {
this.showError(message);
}
},
(error) => {
this._view = new EmptyView();
this._view.showError(error.message);
}
if (isLoggedIn) {
topNavigation.activate('account');
} else {
topNavigation.activate('users');
}
this._view = new UserView({
user: user,
section: section,
isLoggedIn: isLoggedIn,
canEditName: api.hasPrivilege(`users:edit:${infix}:name`),
canEditPassword: api.hasPrivilege(`users:edit:${infix}:pass`),
canEditEmail: api.hasPrivilege(`users:edit:${infix}:email`),
canEditRank: api.hasPrivilege(`users:edit:${infix}:rank`),
canEditAvatar: api.hasPrivilege(`users:edit:${infix}:avatar`),
canEditAnything: api.hasPrivilege(`users:edit:${infix}`),
canListTokens: api.hasPrivilege(`userTokens:list:${infix}`),
canCreateToken: api.hasPrivilege(`userTokens:create:${infix}`),
canEditToken: api.hasPrivilege(`userTokens:edit:${infix}`),
canDeleteToken: api.hasPrivilege(`userTokens:delete:${infix}`),
canDelete: api.hasPrivilege(`users:delete:${infix}`),
ranks: ranks,
tokens: userTokens,
});
this._view.addEventListener('change', e => this._evtChange(e));
this._view.addEventListener('submit', e => this._evtUpdate(e));
this._view.addEventListener('delete', e => this._evtDelete(e));
this._view.addEventListener('create-token', e => this._evtCreateToken(e));
this._view.addEventListener('delete-token', e => this._evtDeleteToken(e));
this._view.addEventListener('update-token', e => this._evtUpdateToken(e));
for (let message of this._successMessages) {
this.showSuccess(message);
}
for (let message of this._errorMessages) {
this.showError(message);
}
}, error => {
this._view = new EmptyView();
this._view.showError(error.message);
});
);
}
showSuccess(message) {
if (typeof this._view === 'undefined') {
this._successMessages.push(message)
if (typeof this._view === "undefined") {
this._successMessages.push(message);
} else {
this._view.showSuccess(message);
}
}
showError(message) {
if (typeof this._view === 'undefined') {
this._errorMessages.push(message)
if (typeof this._view === "undefined") {
this._errorMessages.push(message);
} else {
this._view.showError(message);
}
@ -132,8 +162,10 @@ class UserController {
misc.disableExitConfirmation();
if (this._name !== e.detail.user.name) {
router.replace(
uri.formatClientLink('user', e.detail.user.name, section),
null, false);
uri.formatClientLink("user", e.detail.user.name, section),
null,
false
);
}
}
@ -141,95 +173,128 @@ class UserController {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
const infix = isLoggedIn ? 'self' : 'any';
const infix = isLoggedIn ? "self" : "any";
if (e.detail.name !== undefined) {
if (e.detail.name !== undefined && e.detail.name !== null) {
e.detail.user.name = e.detail.name;
}
if (e.detail.email !== undefined) {
if (e.detail.email !== undefined && e.detail.email !== null) {
e.detail.user.email = e.detail.email;
}
if (e.detail.rank !== undefined) {
if (e.detail.rank !== undefined && e.detail.rank !== null) {
e.detail.user.rank = e.detail.rank;
}
if (e.detail.password !== undefined) {
if (e.detail.password !== undefined && e.detail.password !== null) {
e.detail.user.password = e.detail.password;
}
if (e.detail.avatarStyle !== undefined) {
if (e.detail.avatarStyle !== undefined && e.detail.avatarStyle !== null) {
e.detail.user.avatarStyle = e.detail.avatarStyle;
if (e.detail.avatarContent) {
e.detail.user.avatarContent = e.detail.avatarContent;
}
}
e.detail.user.save().then(() => {
return isLoggedIn ?
api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false) :
Promise.resolve();
}).then(() => {
this._view.showSuccess('Settings updated.');
this._view.enableForm();
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.user
.save()
.then(() => {
return isLoggedIn
? api.login(
e.detail.name || api.userName,
e.detail.password || api.userPassword,
false
)
: Promise.resolve();
})
.then(
() => {
this._view.showSuccess("Settings updated.");
this._view.enableForm();
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
_evtDelete(e) {
this._view.clearMessages();
this._view.disableForm();
const isLoggedIn = api.isLoggedIn(e.detail.user);
e.detail.user.delete()
.then(() => {
e.detail.user.delete().then(
() => {
if (isLoggedIn) {
api.forget();
api.logout();
}
if (api.hasPrivilege('users:list')) {
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('Account deleted.');
if (api.hasPrivilege("users:list")) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("Account deleted.");
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Account deleted.');
ctx.controller.showSuccess("Account deleted.");
}
}, error => {
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtCreateToken(e) {
this._view.clearMessages();
this._view.disableForm();
UserToken.create(e.detail.user.name, e.detail.note, e.detail.expirationTime)
.then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' created.');
}, error => {
UserToken.create(
e.detail.user.name,
e.detail.note,
e.detail.expirationTime
).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " created."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
_evtDeleteToken(e) {
this._view.clearMessages();
this._view.disableForm();
if (api.isCurrentAuthToken(e.detail.userToken)) {
router.show(uri.formatClientLink('logout'));
router.show(uri.formatClientLink("logout"));
} else {
e.detail.userToken.delete(e.detail.user.name)
.then(() => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + e.detail.userToken.token + ' deleted.');
}, error => {
e.detail.userToken.delete(e.detail.user.name).then(
() => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + e.detail.userToken.token + " deleted."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
});
}
);
}
}
@ -237,31 +302,42 @@ class UserController {
this._view.clearMessages();
this._view.disableForm();
if (e.detail.note !== undefined) {
if (e.detail.note !== undefined && e.detail.note !== null) {
e.detail.userToken.note = e.detail.note;
}
e.detail.userToken.save(e.detail.user.name).then(response => {
const ctx = router.show(uri.formatClientLink('user', e.detail.user.name, 'list-tokens'));
ctx.controller.showSuccess('Token ' + response.token + ' updated.');
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
e.detail.userToken.save(e.detail.user.name).then(
(response) => {
const ctx = router.show(
uri.formatClientLink(
"user",
e.detail.user.name,
"list-tokens"
)
);
ctx.controller.showSuccess(
"Token " + response.token + " updated."
);
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(['user', ':name'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'summary');
module.exports = (router) => {
router.enter(["user", ":name"], (ctx, next) => {
ctx.controller = new UserController(ctx, "summary");
});
router.enter(['user', ':name', 'edit'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'edit');
router.enter(["user", ":name", "edit"], (ctx, next) => {
ctx.controller = new UserController(ctx, "edit");
});
router.enter(['user', ':name', 'list-tokens'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'list-tokens');
router.enter(["user", ":name", "list-tokens"], (ctx, next) => {
ctx.controller = new UserController(ctx, "list-tokens");
});
router.enter(['user', ':name', 'delete'], (ctx, next) => {
ctx.controller = new UserController(ctx, 'delete');
router.enter(["user", ":name", "delete"], (ctx, next) => {
ctx.controller = new UserController(ctx, "delete");
});
};

View file

@ -1,35 +1,37 @@
'use strict';
"use strict";
const api = require('../api.js');
const router = require('../router.js');
const uri = require('../util/uri.js');
const UserList = require('../models/user_list.js');
const topNavigation = require('../models/top_navigation.js');
const PageController = require('../controllers/page_controller.js');
const UsersHeaderView = require('../views/users_header_view.js');
const UsersPageView = require('../views/users_page_view.js');
const EmptyView = require('../views/empty_view.js');
const api = require("../api.js");
const router = require("../router.js");
const uri = require("../util/uri.js");
const UserList = require("../models/user_list.js");
const topNavigation = require("../models/top_navigation.js");
const PageController = require("../controllers/page_controller.js");
const UsersHeaderView = require("../views/users_header_view.js");
const UsersPageView = require("../views/users_page_view.js");
const EmptyView = require("../views/empty_view.js");
class UserListController {
constructor(ctx) {
if (!api.hasPrivilege('users:list')) {
this._pageController = new PageController();
if (!api.hasPrivilege("users:list")) {
this._view = new EmptyView();
this._view.showError('You don\'t have privileges to view users.');
this._view.showError("You don't have privileges to view users.");
return;
}
topNavigation.activate('users');
topNavigation.setTitle('Listing users');
topNavigation.activate("users");
topNavigation.setTitle("Listing users");
this._ctx = ctx;
this._pageController = new PageController();
this._headerView = new UsersHeaderView({
hostNode: this._pageController.view.pageHeaderHolderNode,
parameters: ctx.parameters,
});
this._headerView.addEventListener(
'navigate', e => this._evtNavigate(e));
this._headerView.addEventListener("navigate", (e) =>
this._evtNavigate(e)
);
this._syncPageController();
}
@ -40,7 +42,8 @@ class UserListController {
_evtNavigate(e) {
router.showNoDispatch(
uri.formatClientLink('users', e.detail.parameters));
uri.formatClientLink("users", e.detail.parameters)
);
Object.assign(this._ctx.parameters, e.detail.parameters);
this._syncPageController();
}
@ -50,17 +53,22 @@ class UserListController {
parameters: this._ctx.parameters,
defaultLimit: 30,
getClientUrlForPage: (offset, limit) => {
const parameters = Object.assign(
{}, this._ctx.parameters, {offset, offset, limit: limit});
return uri.formatClientLink('users', parameters);
const parameters = Object.assign({}, this._ctx.parameters, {
offset: offset,
limit: limit,
});
return uri.formatClientLink("users", parameters);
},
requestPage: (offset, limit) => {
return UserList.search(
this._ctx.parameters.query, offset, limit);
this._ctx.parameters.query,
offset,
limit
);
},
pageRenderer: pageCtx => {
pageRenderer: (pageCtx) => {
Object.assign(pageCtx, {
canViewUsers: api.hasPrivilege('users:view'),
canViewUsers: api.hasPrivilege("users:view"),
});
return new UsersPageView(pageCtx);
},
@ -68,8 +76,8 @@ class UserListController {
}
}
module.exports = router => {
router.enter(
['users'],
(ctx, next) => { ctx.controller = new UserListController(ctx); });
module.exports = (router) => {
router.enter(["users"], (ctx, next) => {
ctx.controller = new UserListController(ctx);
});
};

View file

@ -1,25 +1,25 @@
'use strict';
"use strict";
const router = require('../router.js');
const api = require('../api.js');
const uri = require('../util/uri.js');
const User = require('../models/user.js');
const topNavigation = require('../models/top_navigation.js');
const RegistrationView = require('../views/registration_view.js');
const EmptyView = require('../views/empty_view.js');
const router = require("../router.js");
const api = require("../api.js");
const uri = require("../util/uri.js");
const User = require("../models/user.js");
const topNavigation = require("../models/top_navigation.js");
const RegistrationView = require("../views/registration_view.js");
const EmptyView = require("../views/empty_view.js");
class UserRegistrationController {
constructor() {
if (!api.hasPrivilege('users:create:self')) {
if (!api.hasPrivilege("users:create:self")) {
this._view = new EmptyView();
this._view.showError('Registration is closed.');
this._view.showError("Registration is closed.");
return;
}
topNavigation.activate('register');
topNavigation.setTitle('Registration');
topNavigation.activate("register");
topNavigation.setTitle("Registration");
this._view = new RegistrationView();
this._view.addEventListener('submit', e => this._evtRegister(e));
this._view.addEventListener("submit", (e) => this._evtRegister(e));
}
_evtRegister(e) {
@ -30,30 +30,35 @@ class UserRegistrationController {
user.email = e.detail.email;
user.password = e.detail.password;
const isLoggedIn = api.isLoggedIn();
user.save().then(() => {
if (isLoggedIn) {
return Promise.resolve();
} else {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}
}).then(() => {
if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink('users'));
ctx.controller.showSuccess('User added!');
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess('Welcome aboard!');
}
}, error => {
this._view.showError(error.message);
this._view.enableForm();
});
user.save()
.then(() => {
if (isLoggedIn) {
return Promise.resolve();
} else {
api.forget();
return api.login(e.detail.name, e.detail.password, false);
}
})
.then(
() => {
if (isLoggedIn) {
const ctx = router.show(uri.formatClientLink("users"));
ctx.controller.showSuccess("User added!");
} else {
const ctx = router.show(uri.formatClientLink());
ctx.controller.showSuccess("Welcome aboard!");
}
},
(error) => {
this._view.showError(error.message);
this._view.enableForm();
}
);
}
}
module.exports = router => {
router.enter(['register'], (ctx, next) => {
module.exports = (router) => {
router.enter(["register"], (ctx, next) => {
new UserRegistrationController();
});
};

View file

@ -1,6 +1,6 @@
'use strict';
"use strict";
const views = require('../util/views.js');
const views = require("../util/views.js");
const KEY_TAB = 9;
const KEY_RETURN = 13;
@ -10,14 +10,14 @@ const KEY_UP = 38;
const KEY_DOWN = 40;
function _getSelectionStart(input) {
if ('selectionStart' in input) {
if ("selectionStart" in input) {
return input.selectionStart;
}
if (document.selection) {
input.focus();
const sel = document.selection.createRange();
const selLen = document.selection.createRange().text.length;
sel.moveStart('character', -input.value.length);
sel.moveStart("character", -input.value.length);
return sel.text.length - selLen;
}
return 0;
@ -27,18 +27,22 @@ class AutoCompleteControl {
constructor(sourceInputNode, options) {
this._sourceInputNode = sourceInputNode;
this._options = {};
Object.assign(this._options, {
verticalShift: 2,
maxResults: 15,
getTextToFind: () => {
const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, '');
Object.assign(
this._options,
{
verticalShift: 2,
maxResults: 15,
getTextToFind: () => {
const value = sourceInputNode.value;
const start = _getSelectionStart(sourceInputNode);
return value.substring(0, start).replace(/.*\s+/, "");
},
confirm: null,
delete: null,
getMatches: null,
},
confirm: null,
delete: null,
getMatches: null,
}, options);
options
);
this._showTimeout = null;
this._results = [];
@ -49,22 +53,25 @@ class AutoCompleteControl {
hide() {
window.clearTimeout(this._showTimeout);
this._suggestionDiv.style.display = 'none';
this._suggestionDiv.style.display = "none";
this._isVisible = false;
}
replaceSelectedText(result, addSpace) {
const start = _getSelectionStart(this._sourceInputNode);
let prefix = '';
let prefix = "";
let suffix = this._sourceInputNode.value.substring(start);
let middle = this._sourceInputNode.value.substring(0, start);
const index = middle.lastIndexOf(' ');
const spaceIndex = middle.lastIndexOf(" ");
const commaIndex = middle.lastIndexOf(",");
const index = spaceIndex < commaIndex ? commaIndex : spaceIndex;
const delimiter = spaceIndex < commaIndex ? "" : " ";
if (index !== -1) {
prefix = this._sourceInputNode.value.substring(0, index + 1);
middle = this._sourceInputNode.value.substring(index + 1);
}
this._sourceInputNode.value = (
prefix + result.toString() + ' ' + suffix.trimLeft());
this._sourceInputNode.value =
prefix + result.toString() + delimiter + suffix.trimLeft();
if (!addSpace) {
this._sourceInputNode.value = this._sourceInputNode.value.trim();
}
@ -86,7 +93,7 @@ class AutoCompleteControl {
}
_show() {
this._suggestionDiv.style.display = 'block';
this._suggestionDiv.style.display = "block";
this._isVisible = true;
}
@ -101,27 +108,32 @@ class AutoCompleteControl {
_install() {
if (!this._sourceInputNode) {
throw new Error('Input element was not found');
throw new Error("Input element was not found");
}
if (this._sourceInputNode.getAttribute('data-autocomplete')) {
if (this._sourceInputNode.getAttribute("data-autocomplete")) {
throw new Error(
'Autocompletion was already added for this element');
"Autocompletion was already added for this element"
);
}
this._sourceInputNode.setAttribute('data-autocomplete', true);
this._sourceInputNode.setAttribute('autocomplete', 'off');
this._sourceInputNode.setAttribute("data-autocomplete", true);
this._sourceInputNode.setAttribute("autocomplete", "off");
this._sourceInputNode.addEventListener(
'keydown', e => this._evtKeyDown(e));
this._sourceInputNode.addEventListener(
'blur', e => this._evtBlur(e));
this._sourceInputNode.addEventListener("keydown", (e) =>
this._evtKeyDown(e)
);
this._sourceInputNode.addEventListener("blur", (e) =>
this._evtBlur(e)
);
this._suggestionDiv = views.htmlToDom(
'<div class="autocomplete"><ul></ul></div>');
this._suggestionList = this._suggestionDiv.querySelector('ul');
'<div class="autocomplete"><ul></ul></div>'
);
this._suggestionList = this._suggestionDiv.querySelector("ul");
document.body.appendChild(this._suggestionDiv);
views.monitorNodeRemoval(
this._sourceInputNode, () => { this._uninstall(); });
views.monitorNodeRemoval(this._sourceInputNode, () => {
this._uninstall();
});
}
_uninstall() {
@ -137,13 +149,21 @@ class AutoCompleteControl {
if (key === KEY_ESCAPE) {
func = this.hide;
} else if (key === KEY_TAB && shift) {
func = () => { this._selectPrevious(); };
func = () => {
this._selectPrevious();
};
} else if (key === KEY_TAB && !shift) {
func = () => { this._selectNext(); };
func = () => {
this._selectNext();
};
} else if (key === KEY_UP) {
func = () => { this._selectPrevious(); };
func = () => {
this._selectPrevious();
};
} else if (key === KEY_DOWN) {
func = () => { this._selectNext(); };
func = () => {
this._selectNext();
};
} else if (key === KEY_RETURN && this._activeResult >= 0) {
func = () => {
this._confirm(this._getActiveSuggestion());
@ -164,14 +184,17 @@ class AutoCompleteControl {
func();
} else {
window.clearTimeout(this._showTimeout);
this._showTimeout = window.setTimeout(
() => { this._showOrHide(); }, 250);
this._showTimeout = window.setTimeout(() => {
this._showOrHide();
}, 250);
}
}
_evtBlur(e) {
window.clearTimeout(this._showTimeout);
window.setTimeout(() => { this.hide(); }, 50);
window.setTimeout(() => {
this.hide();
}, 50);
}
_getActiveSuggestion() {
@ -182,9 +205,11 @@ class AutoCompleteControl {
}
_selectPrevious() {
this._select(this._activeResult === -1 ?
this._results.length - 1 :
this._activeResult - 1);
this._select(
this._activeResult === -1
? this._results.length - 1
: this._activeResult - 1
);
}
_selectNext() {
@ -192,15 +217,18 @@ class AutoCompleteControl {
}
_select(newActiveResult) {
this._activeResult =
newActiveResult.between(0, this._results.length - 1, true) ?
newActiveResult :
-1;
this._activeResult = newActiveResult.between(
0,
this._results.length - 1,
true
)
? newActiveResult
: -1;
this._refreshActiveResult();
}
_updateResults(textToFind) {
this._options.getMatches(textToFind).then(matches => {
this._options.getMatches(textToFind).then((matches) => {
const oldResults = this._results.slice();
this._results = matches.slice(0, this._options.maxResults);
const oldResultsHash = JSON.stringify(oldResults);
@ -223,34 +251,30 @@ class AutoCompleteControl {
}
for (let [resultIndex, resultItem] of this._results.entries()) {
let resultIndexWorkaround = resultIndex;
const listItem = document.createElement('li');
const link = document.createElement('a');
const listItem = document.createElement("li");
const link = document.createElement("a");
link.innerHTML = resultItem.caption;
link.setAttribute('href', '');
link.setAttribute('data-key', resultItem.value);
link.addEventListener(
'mouseenter',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._refreshActiveResult();
});
link.addEventListener(
'mousedown',
e => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
this.hide();
});
link.setAttribute("href", "");
link.setAttribute("data-key", resultItem.value);
link.addEventListener("mouseenter", (e) => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._refreshActiveResult();
});
link.addEventListener("mousedown", (e) => {
e.preventDefault();
this._activeResult = resultIndexWorkaround;
this._confirm(this._getActiveSuggestion());
this.hide();
});
listItem.appendChild(link);
this._suggestionList.appendChild(listItem);
}
this._refreshActiveResult();
// display the suggestions offscreen to get the height
this._suggestionDiv.style.left = '-9999px';
this._suggestionDiv.style.top = '-9999px';
this._suggestionDiv.style.left = "-9999px";
this._suggestionDiv.style.top = "-9999px";
this._show();
const verticalShift = this._options.verticalShift;
const inputRect = this._sourceInputNode.getBoundingClientRect();
@ -264,38 +288,44 @@ class AutoCompleteControl {
inputRect.top + inputRect.height / 2 < viewPortHeight / 2 ? 1 : -1;
let x = inputRect.left - bodyRect.left;
let y = direction == 1 ?
inputRect.bottom - bodyRect.top - verticalShift :
inputRect.top - bodyRect.top - listRect.height + verticalShift;
let y =
direction === 1
? inputRect.bottom - bodyRect.top - verticalShift
: inputRect.top -
bodyRect.top -
listRect.height +
verticalShift;
// remove offscreen items until whole suggestion list can fit on the
// screen
while ((y < 0 || y + listRect.height > viewPortHeight) &&
this._suggestionList.childNodes.length) {
while (
(y < 0 || y + listRect.height > viewPortHeight) &&
this._suggestionList.childNodes.length
) {
this._suggestionList.removeChild(this._suggestionList.lastChild);
const prevHeight = listRect.height;
listRect = this._suggestionDiv.getBoundingClientRect();
const heightDelta = prevHeight - listRect.height;
if (direction == -1) {
if (direction === -1) {
y += heightDelta;
}
}
this._suggestionDiv.style.left = x + 'px';
this._suggestionDiv.style.top = y + 'px';
this._suggestionDiv.style.left = x + "px";
this._suggestionDiv.style.top = y + "px";
}
_refreshActiveResult() {
let activeItem = this._suggestionList.querySelector('li.active');
let activeItem = this._suggestionList.querySelector("li.active");
if (activeItem) {
activeItem.classList.remove('active');
activeItem.classList.remove("active");
}
if (this._activeResult >= 0) {
const allItems = this._suggestionList.querySelectorAll('li');
const allItems = this._suggestionList.querySelectorAll("li");
activeItem = allItems[this._activeResult];
activeItem.classList.add('active');
activeItem.classList.add("active");
}
}
};
}
module.exports = AutoCompleteControl;

View file

@ -1,12 +1,12 @@
'use strict';
"use strict";
const api = require('../api.js');
const misc = require('../util/misc.js');
const events = require('../events.js');
const views = require('../util/views.js');
const api = require("../api.js");
const misc = require("../util/misc.js");
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate('comment');
const scoreTemplate = views.getTemplate('score');
const template = views.getTemplate("comment");
const scoreTemplate = views.getTemplate("score");
class CommentControl extends events.EventTarget {
constructor(hostNode, comment, onlyEditing) {
@ -16,104 +16,111 @@ class CommentControl extends events.EventTarget {
this._onlyEditing = onlyEditing;
if (comment) {
comment.addEventListener(
'change', e => this._evtChange(e));
comment.addEventListener(
'changeScore', e => this._evtChangeScore(e));
comment.addEventListener("change", (e) => this._evtChange(e));
comment.addEventListener("changeScore", (e) =>
this._evtChangeScore(e)
);
}
const isLoggedIn = comment && api.isLoggedIn(comment.user);
const infix = isLoggedIn ? 'own' : 'any';
views.replaceContent(this._hostNode, template({
comment: comment,
user: comment ? comment.user : api.user,
canViewUsers: api.hasPrivilege('users:view'),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
onlyEditing: onlyEditing,
}));
const infix = isLoggedIn ? "own" : "any";
views.replaceContent(
this._hostNode,
template({
comment: comment,
user: comment ? comment.user : api.user,
canViewUsers: api.hasPrivilege("users:view"),
canEditComment: api.hasPrivilege(`comments:edit:${infix}`),
canDeleteComment: api.hasPrivilege(`comments:delete:${infix}`),
onlyEditing: onlyEditing,
})
);
if (this._editButtonNodes) {
for (let node of this._editButtonNodes) {
node.addEventListener('click', e => this._evtEditClick(e));
node.addEventListener("click", (e) => this._evtEditClick(e));
}
}
if (this._deleteButtonNode) {
this._deleteButtonNode.addEventListener(
'click', e => this._evtDeleteClick(e));
this._deleteButtonNode.addEventListener("click", (e) =>
this._evtDeleteClick(e)
);
}
if (this._previewEditingButtonNode) {
this._previewEditingButtonNode.addEventListener(
'click', e => this._evtPreviewEditingClick(e));
this._previewEditingButtonNode.addEventListener("click", (e) =>
this._evtPreviewEditingClick(e)
);
}
if (this._saveChangesButtonNode) {
this._saveChangesButtonNode.addEventListener(
'click', e => this._evtSaveChangesClick(e));
this._saveChangesButtonNode.addEventListener("click", (e) =>
this._evtSaveChangesClick(e)
);
}
if (this._cancelEditingButtonNode) {
this._cancelEditingButtonNode.addEventListener(
'click', e => this._evtCancelEditingClick(e));
this._cancelEditingButtonNode.addEventListener("click", (e) =>
this._evtCancelEditingClick(e)
);
}
this._installScore();
if (onlyEditing) {
this._selectNav('edit');
this._selectTab('edit');
this._selectNav("edit");
this._selectTab("edit");
} else {
this._selectNav('readonly');
this._selectTab('preview');
this._selectNav("readonly");
this._selectTab("preview");
}
}
get _formNode() {
return this._hostNode.querySelector('form');
return this._hostNode.querySelector("form");
}
get _scoreContainerNode() {
return this._hostNode.querySelector('.score-container');
return this._hostNode.querySelector(".score-container");
}
get _editButtonNodes() {
return this._hostNode.querySelectorAll('li.edit>a, a.edit');
return this._hostNode.querySelectorAll("li.edit>a, a.edit");
}
get _previewEditingButtonNode() {
return this._hostNode.querySelector('li.preview>a');
return this._hostNode.querySelector("li.preview>a");
}
get _deleteButtonNode() {
return this._hostNode.querySelector('.delete');
return this._hostNode.querySelector(".delete");
}
get _upvoteButtonNode() {
return this._hostNode.querySelector('.upvote');
return this._hostNode.querySelector(".upvote");
}
get _downvoteButtonNode() {
return this._hostNode.querySelector('.downvote');
return this._hostNode.querySelector(".downvote");
}
get _saveChangesButtonNode() {
return this._hostNode.querySelector('.save-changes');
return this._hostNode.querySelector(".save-changes");
}
get _cancelEditingButtonNode() {
return this._hostNode.querySelector('.cancel-editing');
return this._hostNode.querySelector(".cancel-editing");
}
get _textareaNode() {
return this._hostNode.querySelector('.tab.edit textarea');
return this._hostNode.querySelector(".tab.edit textarea");
}
get _contentNode() {
return this._hostNode.querySelector('.tab.preview .comment-content');
return this._hostNode.querySelector(".tab.preview .comment-content");
}
get _heightKeeperNode() {
return this._hostNode.querySelector('.keep-height');
return this._hostNode.querySelector(".keep-height");
}
_installScore() {
@ -122,32 +129,35 @@ class CommentControl extends events.EventTarget {
scoreTemplate({
score: this._comment ? this._comment.score : 0,
ownScore: this._comment ? this._comment.ownScore : 0,
canScore: api.hasPrivilege('comments:score'),
}));
canScore: api.hasPrivilege("comments:score"),
})
);
if (this._upvoteButtonNode) {
this._upvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, 1));
this._upvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, 1)
);
}
if (this._downvoteButtonNode) {
this._downvoteButtonNode.addEventListener(
'click', e => this._evtScoreClick(e, -1));
this._downvoteButtonNode.addEventListener("click", (e) =>
this._evtScoreClick(e, -1)
);
}
}
enterEditMode() {
this._selectNav('edit');
this._selectTab('edit');
this._selectNav("edit");
this._selectTab("edit");
}
exitEditMode() {
if (this._onlyEditing) {
this._selectNav('edit');
this._selectTab('edit');
this._setText('');
this._selectNav("edit");
this._selectTab("edit");
this._setText("");
} else {
this._selectNav('readonly');
this._selectTab('preview');
this._selectNav("readonly");
this._selectTab("preview");
this._setText(this._comment.text);
}
this._forgetHeight();
@ -173,27 +183,31 @@ class CommentControl extends events.EventTarget {
_evtScoreClick(e, score) {
e.preventDefault();
if (!api.hasPrivilege('comments:score')) {
if (!api.hasPrivilege("comments:score")) {
return;
}
this.dispatchEvent(new CustomEvent('score', {
detail: {
comment: this._comment,
score: this._comment.ownScore === score ? 0 : score,
},
}));
this.dispatchEvent(
new CustomEvent("score", {
detail: {
comment: this._comment,
score: this._comment.ownScore === score ? 0 : score,
},
})
);
}
_evtDeleteClick(e) {
e.preventDefault();
if (!window.confirm('Are you sure you want to delete this comment?')) {
if (!window.confirm("Are you sure you want to delete this comment?")) {
return;
}
this.dispatchEvent(new CustomEvent('delete', {
detail: {
comment: this._comment,
},
}));
this.dispatchEvent(
new CustomEvent("delete", {
detail: {
comment: this._comment,
},
})
);
}
_evtChange(e) {
@ -206,26 +220,24 @@ class CommentControl extends events.EventTarget {
_evtPreviewEditingClick(e) {
e.preventDefault();
this._contentNode.innerHTML =
misc.formatMarkdown(this._textareaNode.value);
this._selectTab('edit');
this._selectTab('preview');
}
_evtEditClick(e) {
e.preventDefault();
this.enterEditMode();
this._contentNode.innerHTML = misc.formatMarkdown(
this._textareaNode.value
);
this._selectTab("edit");
this._selectTab("preview");
}
_evtSaveChangesClick(e) {
e.preventDefault();
this.dispatchEvent(new CustomEvent('submit', {
detail: {
target: this,
comment: this._comment,
text: this._textareaNode.value,
},
}));
this.dispatchEvent(
new CustomEvent("submit", {
detail: {
target: this,
comment: this._comment,
text: this._textareaNode.value,
},
})
);
}
_evtCancelEditingClick(e) {
@ -239,27 +251,27 @@ class CommentControl extends events.EventTarget {
}
_selectNav(modeName) {
for (let node of this._hostNode.querySelectorAll('nav')) {
node.classList.toggle('active', node.classList.contains(modeName));
for (let node of this._hostNode.querySelectorAll("nav")) {
node.classList.toggle("active", node.classList.contains(modeName));
}
}
_selectTab(tabName) {
this._ensureHeight();
for (let node of this._hostNode.querySelectorAll('.tab, .tabs li')) {
node.classList.toggle('active', node.classList.contains(tabName));
for (let node of this._hostNode.querySelectorAll(".tab, .tabs li")) {
node.classList.toggle("active", node.classList.contains(tabName));
}
}
_ensureHeight() {
this._heightKeeperNode.style.minHeight =
this._heightKeeperNode.getBoundingClientRect().height + 'px';
this._heightKeeperNode.getBoundingClientRect().height + "px";
}
_forgetHeight() {
this._heightKeeperNode.style.minHeight = null;
}
};
}
module.exports = CommentControl;

View file

@ -1,10 +1,10 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const CommentControl = require('../controls/comment_control.js');
const events = require("../events.js");
const views = require("../util/views.js");
const CommentControl = require("../controls/comment_control.js");
const template = views.getTemplate('comment-list');
const template = views.getTemplate("comment-list");
class CommentListControl extends events.EventTarget {
constructor(hostNode, comments, reversed) {
@ -13,8 +13,8 @@ class CommentListControl extends events.EventTarget {
this._comments = comments;
this._commentIdToNode = {};
comments.addEventListener('add', e => this._evtAdd(e));
comments.addEventListener('remove', e => this._evtRemove(e));
comments.addEventListener("add", (e) => this._evtAdd(e));
comments.addEventListener("remove", (e) => this._evtRemove(e));
views.replaceContent(this._hostNode, template());
@ -28,16 +28,19 @@ class CommentListControl extends events.EventTarget {
}
get _commentListNode() {
return this._hostNode.querySelector('ul');
return this._hostNode.querySelector("ul");
}
_installCommentNode(comment) {
const commentListItemNode = document.createElement('li');
const commentListItemNode = document.createElement("li");
const commentControl = new CommentControl(
commentListItemNode, comment, false);
events.proxyEvent(commentControl, this, 'submit');
events.proxyEvent(commentControl, this, 'score');
events.proxyEvent(commentControl, this, 'delete');
commentListItemNode,
comment,
false
);
events.proxyEvent(commentControl, this, "submit");
events.proxyEvent(commentControl, this, "score");
events.proxyEvent(commentControl, this, "delete");
this._commentIdToNode[comment.id] = commentListItemNode;
this._commentListNode.appendChild(commentListItemNode);
}
@ -54,6 +57,6 @@ class CommentListControl extends events.EventTarget {
_evtRemove(e) {
this._uninstallCommentNode(e.detail.comment);
}
};
}
module.exports = CommentListControl;

View file

@ -1,26 +1,28 @@
'use strict';
"use strict";
const ICON_CLASS_OPENED = 'fa-chevron-down';
const ICON_CLASS_CLOSED = 'fa-chevron-up';
const ICON_CLASS_OPENED = "fa-chevron-down";
const ICON_CLASS_CLOSED = "fa-chevron-up";
const views = require('../util/views.js');
const views = require("../util/views.js");
const template = views.getTemplate('expander');
const template = views.getTemplate("expander");
class ExpanderControl {
constructor(name, title, nodes) {
this._name = name;
nodes = Array.from(nodes).filter(n => n);
nodes = Array.from(nodes).filter((n) => n);
if (!nodes.length) {
return;
}
const expanderNode = template({title: title});
const toggleLinkNode = expanderNode.querySelector('a');
const toggleIconNode = expanderNode.querySelector('i');
const expanderContentNode = expanderNode.querySelector('div');
toggleLinkNode.addEventListener('click', e => this._evtToggleClick(e));
const expanderNode = template({ title: title });
const toggleLinkNode = expanderNode.querySelector("a");
const toggleIconNode = expanderNode.querySelector("i");
const expanderContentNode = expanderNode.querySelector("div");
toggleLinkNode.addEventListener("click", (e) =>
this._evtToggleClick(e)
);
nodes[0].parentNode.insertBefore(expanderNode, nodes[0]);
@ -32,28 +34,29 @@ class ExpanderControl {
this._toggleIconNode = toggleIconNode;
expanderNode.classList.toggle(
'collapsed',
this._allStates[this._name] === undefined ?
false :
!this._allStates[this._name]);
"collapsed",
this._allStates[this._name] === undefined
? false
: !this._allStates[this._name]
);
this._syncIcon();
}
// eslint-disable-next-line accessor-pairs
set title(newTitle) {
if (this._expanderNode) {
this._expanderNode
.querySelector('header span')
.textContent = newTitle;
this._expanderNode.querySelector("header span").textContent =
newTitle;
}
}
get _isOpened() {
return !this._expanderNode.classList.contains('collapsed');
return !this._expanderNode.classList.contains("collapsed");
}
get _allStates() {
try {
return JSON.parse(localStorage.getItem('expander')) || {};
return JSON.parse(localStorage.getItem("expander")) || {};
} catch (e) {
return {};
}
@ -62,12 +65,12 @@ class ExpanderControl {
_save() {
const newStates = Object.assign({}, this._allStates);
newStates[this._name] = this._isOpened;
localStorage.setItem('expander', JSON.stringify(newStates));
localStorage.setItem("expander", JSON.stringify(newStates));
}
_evtToggleClick(e) {
e.preventDefault();
this._expanderNode.classList.toggle('collapsed');
this._expanderNode.classList.toggle("collapsed");
this._save();
this._syncIcon();
}

View file

@ -1,9 +1,9 @@
'use strict';
"use strict";
const events = require('../events.js');
const views = require('../util/views.js');
const events = require("../events.js");
const views = require("../util/views.js");
const template = views.getTemplate('file-dropper');
const template = views.getTemplate("file-dropper");
const KEY_RETURN = 13;
@ -17,37 +17,53 @@ class FileDropperControl extends events.EventTarget {
allowMultiple: options.allowMultiple,
allowUrls: options.allowUrls,
lock: options.lock,
id: 'file-' + Math.random().toString(36).substring(7),
id: "file-" + Math.random().toString(36).substring(7),
urlPlaceholder:
options.urlPlaceholder || 'Alternatively, paste an URL here.',
options.urlPlaceholder || "Alternatively, paste an URL here.",
});
this._dropperNode = source.querySelector('.file-dropper');
this._urlInputNode = source.querySelector('input[type=text]');
this._urlConfirmButtonNode = source.querySelector('button');
this._fileInputNode = source.querySelector('input[type=file]');
this._fileInputNode.style.display = 'none';
this._dropperNode = source.querySelector(".file-dropper");
this._urlInputNode = source.querySelector("input[type=text]");
this._urlConfirmButtonNode = source.querySelector("button");
this._fileInputNode = source.querySelector("input[type=file]");
this._fileInputNode.style.display = "none";
this._fileInputNode.multiple = options.allowMultiple || false;
this._counter = 0;
this._dropperNode.addEventListener(
'dragenter', e => this._evtDragEnter(e));
this._dropperNode.addEventListener(
'dragleave', e => this._evtDragLeave(e));
this._dropperNode.addEventListener(
'dragover', e => this._evtDragOver(e));
this._dropperNode.addEventListener(
'drop', e => this._evtDrop(e));
this._fileInputNode.addEventListener(
'change', e => this._evtFileChange(e));
this._dropperNode.addEventListener("dragenter", (e) =>
this._evtDragEnter(e)
);
this._dropperNode.addEventListener("dragleave", (e) =>
this._evtDragLeave(e)
);
this._dropperNode.addEventListener("dragover", (e) =>
this._evtDragOver(e)
);
this._dropperNode.addEventListener("drop", (e) => this._evtDrop(e));
this._fileInputNode.addEventListener("change", (e) =>
this._evtFileChange(e)
);
if (this._urlInputNode) {
this._urlInputNode.addEventListener(
'keydown', e => this._evtUrlInputKeyDown(e));
this._urlInputNode.addEventListener("keydown", (e) =>
this._evtUrlInputKeyDown(e)
);
this._urlInputNode.addEventListener("paste", (e) => {
// document.onpaste is used on the post-upload page.
// And this event is used on the post edit page.
if (document.getElementById("post-upload")) return;
this._evtPaste(e)
});
}
if (this._urlConfirmButtonNode) {
this._urlConfirmButtonNode.addEventListener(
'click', e => this._evtUrlConfirmButtonClick(e));
this._urlConfirmButtonNode.addEventListener("click", (e) =>
this._evtUrlConfirmButtonClick(e)
);
}
document.onpaste = (e) => {
if (!document.getElementById("post-upload")) return;
this._evtPaste(e)
}
this._originalHtml = this._dropperNode.innerHTML;
@ -56,24 +72,27 @@ class FileDropperControl extends events.EventTarget {
reset() {
this._dropperNode.innerHTML = this._originalHtml;
this.dispatchEvent(new CustomEvent('reset'));
this.dispatchEvent(new CustomEvent("reset"));
}
_emitFiles(files) {
files = Array.from(files);
if (this._options.lock) {
this._dropperNode.innerText =
files.map(file => file.name).join(', ');
this._dropperNode.innerText = files
.map((file) => file.name)
.join(", ");
}
this.dispatchEvent(
new CustomEvent('fileadd', {detail: {files: files}}));
new CustomEvent("fileadd", { detail: { files: files } })
);
}
_emitUrls(urls) {
urls = Array.from(urls).map(url => url.trim());
urls = Array.from(urls).map((url) => url.trim());
if (this._options.lock) {
this._dropperNode.innerText =
urls.map(url => url.split(/\//).reverse()[0]).join(', ');
this._dropperNode.innerText = urls
.map((url) => url.split(/\//).reverse()[0])
.join(", ");
}
for (let url of urls) {
if (!url) {
@ -84,18 +103,20 @@ class FileDropperControl extends events.EventTarget {
return;
}
}
this.dispatchEvent(new CustomEvent('urladd', {detail: {urls: urls}}));
this.dispatchEvent(
new CustomEvent("urladd", { detail: { urls: urls } })
);
}
_evtDragEnter(e) {
this._dropperNode.classList.add('active');
this._dropperNode.classList.add("active");
this._counter++;
}
_evtDragLeave(e) {
this._counter--;
if (this._counter === 0) {
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
}
}
@ -109,31 +130,42 @@ class FileDropperControl extends events.EventTarget {
_evtDrop(e) {
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
if (!e.dataTransfer.files.length) {
window.alert('Only files are supported.');
window.alert("Only files are supported.");
}
if (!this._options.allowMultiple && e.dataTransfer.files.length > 1) {
window.alert('Cannot select multiple files.');
window.alert("Cannot select multiple files.");
}
this._emitFiles(e.dataTransfer.files);
}
_evtPaste(e) {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
const fileList = Array.from(items).map((x) => x.getAsFile()).filter(f => f);
if (!this._options.allowMultiple && fileList.length > 1) {
window.alert("Cannot select multiple files.");
} else if (fileList.length > 0) {
this._emitFiles(fileList);
}
}
_evtUrlInputKeyDown(e) {
if (e.which !== KEY_RETURN) {
return;
}
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = '';
this._urlInputNode.value = "";
}
_evtUrlConfirmButtonClick(e) {
e.preventDefault();
this._dropperNode.classList.remove('active');
this._dropperNode.classList.remove("active");
this._emitUrls(this._urlInputNode.value.split(/[\r\n]/));
this._urlInputNode.value = '';
this._urlInputNode.value = "";
}
}

View file

@ -0,0 +1,59 @@
"use strict";
const misc = require("../util/misc.js");
const PoolList = require("../models/pool_list.js");
const AutoCompleteControl = require("./auto_complete_control.js");
function _poolListToMatches(pools, options) {
return [...pools]
.sort((pool1, pool2) => {
return pool2.postCount - pool1.postCount;
})
.map((pool) => {
let cssName = misc.makeCssName(pool.category, "pool");
const caption =
'<span class="' +
cssName +
'">' +
misc.escapeHtml(pool.names[0] + " (" + pool.postCount + ")") +
"</span>";
return {
caption: caption,
value: pool,
};
});
}
class PoolAutoCompleteControl extends AutoCompleteControl {
constructor(input, options) {
const minLengthForPartialSearch = 3;
options.getMatches = (text) => {
const term = misc.escapeSearchTerm(text);
const query =
(text.length < minLengthForPartialSearch
? term + "*"
: "*" + term + "*") + " sort:post-count";
return new Promise((resolve, reject) => {
PoolList.search(query, 0, this._options.maxResults, [
"id",
"names",
"category",
"postCount",
"version",
]).then(
(response) =>
resolve(
_poolListToMatches(response.results, this._options)
),
reject
);
});
};
super(input, options);
}
}
module.exports = PoolAutoCompleteControl;

View file

@ -0,0 +1,195 @@
"use strict";
const api = require("../api.js");
const pools = require("../pools.js");
const misc = require("../util/misc.js");
const uri = require("../util/uri.js");
const Pool = require("../models/pool.js");
const settings = require("../models/settings.js");
const events = require("../events.js");
const views = require("../util/views.js");
const PoolAutoCompleteControl = require("./pool_auto_complete_control.js");
const KEY_SPACE = 32;
const KEY_RETURN = 13;
const SOURCE_INIT = "init";
const SOURCE_IMPLICATION = "implication";
const SOURCE_USER_INPUT = "user-input";
const SOURCE_CLIPBOARD = "clipboard";
const template = views.getTemplate("pool-input");
function _fadeOutListItemNodeStatus(listItemNode) {
if (listItemNode.classList.length) {
if (listItemNode.fadeTimeout) {
window.clearTimeout(listItemNode.fadeTimeout);
}
listItemNode.fadeTimeout = window.setTimeout(() => {
while (listItemNode.classList.length) {
listItemNode.classList.remove(listItemNode.classList.item(0));
}
listItemNode.fadeTimeout = null;
}, 2500);
}
}
class PoolInputControl extends events.EventTarget {
constructor(hostNode, poolList) {
super();
this.pools = poolList;
this._hostNode = hostNode;
this._poolToListItemNode = new Map();
// dom
const editAreaNode = template();
this._editAreaNode = editAreaNode;
this._poolInputNode = editAreaNode.querySelector("input");
this._poolListNode = editAreaNode.querySelector("ul.compact-pools");
this._autoCompleteControl = new PoolAutoCompleteControl(
this._poolInputNode,
{
getTextToFind: () => {
return this._poolInputNode.value;
},
confirm: (pool) => {
this._poolInputNode.value = "";
this.addPool(pool, SOURCE_USER_INPUT);
},
delete: (pool) => {
this._poolInputNode.value = "";
this.deletePool(pool);
},
verticalShift: -2,
}
);
// show
this._hostNode.style.display = "none";
this._hostNode.parentNode.insertBefore(
this._editAreaNode,
hostNode.nextSibling
);
// add existing pools
for (let pool of [...this.pools]) {
const listItemNode = this._createListItemNode(pool);
this._poolListNode.appendChild(listItemNode);
}
}
addPool(pool, source) {
if (source !== SOURCE_INIT && this.pools.hasPoolId(pool.id)) {
return Promise.resolve();
}
this.pools.add(pool, false);
const listItemNode = this._createListItemNode(pool);
if (!pool.category) {
listItemNode.classList.add("new");
}
this._poolListNode.prependChild(listItemNode);
_fadeOutListItemNodeStatus(listItemNode);
this.dispatchEvent(
new CustomEvent("add", {
detail: { pool: pool, source: source },
})
);
this.dispatchEvent(new CustomEvent("change"));
return Promise.resolve();
}
deletePool(pool) {
if (!this.pools.hasPoolId(pool.id)) {
return;
}
this.pools.removeById(pool.id);
this._hideAutoComplete();
this._deleteListItemNode(pool);
this.dispatchEvent(
new CustomEvent("remove", {
detail: { pool: pool },
})
);
this.dispatchEvent(new CustomEvent("change"));
}
_createListItemNode(pool) {
const className = pool.category
? misc.makeCssName(pool.category, "pool")
: null;
const poolLinkNode = document.createElement("a");
if (className) {
poolLinkNode.classList.add(className);
}
poolLinkNode.setAttribute(
"href",
uri.formatClientLink("pool", pool.names[0])
);
const poolIconNode = document.createElement("i");
poolIconNode.classList.add("fa");
poolIconNode.classList.add("fa-pool");
poolLinkNode.appendChild(poolIconNode);
const searchLinkNode = document.createElement("a");
if (className) {
searchLinkNode.classList.add(className);
}
searchLinkNode.setAttribute(
"href",
uri.formatClientLink("posts", { query: "pool:" + pool.id })
);
searchLinkNode.textContent = pool.names[0] + " ";
const usagesNode = document.createElement("span");
usagesNode.classList.add("pool-usages");
usagesNode.setAttribute("data-pseudo-content", pool.postCount);
const removalLinkNode = document.createElement("a");
removalLinkNode.classList.add("remove-pool");
removalLinkNode.setAttribute("href", "");
removalLinkNode.setAttribute("data-pseudo-content", "×");
removalLinkNode.addEventListener("click", (e) => {
e.preventDefault();
this.deletePool(pool);
});
const listItemNode = document.createElement("li");
listItemNode.appendChild(removalLinkNode);
listItemNode.appendChild(poolLinkNode);
listItemNode.appendChild(searchLinkNode);
listItemNode.appendChild(usagesNode);
for (let name of pool.names) {
this._poolToListItemNode.set(name, listItemNode);
}
return listItemNode;
}
_deleteListItemNode(pool) {
const listItemNode = this._getListItemNode(pool);
if (listItemNode) {
listItemNode.parentNode.removeChild(listItemNode);
}
for (let name of pool.names) {
this._poolToListItemNode.delete(name);
}
}
_getListItemNode(pool) {
return this._poolToListItemNode.get(pool.names[0]);
}
_hideAutoComplete() {
this._autoCompleteControl.hide();
}
}
module.exports = PoolInputControl;

View file

@ -1,36 +1,38 @@
'use strict';
"use strict";
const settings = require('../models/settings.js');
const views = require('../util/views.js');
const optimizedResize = require('../util/optimized_resize.js');
const settings = require("../models/settings.js");
const views = require("../util/views.js");
const optimizedResize = require("../util/optimized_resize.js");
class PostContentControl {
constructor(hostNode, post, viewportSizeCalculator, fitFunctionOverride) {
this._post = post;
this._viewportSizeCalculator = viewportSizeCalculator;
this._hostNode = hostNode;
this._template = views.getTemplate('post-content');
this._template = views.getTemplate("post-content");
let fitMode = settings.get().fitMode;
if (typeof fitFunctionOverride !== 'undefined') {
if (typeof fitFunctionOverride !== "undefined") {
fitMode = fitFunctionOverride;
}
this._currentFitFunction = {
'fit-both': this.fitBoth,
'fit-original': this.fitOriginal,
'fit-width': this.fitWidth,
'fit-height': this.fitHeight,
}[fitMode] || this.fitBoth;
this._currentFitFunction =
{
"fit-both": this.fitBoth,
"fit-original": this.fitOriginal,
"fit-width": this.fitWidth,
"fit-height": this.fitHeight,
}[fitMode] || this.fitBoth;
this._install();
this._post.addEventListener(
'changeContent', e => this._evtPostContentChange(e));
this._post.addEventListener("changeContent", (e) =>
this._evtPostContentChange(e)
);
}
disableOverlay() {
this._hostNode.querySelector('.post-overlay').style.display = 'none';
this._hostNode.querySelector(".post-overlay").style.display = "none";
}
fitWidth() {
@ -92,22 +94,48 @@ class PostContentControl {
_resize(width, height) {
const resizeListenerNodes = [this._postContentNode].concat(
...this._postContentNode.querySelectorAll('.resize-listener'));
...this._postContentNode.querySelectorAll(".resize-listener")
);
for (let node of resizeListenerNodes) {
node.style.width = width + 'px';
node.style.height = height + 'px';
node.style.width = width + "px";
node.style.height = height + "px";
}
}
_refreshSize() {
if (window.innerWidth <= 800) {
const buttons = document.querySelector(".sidebar > .buttons");
if (buttons) {
const content = document.querySelector(".content");
content.insertBefore(buttons, content.querySelector(".post-container + *"));
const afterControls = document.querySelector(".content > .after-mobile-controls");
if (afterControls) {
afterControls.parentElement.parentElement.appendChild(afterControls);
}
}
} else {
const buttons = document.querySelector(".content > .buttons");
if (buttons) {
const sidebar = document.querySelector(".sidebar");
sidebar.insertBefore(buttons, sidebar.firstElementChild);
}
const afterControls = document.querySelector(".content + .after-mobile-controls");
if (afterControls) {
document.querySelector(".content").appendChild(afterControls);
}
}
this._currentFitFunction();
}
_install() {
this._reinstall();
optimizedResize.add(() => this._refreshSize());
views.monitorNodeRemoval(
this._hostNode, () => { this._uninstall(); });
views.monitorNodeRemoval(this._hostNode, () => {
this._uninstall();
});
}
_reinstall() {
@ -116,7 +144,7 @@ class PostContentControl {
autoplay: settings.get().autoplayVideos,
});
if (settings.get().transparencyGrid) {
newNode.classList.add('transparency-grid');
newNode.classList.add("transparency-grid");
}
if (this._postContentNode) {
this._hostNode.replaceChild(newNode, this._postContentNode);

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