Compare commits

...

489 commits
2.0 ... 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
48c9001194 server/docker: include setuptools in installation 2020-06-04 18:51:30 -04:00
Shyam Sunder
ea675d20cb server/docker: fix missing installation requirements
Furthermore, an update to Pillow has improved the floating-point
precision of the image hash algorithm, requiring minor updates to
the respective unit tests.

See https://github.com/python-pillow/Pillow/pull/4320
2020-06-04 16:38:26 -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
rr-
6a95a66f12 client/file-dropper: fix undefined variable 2020-04-20 18:31:15 +02:00
Shyam Sunder
deffe91fda client/css: orient image posts based on EXIF data
this uses the optionally implemented "image-orientation: from-image"
CSS property, and will not work on every browser.

see #311
2020-04-08 14:14:23 -04:00
Shyam Sunder
8c01c7714f client/css: prevent word-wrapping in source edit textarea 2020-04-07 21:56:30 -04:00
Shyam Sunder
377fe52072 server/posts/upload: refactor youtube-dl caller code to fix some bugs 2020-04-07 15:14:53 -04:00
Shyam Sunder
cd6683c2d8 server/posts/upload: make youtube-dl use best format
Fixes #313
2020-04-05 15:21:03 -04:00
Shyam Sunder
2c6434b08d server/posts/upload: limit filesize for uploads through youtube-dl
This will be controlled by the config parameter 'max_dl_filesize'.

TODO: In a future commit, the regular downloader should also respect
this parameter.
2020-04-03 15:32:25 -04:00
Shyam Sunder
99a69333e6 server/posts/upload: Add youtube-dl functionality
allows for video-based posts to be created by using youtube-dl
on the server. Access is controlled with the 'uploads:use_downloader'
permission.
2020-04-03 13:11:54 -04:00
Shyam Sunder
08e62ec885 client/posts: don't define flags on post upload 2020-04-01 21:01:20 -04:00
Shyam Sunder
65202189e1 server/posts/upload: edit default flag behavior
The 'loop' flag will be auto-selected by default on video posts if
the flags parameter is undefined when creating a new post.
2020-03-21 18:25:54 -04:00
neobooru
c60ec22b92 server/func/tags: allow tags to have longer names 2020-03-15 22:25:45 +01:00
dependabot[bot]
ed83e11552 build(deps): bump acorn from 5.7.1 to 5.7.4 in /client
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.1 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.1...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-15 11:50:37 -04:00
Shyam Sunder
db0c33bb14 server/func/image_hash: added docstrings to functions 2020-03-13 22:45:11 -04:00
Shyam Sunder
1a8de9ef3a all: purge remaining elasticsearch artifacts 2020-03-13 22:45:11 -04:00
Shyam Sunder
6cc2a91632 server/image_search: add migrations for elasticsearch removal 2020-03-13 22:45:11 -04:00
Shyam Sunder
bd9284b7f8 server/tests: update unit tests for elasticsearch removal 2020-03-13 22:45:11 -04:00
Shyam Sunder
4c78cf8c47 server/image_search: implement reverse search functionality in postgres
This will remove the dependency on the Elasticsearch database.

The search query is passed currently as raw SQL. Proper implementation
using SQLAlchemy will need custom ORM classed to be made.

Additional config parameter "allow_broken_uploads" has been added.
2020-03-13 22:45:11 -04:00
Shyam Sunder
a616cf6987 server/migrations: implement database connection timeout 2020-03-13 22:43:31 -04:00
Shyam Sunder
e3401b3993 server/config: gracefully handle bad config files 2020-03-13 13:17:41 -04:00
Shyam Sunder
0e6427d8bc server/tests: use postgresql test database 2020-03-06 18:15:25 -05:00
Shyam Sunder
e19d7041d1 all: updated gitignore 2020-03-06 10:29:03 -05:00
Shyam Sunder
f1a09c21d4 server/func/tag_categories: fixed deprecated SA function call 2020-03-06 10:29:03 -05:00
CorePoint
72e104b145
detect ftypiso5 as mp4 mime type 2020-02-07 12:10:38 +01:00
Shyam Sunder
af6eff9ff8 client/posts: allow for multiple source URLs to be entered and viewed 2020-01-26 17:49:04 -05:00
Shyam Sunder
0ff9f9d5a2 server/func/posts: explicity specify MD5 for post security hash 2020-01-12 12:54:28 -05:00
Shyam Sunder
dce7136f15 server/docker: update renamed dependency pyrfc3339 2020-01-12 12:29:25 -05:00
Shyam Sunder
978a384d9e server/tag-categories: order tag categories alphabetically when requested 2020-01-12 12:18:53 -05:00
Shyam Sunder
53ec25f4c4 client/post_view: Force inline playback for iOS
Fixes #295
2019-12-17 12:41:23 -05:00
Shyam Sunder
0a5279c2c1 docker: changed docker hub image location 2019-11-26 19:13:10 -05:00
Shyam Sunder
6f549cf2db client: update NPM lockfile
Merges #288 #290 #291
2019-11-03 19:54:33 -05:00
Shyam Sunder
80da6467f6 doc/install: update install instructions to remove build step 2019-10-25 12:48:22 -04:00
neobooru
eb49aea683 client/posts: remember offset when opening/closing bulk editor
Fixes rr-#274
Squashed with commit "client/posts: make prevQuery a const"
2019-10-25 11:10:56 -04:00
Shyam Sunder
4f5ea9c5ed server/facade: bump elasticsearch timeout to 2 minutes
Fixes #285
2019-10-15 13:02:24 -04:00
neobooru
73c53fa4e2 all: add support for webp images
Includes webp test image
Merges #283
2019-10-08 18:22:47 -04:00
Shyam Sunder
f4afb145d6 client/docker: fix missing build info 2019-10-04 20:46:37 -04:00
Shyam Sunder
9c04400369 docker: added OCI-compatible image labels
See https://github.com/opencontainers/image-spec/blob/master/annotations.md
2019-10-04 19:52:57 -04:00
Shyam Sunder
91f5a42459 docker: switch to DockerHub hosted builds in compose file 2019-09-30 22:12:53 -04:00
neobooru
c9eae00c8c client/login: always store login cookie as 'auth'
Fixes #268
2019-09-29 23:14:14 -04:00
neobooru
d2a4e50669 server/info: report correct size when filesystem is missing files
Merges PR #279
2019-09-29 23:07:53 -04:00
Shyam Sunder
4fe9c5f4ca server/docker: use Alpine-based image for space savings 2019-09-29 19:22:43 -04:00
Shyam Sunder
6da18036a4 client/docker: improved Dockerfile 2019-09-28 19:53:28 -04:00
Shyam Sunder
2af304b844 docker: add hooks to autotag images 2019-09-28 19:28:17 -04:00
Shyam Sunder
0c05330cfc server/tests: fix failing tests 2019-09-28 18:58:45 -04:00
Shyam Sunder
1231469a35 server/tests: integrate testing into Docker 2019-09-28 18:58:45 -04:00
Shyam Sunder
edf9083552 server/docker: improved Dockerfile 2019-09-27 23:15:34 -04:00
Shyam Sunder
dd56c287b5 server/facade: integrated elasticsearch wait into entrypoint 2019-09-21 14:22:07 -04:00
Shyam Sunder
fa3b6275b3 client/nginx: minor tweaks to nginx config 2019-09-16 08:36:56 -04:00
Shyam Sunder
54eab0aa35 server/image-hash: optionally allow for elasticsearch authentication 2019-09-15 16:50:47 -04:00
Shyam Sunder
734e28e014 server/tools: better documentation for file rename admin script 2019-09-04 17:58:26 -04:00
Shyam Sunder
369ddaf2f8 server/tools: add tool to change post filenames due to changed secret 2019-09-03 14:35:57 -04:00
Shyam Sunder
83442b4977 server/tools: created simple admin command script 2019-08-15 21:53:57 -04:00
Shyam Sunder
9df090b4d9 doc: simplified how to use the base URL feature 2019-08-14 07:57:56 -04:00
Shyam Sunder
48e7eb10f1 doc: moved documentation to a seperate folder 2019-08-05 19:20:41 -04:00
Shyam Sunder
69922fccb6 client/nginx: enable Cross-Origin Resource Sharing for API calls
Fixes #275
2019-08-05 17:11:20 -04:00
Shyam Sunder
9b02a0bd5e server/posts: allow for longer source URLs
Fixes #272
2019-07-27 19:24:39 -04:00
Shyam Sunder
979d8409d5 server/tools: add password reset script 2019-07-27 17:36:15 -04:00
Shyam Sunder
7a42c7a69b server/tools: add script to check audio flags for posts 2019-07-27 16:32:39 -04:00
Shyam Sunder
9329717335 server/docker: Rewrite how files are copied in Docker
This is in preperation of a future commit that will perform
the unit tests in a docker container
2019-07-27 14:34:58 -04:00
neobooru
0839dafd34 client/auth: call tags.refreshCategoryColorMap() after login
When the tag category list permission is not anonymous the category colors fail to load if you are not logged in, and because the page doesn't reload (SPA) the tag colors are still broken after logging in. Manually calling refreshCategoryColorMap after logging in solves this issue.
2019-07-24 16:42:37 +02:00
Shyam Sunder
9a9a475037 server/facade: Check mailer config on startup 2019-07-22 20:26:16 -04:00
neobooru
80d272d60b server/config: Add 'domain' and 'smtp from' config entries
Fixes #193 and #256

This however requires users to manually set the domain in the config.yaml.
This field currently is optional, but it would probably be better to make it required and not fall back to HTTP_ORIGIN and HTTP_REFERER, which might be inaccurate or not set (especially behind reverse proxies and the like)

server/config: Leave domain empty by default

Co-Authored-By: Shyam Sunder <sgsunder1@gmail.com>
2019-07-22 20:26:09 -04:00
neobooru
8f0835f27b client/tag_categories: load tag_categories after (attempted) login
Fixes #262
2019-07-22 19:58:16 -04:00
neobooru
b8699d59d2 client/upload: automatically set source when uploading from url
Fixes #230
2019-07-23 01:20:42 +02:00
Pika
2484aef492
docker: set ulimits for elasticsearch
this sets the ulimits for elasticsearch to the recommended value of 65536 to avoid errors on startup.
2019-07-09 16:27:17 -04:00
neobooru
e14f08ddc6 docs: Fix issue with tags which contain slashes
Apache wouldn't forward api calls which contain encoded slashes (%2F), which caused the api calls to tags with slashes (e.g. 'te/st') to fail with a 404 not found.

See:
- https://httpd.apache.org/docs/2.4/mod/core.html#AllowEncodedSlashes
- https://serverfault.com/questions/715242/encode-url-wihthin-url-apache-mod-proxy-proxypass/715902#715902
2019-05-28 17:10:01 -04:00
Shyam Sunder
e0fc790822 client/settings: Cache calls to settings.get() 2019-05-23 20:27:59 -04:00
neobooru
7b236b02c9 Add setting to display underscores as spaces in tags 2019-05-22 23:10:27 +02:00
Shyam Sunder
bbde0ab9a0
Merge pull request #260 from neobooru/docs-update
docs: Add apache configuration to manual install guide
2019-05-14 10:22:10 -04:00
neobooru
e471d6ad2e Add apache configuration to manual install guide 2019-05-14 16:13:25 +02:00
Shyam Sunder
765e1a711b docker: pin postgres version to 11
Closes #213. PostgreSQL v11's end of life is November 9th, 2023
2019-05-10 15:56:52 -04:00
Shyam Sunder
26127eaaf5 server/config: use safer YAML loader
Fixes #254
2019-04-27 18:08:47 -04:00
Shyam Sunder
4117f63375 server/model/posts: Make post flags a hybrid attribute in model
This should (hopefully) fix #250 and #252
2019-04-22 20:20:19 -04:00
Shyam Sunder
0121b952d1 client/nginx: Remove upload filesize restriction 2019-04-21 13:03:39 -04:00
Shyam Sunder
9edee46dcf client/docker: Added hook to display build info 2019-04-21 13:03:39 -04:00
Shyam Sunder
f36cdc8719 docs: Moved confusing requirements to INSTALL-OLD.md 2019-04-21 12:40:39 -04:00
Shyam Sunder
940631d3bb
Merge pull request #251 from Hunternif/elasticsearch6.3.1
server/build: Ensure elasticsearch library is a compatible version
2019-04-17 19:25:16 -04:00
Hunternif
9e7c77cd73 server/build: require elasticsearch >=5.0.0., <7.0.0. 2019-04-17 23:15:01 +07:00
ReAnzu
8e1e6af232 client/tag_categories: lowercase all color input on tag_categories 2019-04-08 23:50:20 +02:00
rr-
93910a1655 client/tags: fix post search links 2019-04-08 22:06:42 +02:00
Alec Armbruster
2ec6b978ac docs: add nginx reverse proxy documentation 2019-04-08 21:48:13 +02:00
ReAnzu
a4215e35dc client/post: Require the post to not be in edit mode. 2019-04-08 21:36:48 +02:00
ReAnzu
a48116aa05 client/post: Add swipe left and swipe right gestures to post content
client/post: Add swipe left and swipe right gestures to post content
2019-04-08 21:36:48 +02:00
Shyam Sunder
d69ef710b3 server/search: automatically add wildcards for source URL searching 2019-04-07 19:30:35 +02:00
Shyam Sunder
1d8cfd5a89 server/search: allow searching by source URL content 2019-04-07 19:30:35 +02:00
Skybbles // L5474
68bd168434 docs/install: fix typo 2019-04-05 16:31:48 +02:00
Shyam Sunder
b18acf3982 server/func/images: attempt to fix #225 2019-02-11 21:28:02 +01:00
Shyam Sunder
065a466af8 server/func/posts: fix #221 2019-02-11 21:28:02 +01:00
Marcin Kurczewski
03d768881e
Merge pull request #224 from sgsunder/post-view-icons
client/posts: Add some UI icons
2019-02-09 11:34:51 +01:00
Marcin Kurczewski
abc6e018b9
Merge pull request #223 from sgsunder/add-source-handling
client: Reimplement post source functionality
2019-02-09 02:40:35 +01:00
raku-cat
3e6b98df92 client: Reimplement post source functionality 2019-02-08 16:43:38 -05:00
Shyam Sunder
d7feb2792c client/posts: Add some UI icons 2019-02-05 10:56:51 -05:00
Marcin Kurczewski
2fdd8cb3ab
Merge pull request #222 from sgsunder/fix-transparancy-grid
Fix transparency grid for alternate base URIs
2019-02-05 16:15:04 +01:00
Shyam Sunder
a2dc964e52 client/posts: fix transparency grid for alternate base URIs 2019-02-05 09:26:41 -05:00
Joshua Avalon
6510d0750c client/posts: fix missing transparency grid 2019-01-21 07:26:20 +01:00
rr-
5ed70b2ec4 server/func/images: work around ffmpeg bug 6375 2019-01-09 21:15:58 +01:00
Shyam Sunder
14377933a7 server/func/posts: transfer flags on merge 2018-12-22 12:31:25 +01:00
Shyam Sunder
e80c482891 server/func/images: Fix Unicode Error 2018-12-22 12:31:25 +01:00
Shyam Sunder
987a3aa8f2 docker: make deployment easier 2018-12-22 12:31:25 +01:00
Shyam Sunder
7081b5be90 client/app: Fixed relative links in app manifest 2018-12-22 12:31:25 +01:00
Shyam Sunder
116919d2a2 client/public: Remove public/ folder and generate it on build 2018-12-22 12:31:25 +01:00
Shyam Sunder
a5a06bf2d1 client/build: Clean up build process
Fixes incorrect URIs of iOS splash screens and OpenSans font
Files get gzipped inside build script
Better nginx configuration
build.js uses more consistent, synchronous code
2018-12-22 12:31:25 +01:00
Robin Appelman
e6445b431f client/posts: fix absolute url on certain domains
Use the document base href to generate absolute url.
Otherwise the image link send to IQDB/google images will be invalid
2018-12-22 12:25:12 +01:00
rr-
d3cabc4a36 server: handle empty flags in migration 2018-09-24 11:40:11 +02:00
Shyam Sunder
8a10fc8ffd server/posts: automatically detect sound in video post uploads 2018-09-24 11:36:13 +02:00
Shyam Sunder
3879c2ec20 server/search: allow searching by post flags 2018-09-24 11:36:13 +02:00
Shyam Sunder
2235a72d2f server+client: added sound flag to video posts 2018-09-24 11:36:13 +02:00
Shyam Sunder
c8fe0fcdff client: Stop showing mp4 files as undefined 2018-09-13 07:33:48 +02:00
Shyam Sunder
cbf67587e2 client: Some minor fixups to base URL feature
* Cleanup cookie storage path
* Cleanup Data URL
2018-08-23 21:04:19 +02:00
Shyam Sunder
565027269c client/js/router.js: Reads <base> href tag 2018-08-23 21:04:19 +02:00
Shyam Sunder
defada45ab client: adapted code to use <base> HTML tag 2018-08-23 21:04:19 +02:00
Shyam Sunder
b29bf8b37a client: generate web app images in build script 2018-08-23 21:04:05 +02:00
Michael Serajnik
b22c887e4b client: add basic web app support 2018-08-06 14:12:29 +02:00
rr-
45b6df020a build: fix paths to config files 2018-08-04 13:19:02 +02:00
rr-
8da22cbd5e server: fix paths to config 2018-08-03 21:04:23 +02:00
Shyam Sunder
70385cfe3d docker: Clarify required version numbers.
Per #184
2018-07-28 17:36:09 +02:00
rr-
b1a20a7134 tests: fix failing tests
Regression caused by changing the way images are converted to grayscale
in 9730aa5c
2018-07-25 19:53:37 +02:00
Shyam Sunder
6a6c4dc822 build: add Docker functionality and documentation 2018-07-25 13:39:57 +02:00
Shyam Sunder
9730aa5c05 client: clean up required Python packages
* Packages that are only used in testing or development
have been moved to `dev-requirements.txt`
* Closes #178
* Minor rewrite to drop the `scikit-image` package, which
saves around 200MB in install size
2018-07-22 14:02:30 +02:00
rr-
1fe22a4d0a server/tag-categories: disallow uppercase colors 2018-07-08 10:10:06 +02:00
rr-
c9cb9aa539 server/password-reset: try to construct full URL 2018-07-08 10:10:06 +02:00
rr-
d85e746a65 server/tests: fix failing info api tests 2018-07-08 09:42:13 +02:00
rr-
b6a5be74cf config: fix camelCase 2018-07-08 09:38:41 +02:00
rr-
320c16743d docs/install: update test instructions 2018-07-08 09:36:21 +02:00
Michael Serajnik
d43758bcc2 client/build: replace uglify-es, update dependencies 2018-07-08 09:30:29 +02:00
Shyam Sunder
60ab9246c6 client: improved build.js, use relative links
* Removed unnecessary require('config.js') calls
* 'markdown.js' now uses rel. links in EntityPermalinkWrapper
* 'password_reset.py' now generates rel. links
* Removed 'Base URL' config parameter
* Removed 'API URL' config parameter
* 'build.js' no longer reads/requires config.yaml
* Updated documentation
* Removed unnecessary node packages used in 'build.js'

abandon api_url parameter
2018-07-06 19:40:20 +02:00
Shyam Sunder
3972b902d8 client: fetch configurations from server at runtime
Permissions, regex filters, app title, email info,
and safety now fetched using server's Info API
2018-06-27 21:20:03 +02:00
Nesswit
2bf361c64a client/posts: fix upload error caused by anonymous node
Anonymous node does not exist in view when a user without anonymous upload permission tries to post upload. So in this case we should check for the existence of anonymousNode first.
2018-05-21 21:41:23 +02:00
Michael Serajnik
d39439d549 client/posts: fix viewport height calculation on iOS 2018-05-01 22:26:17 +02:00
ReAnzu
2a69f0193f server/auth: add token authentication
* Users are only authenticated against their password on login,
  and to retrieve a token
* Passwords are wiped from the GUI frontend and cookies
  after login and token retrieval
* Tokens are revoked at the end of the session/logout
* If the user chooses the "remember me" option,
  the token is stored in the cookie
* Tokens correctly delete themselves on logout
* Tokens can expire at user-specified date
* Tokens have their last usage time
* Tokens can have user defined descriptions
* Users can manage login tokens in their account settings
2018-03-25 22:23:29 +02:00
rr-
e35e709927 docs/install: use example.com for example domain 2018-03-22 09:42:58 +01:00
Michael Serajnik
a98ca55391 client/css: optimize help view margins 2018-03-10 17:45:37 +01:00
Michael Serajnik
db9132432b client/css: add default margins 2018-03-10 17:45:37 +01:00
Michael Serajnik
23a28ce69c client/css: make tab navigations scrollable on smaller screens 2018-03-10 17:45:37 +01:00
Michael Serajnik
a962bb351a client/css: refine mobile sidebar styling 2018-03-10 17:45:37 +01:00
Michael Serajnik
a08c7d65da client/css: add scrollbar styling 2018-03-10 17:45:37 +01:00
Michael Serajnik
7596f9042c client/css: remove margin on empty post container 2018-03-10 17:45:37 +01:00
Michael Serajnik
9b10d2bebf client/css: add default font sizes for headings 2018-03-10 17:45:37 +01:00
Michael Serajnik
e15dffa1dc client/css: change container paddings to be viewport size independent 2018-03-10 17:45:37 +01:00
Michael Serajnik
4ce29cf222 client/css: change font size declarations to em 2018-03-10 17:45:37 +01:00
Michael Serajnik
26a1451ff6 client/css: improve mobile styling 2018-03-10 17:45:37 +01:00
ReAnzu
c770ad8f28 client/posts: fix copy tags list of string values error #153 2018-03-09 07:53:54 +01:00
ReAnzu
3f52aceca4 server/users: harden password hashes
- Changed password setup to use libsodium and argon2id (regular SHA256
  hashing for passwords is inadequate as modern GPU's can hash generate
  billions of hashes per second).
- Added code to auto migrate old passwords to the new password_hash if
  the existing password_hash matches either of the legacy password
  generation schemes (SHA1 or SHA256).
- Added migration to support new password_hash format length
- Added column password_revision. This field will default to 0, which
  all passwords will have till they're updated. After that each password
  hash method has a revision.
2018-03-08 23:40:47 +01:00
ReAnzu
7519e071e7 server/posts: deleting a post purges its artifacts
Specifically, its thumbnail and post source.
2018-03-08 23:37:37 +01:00
ReAnzu
12ec43f098 server/posts: auto convert GIFs to WEBMs/MP4s
- Default setting is false for both conversions, as this will require
  additional resources of the server, but is bandwidth friendly for
  viewers
- WEBM conversion is slow, but better quality than MP4 conversion with
  a typically smaller file size
- Tags are copied over from the original upload
- Snapshots are generated for the new auto posts
2018-03-08 07:48:45 +01:00
ReAnzu
4ff8be6a2f server/posts: ignore ffmpeg warnings
Poorly formatted MP4 and WEBM sources can cause ffmpeg to throw a lot
of warnings. However when there is byte ouptut, the generated thumbnail
is valid. Add a bypass for the resize_fill function to allow ffmpeg to
error.
2018-03-08 07:48:44 +01:00
ReAnzu
4b3529272e server/users: let administrators add new users
* Added functionality for administrators to directly add users to the
  application
* Added permission users:create:any to handle level that users are
  allowed to create other users
* Moved old permission users:create to users:create:self
2018-03-07 21:30:24 +01:00
rr-
a1fbeb91a0 server/users: fix checking passwords with colons 2018-02-10 14:04:02 +01:00
rr-
59d8b0d4c5 client: update dependencies 2018-01-06 21:35:53 +01:00
Michael Serajnik
69421464f6 client/posts: override resize mode in home view 2017-12-15 19:11:39 +00:00
Michael Serajnik
85cb3d4702 client/help: fix spelling issues 2017-12-02 23:38:22 +01:00
rr-
f8c7375b01 server/tags: allow uppercase tag category colors
i.e. colors such as "#FF0000"
2017-10-08 21:38:38 +02:00
rr-
cdf454818c client: widen search inputs to match post search 2017-10-02 21:08:13 +02:00
rr-
4848bee5e3 client/tags: remove unused cruft 2017-10-01 22:09:00 +02:00
rr-
36698cddc2 client/posts: fix promise chaining 2017-10-01 22:00:42 +02:00
rr-
1c4c5c5f91 remove tags.json 2017-10-01 21:48:00 +02:00
Robin Appelman
253e28c1b5 client/posts: add shortcut for deleting posts 2017-09-23 20:05:57 +02:00
Robin Appelman
6d78c5e55d client/posts: fix keyboard nav to next/prev post
The exact search query was discarded.
2017-09-23 16:10:03 +02:00
rr-
795891767e client/home: fix featured WEBMs being unclickable 2017-09-09 23:42:00 +02:00
rr-
234afc8dfe client: update dependencies 2017-08-25 23:54:29 +02:00
rr-
87735110aa client/posts: add copying notes to clipboard
Saves some frustration when losing changes due to editing conflict
2017-08-25 23:53:51 +02:00
rr-
674d6c35d7 server/posts: add posts:view:featured privilege 2017-08-24 17:17:09 +02:00
rr-
4afece8d50 server/posts: add non-guessable IDs to post URLs 2017-08-24 17:17:09 +02:00
Michael Serajnik
90b0d77147 client/build: fix build, use uglify-es package directly 2017-08-11 17:36:10 +02:00
rr-
043b182b5e client/paging: add cues for qutebrowser 2017-06-25 17:47:40 +02:00
rr-
3c138685ea server/images: handle resizing errors 2017-05-03 12:10:04 +02:00
rr-
a1b762c65f api: fix getting cached disk usage with empty dirs 2017-05-01 20:26:53 +02:00
rr-
4bc58a3c95 server: lint 2017-04-24 23:30:53 +02:00
rr-
fea9a94945 client/routing: fix certain history bug
The bug could be reproduced as follows:

1. Navigate to /posts
2. Search for "test"
3. Navigate to /posts again
4. Refresh the page

The user should see plain post list, but instead they were seeing the
"test" search results again as if step 3 never happened.
2017-04-24 23:02:25 +02:00
rr-
467b4a7630 server/tags: fix nondeterministic siblings order 2017-04-24 22:48:11 +02:00
rr-
8e5798ab8c server/tests: fix content sync tests on postgres 2017-04-24 22:36:41 +02:00
rr-
e4aa38f159 server/search: fix errors on negative page offsets 2017-04-24 22:12:12 +02:00
rr-
ba4df16499 server/search: add search term escaping 2017-04-24 21:59:38 +02:00
rr-
9814b132c3 server/search: fix searching for ---
Allow only one negation sign.
Also throw an error if user searches only for "-".
2017-04-24 19:55:02 +02:00
rr-
0014721053 server/tags: fix retrieving many tags 2017-04-19 14:44:54 +02:00
rr-
77bf3bdc3c client/posts: add option to disable safety ratings 2017-03-30 20:50:12 +02:00
rr-
c2be365b6e config: remove unused values 2017-03-30 19:48:48 +02:00
rr-
01e1641475 config: improve comments 2017-03-30 19:47:14 +02:00
rr-
7044d2aaee server/posts: ignore old elasticsearch results 2017-03-12 18:30:42 +01:00
rr-
49feb932f3 client/tags: merging can now also add aliases 2017-03-04 16:55:53 +01:00
rr-
5681fd11ef server/net: make the user-agent configurable
Fixes #127
2017-03-03 17:27:23 +01:00
rr-
e087b83082 client/notes: don't rely on class names
The state names, used by CSS, were being broken by the minifier.
2017-02-26 18:47:53 +01:00
rr-
87b3572ce5 client/paging: fix endless scroll on android 2017-02-26 12:57:24 +01:00
rr-
5467ca6b7e client/posts: improve placeholder in file dropper
The default one was too long to fit in the sidebar
2017-02-21 19:09:18 +01:00
rr-
d00d282bff client/posts: improve file dropper appearance 2017-02-21 19:00:02 +01:00
rr-
1e58899b03 client/posts: allow updating content from URL 2017-02-21 19:00:02 +01:00
rr-
b27855523a client/file-dropper: fix drawing long URLs 2017-02-21 18:59:12 +01:00
rr-
34366b72fb client/file-dropper: add ability to lock URLs 2017-02-21 18:59:12 +01:00
rr-
5dfdfd49e9 client/paging: fix loading on small page sizes
Fixes #126
2017-02-19 14:24:01 +01:00
rr-
33b49ebffd client/paging: fix mass tag double binding
Fixes #125
2017-02-19 14:23:58 +01:00
rr-
c01214e919 server/password-reset: support having no smtp 2017-02-17 23:10:51 +01:00
rr-
32d15a493c client/css: add margin to file dropper button 2017-02-12 10:41:49 +01:00
rr-
aa1f4d3ff8 client/posts: add file extensions info to upload 2017-02-12 10:40:50 +01:00
rr-
1caf76b1b2 client/posts: add bulk safety editing (#122) 2017-02-11 22:03:38 +01:00
rr-
0dc7a4058e client/posts: refactor bulk tag editor
Extract the state that controls mass tag form in the posts list header
to a separate class.

It's not exactly a 100% reusable control (the .tpl is shared), but it
should greatly simplify reading the JS.
2017-02-11 21:58:26 +01:00
rr-
0e4e994431 client: rename 'mass tag' to 'bulk edit tags'
That way other bulk operations will be easier to name.
This also changes the privilege name.
2017-02-11 19:50:22 +01:00
rr-
eda6d6d02a client/paging: support item removal (#123) 2017-02-09 22:40:02 +01:00
rr-
fdad08e176 server: use index-based paging (#123) 2017-02-09 22:40:00 +01:00
rr-
ba7ca0cd87 client/tags: use new color input (#119) 2017-02-07 21:34:53 +01:00
Alice Ryhl
a3b3532ca4 server/api: patch timing attack on password reset form 2017-02-07 20:29:37 +01:00
rr-
7f09306dde server/api: fix unicode urls (#121) 2017-02-07 18:03:35 +01:00
rr-
74c583f11d server/build: fix alembic environment script 2017-02-05 23:29:21 +01:00
rr-
72056e0cd2 server/requirements: fix skimage package name...
Brain fart during previous commit...
2017-02-05 23:27:59 +01:00
rr-
ee6b66329b server/posts: fix search by aspect ratio
It was being rounded to nearest integer because of the width/height
columns' data type.
2017-02-05 23:21:43 +01:00
rr-
49e5975254 server/model: use new sqlalchemy import style 2017-02-05 23:21:43 +01:00
rr-
f40a8875c4 server/files: fix import for Py3.5
os.DirEntry is available only from Python3.6+.
2017-02-05 22:38:55 +01:00
rr-
4caa980bf8 server/build: add missing dependency
Althought szurubooru is now no longer dependent from image-match, the
pulled code still needs the skimage library.
2017-02-05 22:38:05 +01:00
rr-
00c3a4320b server/posts: support aspect-ratio search query 2017-02-05 22:09:33 +01:00
rr-
0b21d98c9b server/posts: support note-text search query 2017-02-05 21:51:53 +01:00
rr-
1f14f2fc16 docs/api: add info about wildcards 2017-02-05 21:47:52 +01:00
rr-
6cc18be68d client/posts: fix editing post relations
Regression since e725f4f9
2017-02-05 16:54:11 +01:00
rr-
e725f4f99c server/api: extra validation of list fields 2017-02-05 16:34:45 +01:00
rr-
705967d0fb server/scripts: remove lint
Any configuration for pycodestyle should go to the new setup.cfg file.
2017-02-05 16:34:45 +01:00
rr-
350e9dd331 server/scripts: replace ./test with setup.cfg 2017-02-05 16:34:45 +01:00
rr-
e490080347 server/scripts: remove migration script
It was unmaintained for months (years?) anyway
2017-02-05 16:34:45 +01:00
rr-
ad842ee8a5 server: refactor + add type hinting
- Added type hinting (for now, 3.5-compatible)
- Split `db` namespace into `db` module and `model` namespace
- Changed elastic search to be created lazily for each operation
- Changed to class based approach in entity serialization to allow
  stronger typing
- Removed `required` argument from `context.get_*` family of functions;
  now it's implied if `default` argument is omitted
- Changed `unalias_dict` implementation to use less magic inputs
2017-02-05 16:34:45 +01:00
rr-
abf1fc2b2d server: make linters happier 2017-02-03 22:42:14 +01:00
rr-
fd30675124 server/image-hash: do not depend on image-match
While I hold this library in great esteem for its excellent work on
implementing the original paper, I have several problems with it:

- as of this commit, it (again) has bug fixes unreleased on pip
- its code is badly structured
    - forces OOP and then proceeds @staticmethod everything
    - bad class design, parameters are repeated in several places
    - terrible contract of make_record() and generate_signature()
    - ambiguous parameters: path vs. image path vs. image content
    - doesn't adhere to PEP-8
- depends on cairo just to render svg images almost no one uses this
  library with
2017-02-03 21:20:52 +01:00
rr-
894cd29511 server/tests: test image hash 2017-02-03 19:53:10 +01:00
rr-
b21ffac820 server/scripts: make pytest happier 2017-02-03 19:22:33 +01:00
rr-
f828c375e6 server/posts: fix reverse search late evaluation
Uploading webms caused 'Not an image.' error to be shown, cause
generators are evaluated lazily, so the `catch` never worked.
2017-02-02 21:52:52 +01:00
rr-
accdb51c0b server/migrations: add default tag category 2017-02-02 20:26:22 +01:00
rr-
f2fd769767 server/migrations: fix imports for alembic
`alembic revision -m 'blah blah'` rightfully complained about imports
(in case of `upgrade`, that module was being populated by some other
module.)
2017-02-02 20:06:20 +01:00
rr-
e92bd2fd80 server/tags: fix getting default category name
No categories? Should have thrown an error rather than returning None.
2017-02-02 20:04:09 +01:00
rr-
cce543e0b6 server/posts: commit reverse search population 2017-02-02 19:46:35 +01:00
rr-
af6c35ed6b server/rest: rollback session on query exception
Kills complaints from sqlalchemy when an error happens during
insertion/update hook.
2017-02-02 19:46:03 +01:00
rr-
07d0b43d4c server/posts: reduce warnings from sqlalchemy
...regarding empty IN() statements
2017-02-02 19:46:03 +01:00
rr-
8be0e731a7 server/facade: run without elasticsearch
...but don't let user upload any images until they fix their
configuration
2017-02-02 19:46:03 +01:00
rr-
ec9c70ba68 server/facade: disable elasticsearch logs
Errors are covered by new safety mechanisms in image hash.
2017-02-02 19:46:03 +01:00
rr-
aa1faa3ccb server/image-hash: improve exception handling 2017-02-02 19:46:03 +01:00
rr-
f42fbbdc56 server/images: support webm with multiple streams 2017-01-25 17:13:39 +01:00
rr-
0cfc9bcafd server/posts: fix handling corrupt files
In case of a ProcessingError, the image dimensions are set to None. But
after that, they are compared with 0, which resulted in a TypeError.
2017-01-25 17:11:05 +01:00
rr-
9b27e113b3 server/search: escape backslashes in search 2017-01-21 00:22:53 +01:00
rr-
783171729f server: remove unneeded waitress wrapper 2017-01-21 00:22:53 +01:00
rr-
2ab559c7e5 docs/install: describe how to run with gunicorn 2017-01-21 00:22:53 +01:00
rr-
e5f250260d server: make gunicorn friendly 2017-01-21 00:22:53 +01:00
rr-
6b42d787a7 server: fix problems with escaping 2017-01-21 00:22:53 +01:00
rr-
1acceb941d client: refactor linking and routing
Print all links through new uri.js component
Refactor the router to use more predictable parsing
Fix linking to entities with weird names (that contain slashes, + etc.)
2017-01-21 00:13:35 +01:00
rr-
6714f05b49 client/posts: remove bullets from post management 2017-01-21 00:13:35 +01:00
rr-
b0e60a340b client/home: centerize messages 2017-01-21 00:13:35 +01:00
rr-
7414d1f7a6 server/posts: fix getting posts around
Querying this undocumented API resulted in 500 ISE unless the client
asked only for the "id" field.
2017-01-20 22:17:26 +01:00
rr-
eead1560ee client: fix reporting errors in pager 2017-01-15 21:09:08 +01:00
rr-
8934b85c92 client/posts: fix skipping duplicate uploads 2017-01-15 14:58:29 +01:00
rr-
fb71b81c62 client/comments: fix top margin in block quotes 2017-01-10 17:32:12 +01:00
rr-
592d2a7dae client/posts: fix uploading posts from URLs 2017-01-08 23:52:20 +01:00
rr-
76eab79828 client: fix leftover code 2017-01-08 22:32:05 +01:00
rr-
5229ce5774 client/posts: fix videos being always looped
fixes #115
2017-01-08 22:29:05 +01:00
rr-
43198daba3 client/posts: wrap with big progress
fixes #114
2017-01-08 22:29:05 +01:00
rr-
e5f08b454c client/tags: fix list bullets in tag suggestions
fixes #113
2017-01-08 22:29:05 +01:00
rr-
8d8165a0d7 server/tags: fix order of aliases in export
fixes #112
2017-01-08 22:29:05 +01:00
rr-
a703195c6c client/posts: fix reordering uploads
fixes #111
2017-01-08 22:29:05 +01:00
rr-
133ed522da client/posts: fix dup finder for swf and webm
fixes #110
2017-01-08 22:28:50 +01:00
rr-
b366d8981c client/api: fix null reference error 2017-01-08 20:56:48 +01:00
rr-
ecf347ef6e client/api: handle expired uploads 2017-01-08 11:04:49 +01:00
rr-
cc969a808f client/posts: show ! in title for similar posts 2017-01-08 10:25:29 +01:00
rr-
cb8bb0f23b client/util: fix style 2017-01-08 10:25:29 +01:00
rr-
beb8d8091b client/api: better promise aborting 2017-01-08 10:25:29 +01:00
rr-
8a73f7e400 client: rework promise error handling 2017-01-08 10:25:29 +01:00
rr-
5c0765c30e client/build: remove extra printer
It kept hanging node. Fuck.
2017-01-08 10:25:29 +01:00
rr-
df663e7b35 client/build: ditch watch
This shit has been always triggering 150 times for every single changed
file; now it simply doesn't fucking work.
2017-01-08 10:25:29 +01:00
rr-
5bf3d5da44 client/api: use temporary upload api 2017-01-08 10:25:29 +01:00
rr-
be6f8d7f46 client/api: merge URL and Blob based file uploads 2017-01-08 10:25:29 +01:00
rr-
036fa9ee39 server/uploads: add file upload api 2017-01-08 10:25:29 +01:00
rr-
f00cc5f3fa client/posts: search for similar posts on upload 2017-01-08 02:26:26 +01:00
rr-
d1bb33ecf0 client/posts: tweak upload appearance and UX 2017-01-08 02:26:13 +01:00
rr-
4cb613a5c9 server/posts: change reverse image search API
Add exact duplicates search; refactor to use classes over dictionaries
2017-01-07 14:07:31 +01:00
rr-
04b820c730 client/comments: fix missing thumbnail margins 2017-01-07 00:00:00 +01:00
rr-
02d90cb5e8 client/comments: fix comment control tab margins 2017-01-04 23:41:27 +01:00
rr-
ac98b7d8e6 client/posts: fix merge could be used only once 2017-01-03 22:07:47 +01:00
rr-
58fabc6e36 client/merge: add search button 2017-01-03 21:58:32 +01:00
rr-
9edaaffec2 server/posts: fix post relations
Trying to relate post to itself resulted in 500 ISE.
2017-01-03 21:37:38 +01:00
rr-
627574a9c2 server: make pylint happier 2017-01-03 21:35:08 +01:00
rr-
902a0d3fe0 server/db: fix closing DB sessions
Certain exception scenarios led to small disasters. Moved database
session management directly to router, since it's that sensitive.
2017-01-03 21:29:48 +01:00
rr-
ef079121a9 server/rest: simplify error handling flow 2017-01-03 21:17:41 +01:00
rr-
4340b4d9b2 client/posts: fix resize modes on chrome 2017-01-03 20:14:27 +01:00
rr-
e2fcd08ce9 client/comments: fix header wrapping on chrome 2017-01-03 19:37:59 +01:00
rr-
42bf4b12a2 client/comments: fix 1px jumping on edit preview 2017-01-03 19:37:15 +01:00
rr-
4ecd05d8b2 client/comments: don't use flexbox 2017-01-03 19:35:53 +01:00
rr-
f301ca9a8a server/image-hash: fix handling invalid input 2016-12-26 19:03:04 +01:00
rr-
e8636a7775 docs/api: fix stupid wording 2016-12-26 15:00:16 +01:00
rr-
a7a5cc8180 server/posts: expose reverse image search 2016-12-26 15:00:16 +01:00
rr-
1a59a74d63 server/image-hash: add image search engine 2016-12-26 15:00:16 +01:00
rr-
b9fa64317d docs: specify expected Python version 2016-12-26 11:57:05 +01:00
rr-
5981b5a0da client/css: fix stacking uploads in upload form 2016-12-25 21:52:25 +01:00
rr-
fe0ba63f19 client/comments: rework comments appearance and UX 2016-12-25 21:49:39 +01:00
rr-
f0573be715 client/css: improve list margins in comments 2016-12-22 23:45:15 +01:00
rr-
cf24d63fa4 client/css: fix lists in comments css inheritance
Markdown lists in comments inherited some unwanted CSS rules. The fix is
to make the culprit rules apply to more specific elements.
2016-12-22 23:45:14 +01:00
rr-
40fa118cca client/settings: fix hint button placement 2016-12-22 23:45:14 +01:00
rr-
32d498c74b client/markdown: allow to specify image size 2016-12-22 23:41:43 +01:00
rr-
6bf5764c6c client/posts: fix adding loop flag to non videos 2016-11-27 22:05:12 +01:00
rr-
9ae2b6aa44 client/notes: fix notes being added twice
Slight issue with event listeners.
2016-11-21 18:11:30 +01:00
rr-
42666706d9 server/util: fix API queries for empty ?options 2016-11-20 16:02:45 +01:00
rr-
e21a31e72f client/posts: fix hiding notes on interaction
Fixes #108
2016-11-13 19:10:55 +01:00
rr-
81080da06f client/settings: add ability to autoplay videos 2016-11-11 23:14:51 +01:00
rr-
bf0342df71 client/views: refactor make(Non)VoidElement
Merge into one function
2016-11-11 23:08:50 +01:00
rr-
143a015473 client/posts: control over video loops on upload
Also loop videos by default
2016-11-11 22:35:58 +01:00
rr-
20a5a58734 client/markdown: recognize entity links 2016-11-11 21:52:07 +01:00
rr-
c0d484689b server: postpone circular dependency evaluation
Hopefully this improves importing with python 3.4
2016-11-07 19:28:54 +01:00
rr-
b44b2aef7e client/posts: fix mass tag case sensitivity
Mass tagging with `TAG` marked posts tagged with `tag` as untagged.
2016-10-27 17:54:11 +02:00
rr-
39973386c6 client/posts: fix editing post safety
Broken by 865c4f3b79
2016-10-23 19:49:40 +02:00
rr-
141c9fcdc9 server/tags: merge also tag relations 2016-10-22 18:02:50 +02:00
rr-
995cd4610d server: drop old style class declarations 2016-10-22 14:43:52 +02:00
rr-
f1445b9c24 client/posts: add post merging 2016-10-22 14:05:56 +02:00
rr-
8c0fa7f49e client/posts: fix post mgmt privilege checking 2016-10-22 14:03:34 +02:00
rr-
9aa59a228e client/css: align radioboxes to first line 2016-10-22 14:03:34 +02:00
rr-
e71718c50d server/posts: add replaceContent to post merging 2016-10-21 22:34:45 +02:00
rr-
9d6a0e0173 server/posts: add post merging 2016-10-21 21:48:38 +02:00
468 changed files with 40046 additions and 13486 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/

14
.gitignore vendored
View file

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

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

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

View file

@ -3,14 +3,16 @@
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 [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments
- Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](https://github.com/rr-/szurubooru/blob/master/API.md))
- Rich JSON REST API ([see documentation](doc/API.md))
- Token based authentication for clients
- Rich search system
- Rich privilege system
- Autocomplete in search and while editing tags
@ -18,20 +20,20 @@ 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
- Browser configurable endless paging
- Browser configurable backdrop grid for transparent images
## Requirements
## Installation
- Python
- Postgres
- FFmpeg
- node.js
It is recommended that you use Docker for deployment.
[See installation instructions.](doc/INSTALL.md)
[See installation instructions.](https://github.com/rr-/szurubooru/blob/master/INSTALL.md)
More installation resources, as well as related projects can be found on the
[GitHub project Wiki](https://github.com/rr-/szurubooru/wiki)
## Screenshots
@ -45,4 +47,4 @@ Post view:
## License
[GPLv3](https://github.com/rr-/szurubooru/blob/master/LICENSE.md).
[GPLv3](LICENSE.md).

View file

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

4
client/.dockerignore Normal file
View file

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

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

44
client/Dockerfile Normal file
View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -4,21 +4,21 @@ form
display: block
width: 20em
ul
.input
list-style-type: none
margin: 0 0 1em 0
margin: 0 0 2em 0
padding: 0
li
margin-top: 1.2em
label
display: block
padding: 0.3em 0
.input
margin-bottom: 2em
.input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
.input li:first-child
padding-top: 0
margin-top: 0
form:not(.horizontal)
.hint
margin-top: 0.2em
margin-bottom: 0
@ -26,18 +26,31 @@ form
font-size: 80%
line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal
display: inline-block
margin-bottom: 1em
.input, .buttons, ul
display: inline-block
vertical-align: middle
vertical-align: top
margin: 0
padding: 0
input
vertical-align: middle
vertical-align: top
.buttons
margin-right: 0.5em
@media (max-width: 1000px)
display: block
.input, .buttons, ul
display: block
margin-top: 0.5em
&:first-child
margin-top: 0
.buttons
margin-right: 0
@ -65,10 +78,9 @@ input[type=radio], input[type=checkbox]
.radio:before, .checkbox:before
transition: border-color 0.1s linear
position: absolute
top: 50%
left: 0
top: 0.15em
display: block
margin-top: -10px
width: 16px
height: 16px
background: $input-enabled-background-color
@ -79,10 +91,10 @@ input[type=radio], input[type=checkbox]
background: $main-color
transition: opacity 0.1s linear
position: absolute
top: 50%
left: 5px
top: 0.15em
margin-top: 5px
display: block
margin-top: -5px
width: 10px
height: 10px
border-radius: 50%
@ -92,10 +104,10 @@ input[type=radio], input[type=checkbox]
.checkbox:after
transition: opacity 0.1s linear
position: absolute
top: 50%
top: 0.15em
left: 6px
display: block
margin-top: -7px
margin-top: 3px
width: 5px
height: 9px
border-right: 3px solid $main-color
@ -129,6 +141,48 @@ input[type=checkbox]:focus + .checkbox:before
/*
* Date and time inputs
*/
input[type=date],
input[type=time]
vertical-align: top
font-family: 'Droid Sans', sans-serif
font-size: 100%
padding: 0.2em 0.3em
box-sizing: border-box
border: 2px solid $input-enabled-border-color
background: $input-enabled-background-color
color: $input-enabled-text-color
box-shadow: none /* :-moz-submit-invalid on FF */
transition: border-color 0.1s linear, background-color 0.1s linear
&:disabled
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
&:focus
border-color: $main-color
&[readonly]
border: 2px solid $input-disabled-border-color
background: $input-disabled-background-color
color: $input-disabled-text-color
.darktheme
input[type=date],
input[type=time]
border: 2px solid darken($input-enabled-border-color, 75%)
background: darken($input-enabled-background-color, 75%)
color: $input-enabled-text-color-darktheme
&:disabled
background: darken($input-disabled-background-color, 75%)
&[readonly]
background: darken($input-disabled-background-color, 75%)
/*
* Regular inputs
*/
@ -164,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,
@ -173,13 +242,25 @@ input:disabled
cursor: not-allowed
label.color
white-space: nowrap
position: relative
display: flex
input[type=text]
margin-right: 0.25em
width: auto
.preview
display: inline-block
text-align: center
pointer-events: none
input[type=color]
position: absolute
opacity: 0
padding: 0 0.5em
border: 2px solid black
&:after
content: 'A'
.background-preview
border-right: 0
color: transparent
.text-preview
border-left: 0
form.show-validation .input
input:invalid
@ -190,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
@ -202,10 +284,13 @@ input[type=submit]
cursor: pointer
font-size: 100%
padding: 0.2em 0.7em
border-radius: 0
border: 2px solid $button-enabled-background-color
background: $button-enabled-background-color
color: $button-enabled-text-color
outline: 0 /* something on Chrome */
-moz-appearance: none
-webkit-appearance: none
&:disabled
cursor: default
@ -234,25 +319,30 @@ input::-moz-focus-inner
* File dropper
*/
.file-dropper-holder
display: flex
flex-wrap: wrap
.file-dropper
display: block
width: 100%
background: $window-color
border: 3px dashed #eee
padding: 0.3em 0.5em
line-height: 140%
text-align: center
cursor: pointer
overflow: hidden
word-wrap: break-word
input
.url-holder
display: flex
margin-top: 0.5em
width: auto
input, button
min-width: 0 /* firefox being sassy */
width: auto !important /* don't inherit anything weird */
input
flex: 1
button
margin-top: 0.5em
width: 8em
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper
cursor: default
@ -263,8 +353,6 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active
border-color: $main-color
.autocomplete
position: absolute
z-index: 10
@ -289,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,12 +1,17 @@
@import colors
@import mixins
$active-tab-text-color = $text-color
$active-tab-text-color-darktheme = $text-color-darktheme
$inactive-tab-text-color = $inactive-link-color
$inactive-tab-text-color-darktheme = $inactive-link-color-darktheme
/* latin */
@font-face
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(/fonts/open_sans.woff2) format('woff2');
src: local('Open Sans'), local('OpenSans'), url(../fonts/open_sans.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
/* make <body> cover entire viewport */
@ -21,19 +26,32 @@ body
margin: 0
color: $text-color
font-family: 'Open Sans', sans-serif
font-size: 12pt
line-height: 18pt
font-size: 1em
line-height: 1.4
@media (max-width: 800px)
font-size: 10pt
line-height: 15pt
font-size: 0.875em
@media (max-width: 1200px)
font-size: 11pt
line-height: 16.5pt
font-size: 0.95em
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3
font-weight: normal
margin-bottom: 1em
h1
font-size: 2em
h2
font-size: 1.5em
p,
ol,
ul
margin: 1em 0
th
font-weight: normal
@ -53,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
@ -61,8 +84,10 @@ form .fa-question-circle-o
vertical-align: middle
#content-holder
padding: 1.5vw
padding: 1.5em
text-align: center
@media (max-width: 1000px)
padding: 1em
>.content-wrapper
box-sizing: border-box /* make max-width: 100% on this element include padding */
text-align: left
@ -70,9 +95,30 @@ form .fa-question-circle-o
margin: 0 auto
>*:first-child, form h1
margin-top: 0
nav.buttons
ul
display: block
max-width: 100%
white-space: nowrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
>.content-wrapper:not(.transparent)
background: $top-navigation-color
padding: 2vw
padding: 1.8em
@media (max-width: 1000px)
padding: 1.5em
.content,
.content .subcontent
>*:last-child
margin-bottom: 0
.darktheme #content-holder
>.content-wrapper:not(.transparent)
background: $top-navigation-color-darktheme
hr
border: 0
@ -80,6 +126,9 @@ hr
margin: 1em 0
padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav
ul
list-style-type: none
@ -125,6 +174,39 @@ nav
li
display: inline-block
float: left
a
padding: 0 1.5em
#mobile-navigation-toggle
display: none
width: 100%
padding: 0 1em
line-height: 2.3em
font-family: inherit
border: none
background: none
color: $active-tab-text-color
.site-name
display: block
float: left
max-width: 50vw
overflow: hidden
text-overflow: ellipsis
.toggle-icon
display: block
float: right
@media (max-width: 1000px)
text-align: left
li
display: none
float: none
a
display: block
padding: 0 1em
#mobile-navigation-toggle
display: block
&.opened
li
display: block
ul li[data-name=account],
ul li[data-name=register],
ul li[data-name=login],
@ -141,6 +223,26 @@ nav
margin-right: 0.6em
margin-left: calc(0.6em - 1.2em)
float: left
@media (max-width: 1000px)
display: none
.darktheme nav
&.buttons
ul
li:not(.active) a
color: $inactive-tab-text-color-darktheme
li:hover:not(.active) a
color: $active-tab-text-color-darktheme
li.active a
background: $active-tab-background-color-darktheme
color: $active-tab-text-color-darktheme
:focus
background: $focused-tab-background-color-darktheme
&#top-navigation
background: $top-navigation-color-darktheme
ul
#mobile-navigation-toggle
color: $text-color-darktheme
a .access-key
text-decoration: underline
@ -166,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
@ -176,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%
@ -194,6 +313,14 @@ a .access-key
margin-top: 0 !important
margin-bottom: 0 !important
.table-wrap
overflow-x: auto
&::-webkit-scrollbar
height: 6px
background-color: $scrollbar-bg-color
&::-webkit-scrollbar-thumb
background-color: $scrollbar-thumb-color
/* hack to prevent text from being copied */
[data-pseudo-content]:before {
content: attr(data-pseudo-content)

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@
.page
position: relative
.page-header
margin: 0.5em 0.5em 0.5em 0
margin: 0.5em 0
position: relative
&:before
display: block
@ -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,2 @@
#password-reset
max-width: 30em

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
@ -8,20 +16,14 @@
margin: 0 auto
position: relative
img, object, video, .post-overlay
.resize-listener
position: absolute
height: 100%
width: 100%
left: 0
right: 0
top: 0
bottom: 0
width: 100%
height: 100%
.post-overlay>*
position: absolute
left: 0
right: 0
top: 0
bottom: 0
width: 100%
height: 100%
img
image-orientation: from-image

View file

@ -0,0 +1,37 @@
#post
width: 100%
max-width: 40em
h1
margin-top: 0
form
width: 100%
.buttons i
margin-right: 0.5em
.post-merge
.left-post-container
width: 47%
float: left
.right-post-container
width: 47%
float: right
.post-mirror
margin-bottom: 1em
&:after
display: block
height: 1px
content: ' '
clear: both
.post-thumbnail .thumbnail
width: 100%
height: 9em
.target-post .thumbnail
margin-right: 0.35em
.target-post, .target-post-content
margin: 1em 0
header
margin-bottom: 1em
label
display: inline-block
margin-top: 2px
input[type=text]
width: 6em

View file

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

View file

@ -7,15 +7,15 @@
>.sidebar
margin-right: 1em
min-width: 20em
max-width: 20em
min-width: 21em
max-width: 21em
line-height: 160%
a:active
border: 0
outline: 0
nav.buttons
>.sidebar>nav.buttons, >.content nav.buttons
margin-top: 0
display: flex
flex-wrap: wrap
@ -27,29 +27,46 @@
padding: 0.3em 0
text-align: center
vertical-align: middle
transition: background 0.2s linear
transition: background 0.2s linear, box-shadow 0.2s linear
&:not(.inactive):hover
background: lighten($main-color, 90%)
i
font-size: 140%
text-align: center
@media (max-width: 800px)
margin-top: 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%
max-width: 0
margin-right: 0
>.content
order: 1
@ -102,7 +119,6 @@
h1
margin-bottom: 0.5em
.thumbnail
background-position: 50% 30%
width: 4em
height: 3em
li
@ -120,19 +136,34 @@
margin-bottom: 1em
.safety
&>label
width: 100%
.radio-wrapper
display: flex
flex-wrap: wrap
label:not(.radio)
width: 100%
.radio
.radio-wrapper label
flex-grow: 1
display: inline-block
.management
ul
list-style-type: none
margin: 0
padding: 0
li
margin: 0
padding: 0
label
.post-source
textarea
white-space: pre
overflow-wrap: normal
overflow-x: scroll
form
width: auto
label:not(.file-dropper)
margin-bottom: 0.3em
display: block

View file

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

@ -8,11 +8,16 @@ $snapshot-merged-background-color = #FEC
ul
margin: 0 auto
padding: 0
width: 100%
max-width: 35em
list-style-type: none
li
margin-bottom: 1em
&:last-child
margin-bottom: 0
.time
float: right
@ -26,19 +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%)
div.details
margin-bottom: 2em
.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

@ -2,7 +2,7 @@
.content-wrapper.tag-categories
width: 100%
max-width: 40em
max-width: 45em
table
border-spacing: 0
width: 100%
@ -11,13 +11,19 @@
td, th
padding: .4em
&.color
text-align: center
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

@ -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
@ -55,13 +55,14 @@ div.tag-input
padding: 0.2em 1em
margin: 0
ul
list-style-type: none
margin: 0
overflow-y: auto
overflow-x: none
max-height: 20em
padding: 0.5em 1em 0 1em
li:last-child
border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0)
border-bottom: 0.5em solid alpha($window-color, 0)
li
margin: 0
font-size: 90%
@ -85,9 +86,16 @@ 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-top: 0.5em
margin: 0.5em 0 0 0
padding: 0
li
margin: 0
width: 100%
@ -101,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
@ -123,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

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

View file

@ -21,19 +21,31 @@
.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
display: none
display: none !important
text-align: left
form
width: auto
input[name=search-text]
max-width: 15em
width: 25em
@media (max-width: 1000px)
width: 100%
.append
vertical-align: middle
font-size: 0.95em
color: $inactive-link-color
.darktheme .user-list-header
.append
color: $inactive-link-color-darktheme

View file

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

11
client/docker-start.sh Executable file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<ul>
<li><%- ctx.postCount %> posts</li><span class='sep'>
</span><li><%= ctx.makeFileSize(ctx.diskUsage) %></li><span class='sep'>
</span><li>Build <a class='version' href='https://github.com/rr-/szurubooru/commits/master'><%- ctx.version %></a> from <%= ctx.makeRelativeTime(ctx.buildDate) %></li><span class='sep'>
</span><% if (ctx.canListSnapshots) { %><li><a href='/history'>History</a></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,16 +2,31 @@
<html>
<head>
<meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'>
<title><!-- configured in the config file --></title>
<link href='/css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<meta name='theme-color' content='#24aadd'/>
<meta name='apple-mobile-web-app-capable' content='yes'/>
<meta name='apple-mobile-web-app-status-bar-style' content='black'/>
<meta name='msapplication-TileColor' content='#ffffff'/>
<meta name="msapplication-TileImage" content="/img/mstile-150x150.png">
<title>Loading...</title>
<!-- Base HTML Placeholder -->
<link href='css/app.min.css' rel='stylesheet' type='text/css'/>
<link href='css/vendor.min.css' rel='stylesheet' type='text/css'/>
<link rel='shortcut icon' type='image/png' href='img/favicon.png'/>
<link rel='apple-touch-icon' sizes='180x180' href='img/apple-touch-icon.png'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-640x1136.png' media='(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-750x1294.png' media='(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1242x2148.png' media='(device-width: 414px) and (device-height: 736px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1125x2436.png' media='(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1536x2048.png' media='(min-device-width: 768px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-1668x2224.png' media='(min-device-width: 834px) and (max-device-width: 834px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='apple-touch-startup-image' href='img/apple-touch-startup-image-2048x2732.png' media='(min-device-width: 1024px) and (max-device-width: 1024px) and (-webkit-min-device-pixel-ratio: 2) and (orientation: portrait)'/>
<link rel='manifest' href='manifest.json'/>
</head>
<body>
<div id='top-navigation-holder'></div>
<div id='content-holder'></div>
<script type='text/javascript' src='/js/vendor.min.js'></script>
<script type='text/javascript' src='/js/app.min.js'></script>
<script type='text/javascript' src='js/vendor.min.js'></script>
<script type='text/javascript' src='js/app.min.js'></script>
</body>
</html>

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

@ -4,9 +4,10 @@
<div class='messages'></div>
<% if (ctx.canEditPostSafety) { %>
<% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
<section class='safety'>
<label>Safety</label>
<div class='radio-wrapper'>
<%= ctx.makeRadio({
name: 'safety',
class: 'safety-safe',
@ -25,6 +26,7 @@
selectedValue: ctx.post.safety,
class: 'safety-unsafe',
text: 'Unsafe'}) %>
</div>
</section>
<% } %>
@ -48,14 +50,32 @@
name: 'loop',
checked: ctx.post.flags.includes('loop'),
}) %>
<%= ctx.makeCheckbox({
text: 'Sound',
name: 'sound',
checked: ctx.post.flags.includes('sound'),
}) %>
</section>
<% } %>
<% if (ctx.canEditPostSource) { %>
<section class='post-source'>
<%= ctx.makeTextarea({
text: 'Source',
value: ctx.post.source,
}) %>
</section>
<% } %>
<% if (ctx.canEditPostTags) { %>
<section class='tags'>
<%= ctx.makeTextInput({
value: ctx.post.tags.join(' '),
}) %>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section>
<% } %>
@ -64,6 +84,12 @@
<a href class='add'>Add a note</a>
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
<a href class='delete inactive'>Delete selected note</a>
<% if (ctx.hasClipboard) { %>
<br/>
<a href class='copy'>Export notes to clipboard</a>
<br/>
<a href class='paste'>Import notes from clipboard</a>
<% } %>
</section>
<% } %>
@ -82,12 +108,15 @@
</section>
<% } %>
<% if (ctx.canFeaturePosts) { %>
<% if (ctx.canFeaturePosts || ctx.canDeletePosts || ctx.canMergePosts) { %>
<section class='management'>
<ul>
<% if (ctx.canFeaturePosts) { %>
<li><a href class='feature'>Feature this post on main page</a></li>
<% } %>
<% if (ctx.canMergePosts) { %>
<li><a href class='merge'>Merge this post with another</a></li>
<% } %>
<% if (ctx.canDeletePosts) { %>
<li><a href class='delete'>Delete this post</a></li>
<% } %>

View file

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

View file

@ -0,0 +1,23 @@
<div class='post-merge'>
<form>
<ul class='input'>
<li class='post-mirror'>
<div class='left-post-container'></div>
<div class='right-post-container'></div>
</li>
<li>
<p>Tags, relations, scores, favorites and comments will be
merged. All other properties need to be handled manually.</p>
<%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge these posts.'}) %>
</li>
</ul>
<div class='messages'></div>
<div class='buttons'>
<input type='submit' value='Merge posts'/>
</div>
</form>
</div>

View file

@ -0,0 +1,59 @@
<header>
<label for='merge-id-<%- ctx.name %>'>Post #</label>
<% if (ctx.editable) { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/>
<input type='button' value='Search'/>
<% } else { %>
<input type='text' id='merge-id-<%-ctx.name %>' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/>
<% } %>
</header>
<% if (ctx.post) { %>
<div class='post-thumbnail'>
<a rel='external' href='<%- ctx.post.contentUrl %>'>
<%= ctx.makeThumbnail(ctx.post.thumbnailUrl) %>
</a>
</div>
<div class='target-post'>
<%= ctx.makeRadio({
required: true,
text: 'Merge to this post<br/><small>' +
ctx.makeUserLink(ctx.post.user) +
', ' +
ctx.makeRelativeTime(ctx.post.creationTime) +
'</small>',
name: 'target-post',
value: ctx.name,
}) %>
</div>
<div class='target-post-content'>
<%= ctx.makeRadio({
required: true,
text: 'Use this file<br/><small>' +
ctx.makeFileSize(ctx.post.fileSize) + ' ' +
{
'image/gif': 'GIF',
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] +
' (' +
(ctx.post.canvasWidth ?
`${ctx.post.canvasWidth}x${ctx.post.canvasHeight}` :
'?') +
')</small>',
name: 'target-post-content',
value: ctx.name,
}) %>
<p>
</p>
</div>
<% } %>

View file

@ -8,11 +8,22 @@
'image/gif': 'GIF',
'image/jpeg': 'JPEG',
'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] %>
</a>
}[ctx.post.mimeType] %><!--
--></a>
(<%- ctx.post.canvasWidth %>x<%- ctx.post.canvasHeight %>)
<% if (ctx.post.flags.length) { %><!--
--><% if (ctx.post.flags.includes('loop')) { %><i class='fa fa-repeat'></i><% } %><!--
--><% if (ctx.post.flags.includes('sound')) { %><i class='fa fa-volume-up'></i><% } %>
<% } %>
</section>
<section class='upload-info'>
@ -20,10 +31,12 @@
<%= ctx.makeRelativeTime(ctx.post.creationTime) %>
</section>
<% if (ctx.enableSafety) { %>
<section class='safety'>
<i class='fa fa-circle safety-<%- ctx.post.safety %>'></i><!--
--><%- ctx.post.safety[0].toUpperCase() + ctx.post.safety.slice(1) %>
</section>
<% } %>
<section class='zoom'>
<a href class='fit-original'>Original zoom</a> &middot;
@ -32,10 +45,20 @@
<a href class='fit-both'>both</a>
</section>
<% if (ctx.post.source) { %>
<section class='source'>
Source: <% for (let i = 0; i < ctx.post.sourceSplit.length; i++) { %>
<% if (i != 0) { %>&middot;<% } %>
<a href='<%- ctx.post.sourceSplit[i] %>' title='<%- ctx.post.sourceSplit[i] %>'><%- ctx.extractRootDomain(ctx.post.sourceSplit[i]) %></a>
<% } %>
</section>
<% } %>
<section class='search'>
Search on
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>IQDB</a> &middot;
<a href='https://www.google.com/searchbyimage?&image_url=<%- encodeURIComponent(ctx.post.contentUrl) %>'>Google Images</a>
<a href='http://iqdb.org/?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>IQDB</a> &middot;
<a href='https://danbooru.donmai.us/posts?tags=md5:<%- ctx.post.checksumMD5 %>'>Danbooru</a> &middot;
<a href='https://lens.google.com/uploadbyurl?url=<%- encodeURIComponent(ctx.post.fullContentUrl) %>'>Google Images</a>
</section>
<section class='social'>
@ -67,20 +90,20 @@
--><% for (let tag of ctx.post.tags) { %><!--
--><li><!--
--><% if (ctx.canViewTags) { %><!--
--><a href='/tag/<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><a href='<%- ctx.formatClientLink('tag', tag.names[0]) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><i class='fa fa-tag'></i><!--
--><% } %><!--
--><% if (ctx.canViewTags) { %><!--
--></a><!--
--><% } %><!--
--><% if (ctx.canListPosts) { %><!--
--><a href='/posts/query=<%- encodeURIComponent(tag) %>' class='<%= ctx.makeCssName(ctx.getTagCategory(tag), 'tag') %>'><!--
--><a href='<%- ctx.formatClientLink('posts', {query: ctx.escapeTagName(tag.names[0])}) %>' class='<%= ctx.makeCssName(tag.category, 'tag') %>'><!--
--><% } %><!--
--><%- tag %>&#32;<!--
--><%- ctx.getPrettyName(tag.names[0]) %><!--
--><% if (ctx.canListPosts) { %><!--
--></a><!--
--><% } %><!--
--><span class='tag-usages' data-pseudo-content='<%- ctx.getTagUsages(tag) %>'></span><!--
--><% } %>&#32;<!--
--><span class='tag-usages' data-pseudo-content='<%- tag.postCount %>'></span><!--
--></li><!--
--><% } %><!--
--></ul>

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

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

View file

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

View file

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

View file

@ -5,11 +5,10 @@
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
text: 'Enable keyboard shortcuts',
text: "Enable keyboard shortcuts <a class='append icon' href='" + ctx.formatClientLink('help', 'keyboard') + "'><i class='fa fa-question-circle-o'></i></a>",
name: 'keyboard-shortcuts',
checked: ctx.browsingSettings.keyboardShortcuts,
}) %>
<a class='append icon' href='/help/keyboard'><i class='fa fa-question-circle-o'></i></a>
</li>
<li>
@ -23,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',
@ -39,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',
@ -56,6 +73,23 @@
}) %>
<p class='hint'>Shows a popup with suggested tags in edit forms.</p>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Automatically play video posts',
name: 'autoplay-videos',
checked: ctx.browsingSettings.autoplayVideos,
}) %>
</li>
<li>
<%= ctx.makeCheckbox({
text: 'Display underscores as spaces',
name: 'underscores-as-spaces',
checked: ctx.browsingSettings.tagUnderscoresAsSpaces,
}) %>
<p class='hint'>Display all underscores as if they were spaces. This is only a visual change, which means that you'll still have to use underscores when searching or editing tags.</p>
</li>
</ul>
<div class='messages'></div>

View file

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

View file

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

View file

@ -1,17 +1,20 @@
<div class='content-wrapper tag-categories'>
<form>
<h1>Tag categories</h1>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class='name'>Category name</th>
<th class='color'>CSS color</th>
<th class='order'>Order</th>
<th class='usages'>Usages</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<% if (ctx.canCreate) { %>
<p><a href class='add'>Add new category</a></p>
@ -19,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,9 +17,16 @@
<%- 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='/tags/query=category:<%- encodeURIComponent(ctx.tagCategory.name) %>'>
<a href='<%- ctx.formatClientLink('tags', {query: 'category:' + ctx.tagCategory.name}) %>'>
<%- ctx.tagCategory.tagCount %>
</a>
<% } else { %>

View file

@ -1,8 +1,8 @@
<div class='tag-delete'>
<form>
<p>This tag has <a href='/posts/query=<%- encodeURIComponent(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>
<ul class='input'>
<li>
<%= ctx.makeCheckbox({
name: 'confirm-deletion',

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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