Compare commits

..

481 commits
2.1 ... 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
466 changed files with 39040 additions and 13550 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 config.yaml
.env
# Client Development Artifacts
*/*_modules/ */*_modules/
client/public
# Server Development Artifacts
.coverage .coverage
.cache .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, Szurubooru is an image board engine inspired by services such as Danbooru,
Gelbooru and Moebooru dedicated for small and medium communities. Its name [has Gelbooru and Moebooru dedicated for small and medium communities. Its name [has
its roots in Polish language and has onomatopeic meaning of scraping or its roots in Polish language and has onomatopeic meaning of scraping or
scrubbing](http://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*. scrubbing](https://sjp.pwn.pl/sjp/;2527372). It is pronounced as *shoorubooru*.
## Features ## Features
- Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations - Post content: images (JPG, PNG, GIF, animated GIF), videos (MP4, WEBM), Flash animations
- Ability to retrieve web video content using [yt-dlp](https://github.com/yt-dlp/yt-dlp)
- Post comments - Post comments
- Post notes / annotations, including arbitrary polygons - Post notes / annotations, including arbitrary polygons
- Rich JSON REST API ([see documentation](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 search system
- Rich privilege system - Rich privilege system
- Autocomplete in search and while editing tags - 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 suggestions
- Tag implications (adding a tag automatically adds another) - Tag implications (adding a tag automatically adds another)
- Tag aliases - Tag aliases
- Pools and pool categories
- Duplicate detection - Duplicate detection
- Post rating and favoriting; comment rating - Post rating and favoriting; comment rating
- Polished UI - Polished UI
- Browser configurable endless paging - Browser configurable endless paging
- Browser configurable backdrop grid for transparent images - Browser configurable backdrop grid for transparent images
## Requirements ## Installation
- Python It is recommended that you use Docker for deployment.
- Postgres [See installation instructions.](doc/INSTALL.md)
- FFmpeg
- node.js
[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 ## Screenshots
@ -45,4 +47,4 @@ Post view:
## License ## 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"

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

@ -1,234 +1,424 @@
#!/usr/bin/env node
'use strict'; '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 fs = require('fs');
const glob = require('glob'); const glob = require('glob');
const path = require('path'); const path = require('path');
const util = require('util'); const util = require('util');
const execSync = require('child_process').execSync; const execSync = require('child_process').execSync;
const camelcase = require('camelcase'); const browserify = require('browserify');
const chokidar = require('chokidar');
function convertKeysToCamelCase(input) { const WebSocket = require('ws');
let result = {}; var PrettyError = require('pretty-error');
Object.keys(input).map((key, _) => { var pe = new PrettyError();
const value = input[key];
if (value !== null && value.constructor == Object) {
result[camelcase(key)] = convertKeysToCamelCase(value);
} else {
result[camelcase(key)] = value;
}
});
return result;
}
function readTextFile(path) { function readTextFile(path) {
return fs.readFileSync(path, 'utf-8'); return fs.readFileSync(path, 'utf-8');
} }
function writeFile(path, content) { function gzipFile(file) {
return fs.writeFileSync(path, content); file = path.normalize(file);
execSync('gzip -6 -k ' + file);
} }
function getVersion() { function baseUrl() {
return execSync('git describe --always --dirty --long --tags') return process.env.BASE_URL ? process.env.BASE_URL : '/';
.toString()
.trim();
} }
function getConfig() { // -------------------------------------------------
const yaml = require('js-yaml');
const merge = require('merge');
const camelcaseKeys = require('camelcase-keys');
function parseConfigFile(path) { function bundleHtml() {
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 minifyHtml(html) {
return require('html-minifier').minify(html, {
removeComments: true,
collapseWhitespace: true,
conservativeCollapse: true,
}).trim();
}
function bundleHtml(config) {
const underscore = require('underscore'); const underscore = require('underscore');
const babelify = require('babelify'); 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));
glob('./html/**/*.tpl', {}, (er, files) => { function minifyHtml(html) {
let compiledTemplateJs = '\'use strict\'\n'; return require('html-minifier').minify(html, {
compiledTemplateJs += 'let _ = require(\'underscore\');'; removeComments: true,
compiledTemplateJs += 'let templates = {};'; collapseWhitespace: true,
for (const file of files) { conservativeCollapse: true,
const name = path.basename(file, '.tpl').replace(/_/g, '-'); }).trim();
const placeholders = []; }
let templateText = readTextFile(file, 'utf-8');
templateText = templateText.replace(
/<%.*?%>/ig,
(match) => {
const ret = '%%%TEMPLATE' + placeholders.length;
placeholders.push(match);
return ret;
});
templateText = minifyHtml(templateText);
templateText = templateText.replace(
/%%%TEMPLATE(\d+)/g,
(match, number) => { return placeholders[number]; });
const functionText = underscore.template( const baseHtml = readTextFile('./html/index.htm')
templateText, {variable: 'ctx'}).source; .replace('<!-- Base HTML Placeholder -->', `<base href="${baseUrl()}"/>`);
compiledTemplateJs += `templates['${name}'] = ${functionText};`; fs.writeFileSync('./public/index.htm', minifyHtml(baseHtml));
}
compiledTemplateJs += 'module.exports = templates;'; let compiledTemplateJs = [
writeFile('./js/.templates.autogen.js', compiledTemplateJs); `'use strict';`,
console.info('Bundled HTML'); `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);
templateText = templateText.replace(
/<%.*?%>/ig,
(match) => {
const ret = '%%%TEMPLATE' + placeholders.length;
placeholders.push(match);
return ret;
});
templateText = minifyHtml(templateText);
templateText = templateText.replace(
/%%%TEMPLATE(\d+)/g,
(match, number) => { return placeholders[number]; });
const functionText = underscore.template(
templateText, { variable: 'ctx' }).source;
compiledTemplateJs.push(`templates['${name}'] = ${functionText};`);
}
compiledTemplateJs.push('module.exports = templates;');
fs.writeFileSync('./js/.templates.autogen.js', compiledTemplateJs.join('\n'));
console.info('Bundled HTML');
} }
function bundleCss() { function bundleCss() {
const stylus = require('stylus'); 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) {
'./node_modules/font-awesome/css/font-awesome.min.css', return require('csso').minify(css).css;
'./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 writeJsBundle(b, path, message, compress) {
let outputFile = fs.createWriteStream(path);
b.bundle().pipe(outputFile);
outputFile.on('finish', function() {
if (compress) {
writeFile(path, minifyJs(path));
}
console.info(message);
});
} }
glob('./js/**/*.js', {}, (er, files) => { let css = '';
if (!process.argv.includes('--no-vendor-js')) { for (const file of glob.sync('./css/**/*.styl')) {
let b = browserify(); css += stylus.render(readTextFile(file), { filename: file });
for (let lib of external) { }
b.require(lib); fs.writeFileSync('./public/css/app.min.css', minifyCss(css));
} if (process.argv.includes('--gzip')) {
if (config.transpile) { gzipFile('./public/css/app.min.css');
b.add(require.resolve('babel-polyfill')); }
}
writeJsBundle(
b, './public/js/vendor.min.js', 'Bundled vendor JS', true);
}
if (!process.argv.includes('--no-app-js')) { fs.copyFileSync(
let outputFile = fs.createWriteStream('./public/js/app.min.js'); './node_modules/font-awesome/css/font-awesome.min.css',
let b = browserify({debug: config.debug}); './public/css/vendor.min.css');
if (config.transpile) { if (process.argv.includes('--gzip')) {
b = b.transform('babelify'); gzipFile('./public/css/vendor.min.css');
} }
writeJsBundle(
b.external(external).add(files), console.info('Bundled CSS');
'./public/js/app.min.js', }
'Bundled app JS',
!config.debug); function minifyJs(path) {
return require('terser').minify(
fs.readFileSync(path, 'utf-8'), { compress: { unused: false } }).code;
}
function writeJsBundle(b, path, compress, callback) {
let outputFile = fs.createWriteStream(path);
b.bundle().on('error', (e) => console.error(pe.render(e))).pipe(outputFile);
outputFile.on('finish', () => {
if (compress) {
fs.writeFileSync(path, minifyJs(path));
} }
callback();
}); });
} }
function bundleConfig(config) { function bundleVendorJs(compress) {
writeFile( let b = browserify();
'./js/.config.autogen.json', JSON.stringify(config)); for (let lib of external_js) {
glob('./node_modules/font-awesome/fonts/*.*', {}, (er, files) => { b.require(lib);
for (let file of files) { }
if (fs.lstatSync(file).isDirectory()) { if (!process.argv.includes('--no-transpile')) {
continue; b.add(require.resolve('babel-polyfill'));
} }
copyFile(file, path.join('./public/fonts/', path.basename(file))); 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 watchify = require('watchify');
let b = browserify({ debug: process.argv.includes('--debug') });
if (!process.argv.includes('--no-transpile')) {
b = b.transform('babelify');
}
b = b.external(external_js).add(glob.sync('./js/**/*.js'));
const compress = !process.argv.includes('--debug');
bundleAppJs(b, compress, () => { });
}
}
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';
}
}
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() { function bundleBinaryAssets() {
glob('./img/*.png', {}, (er, files) => { fs.copyFileSync('./img/favicon.png', './public/img/favicon.png');
for (let file of files) { console.info('Copied images');
copyFile(file, path.join('./public/img/', path.basename(file)));
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');
});
}
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);
}
}
}
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));
} }
}); });
}
process.on('uncaughtException', (error) => { bundleBinaryAssets();
const stack = error.stack; bundleWebAppFiles();
delete error.stack;
console.log(error);
console.log(stack);
});
const config = getConfig();
bundleConfig(config);
bundleBinaryAssets();
if (!process.argv.includes('--no-html')) {
bundleHtml(config);
}
if (!process.argv.includes('--no-css')) {
bundleCss(); 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();
} }
if (!process.argv.includes('--no-js')) {
bundleJs(config); // -------------------------------------------------
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();
}
if (!process.argv.includes('--no-css')) {
bundleCss();
}
if (!process.argv.includes('--no-js')) {
bundleJs();
}
} }

View file

@ -1,13 +1,17 @@
$main-color = #24AADD $main-color = #24AADD
$window-color = white $window-color = white
$window-color-darktheme = #1a1a1a
$top-navigation-color = #F5F5F5 $top-navigation-color = #F5F5F5
$top-navigation-color-darktheme = #333333
$text-color = #111 $text-color = #111
$text-color-darktheme = #e6e6e6
$inactive-link-color = #888 $inactive-link-color = #888
$inactive-link-color-darktheme = #cccccc
$line-color = #DDD $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) $active-tab-background-color = rgba(0, 0, 0, 0.06)
$focused-tab-background-color = rgba(0, 0, 0, 0.03) $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-border-color = #BDF
$message-info-background-color = #E3EFF9 $message-info-background-color = #E3EFF9
$message-error-border-color = #FCC $message-error-border-color = #FCC
@ -21,6 +25,7 @@ $input-good-background-color = #F5FFF5
$input-enabled-background-color = #FAFAFA $input-enabled-background-color = #FAFAFA
$input-enabled-border-color = #EEE $input-enabled-border-color = #EEE
$input-enabled-text-color = $text-color $input-enabled-text-color = $text-color
$input-enabled-text-color-darktheme = $text-color-darktheme
$input-disabled-background-color = #FAFAFA $input-disabled-background-color = #FAFAFA
$input-disabled-border-color = #EEE $input-disabled-border-color = #EEE
$input-disabled-text-color = #888 $input-disabled-text-color = #888
@ -35,7 +40,6 @@ $new-tag-background-color = #DFC
$new-tag-text-color = black $new-tag-text-color = black
$implied-tag-background-color = #FFC $implied-tag-background-color = #FFC
$implied-tag-text-color = black $implied-tag-text-color = black
$tag-suggestions-background-color = $window-color
$tag-suggestions-header-color = #EEE $tag-suggestions-header-color = #EEE
$tag-suggestions-border-color = #AAA $tag-suggestions-border-color = #AAA
$duplicate-tag-background-color = #FDC $duplicate-tag-background-color = #FDC
@ -55,3 +59,6 @@ $hovered-first-note-point-color = red
$safety-safe = #88D488 $safety-safe = #88D488
$safety-sketchy = #F3D75F $safety-sketchy = #F3D75F
$safety-unsafe = #F3985F $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 @import colors
$comment-header-background-color = $top-navigation-color
$comment-header-background-color-darktheme = $top-navigation-color-darktheme
.comment-form-container $comment-border-color = #DDD
&: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 */
form .comment-container
width: auto padding: 0 0 0 60px
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
.avatar .avatar
margin-right: 1em float: left
-webkit-flex-shrink: 0 margin-left: -60px
flex-shrink: 0
vertical-align: top vertical-align: top
.thumbnail .thumbnail
@ -63,25 +18,72 @@
a a
display: inline-block display: inline-block
.body nav:not(.active), .tab:not(.active)
flex-grow: 1 display: none
.comment
border: 1px solid $comment-border-color
header header
white-space: nowrap white-space: nowrap
line-height: 16pt font-size: 95%
vertical-align: middle vertical-align: middle
margin-bottom: 0.5em position: relative
background: $top-navigation-color background: $comment-header-background-color
padding: 0.2em 0.5em 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
.nickname, .date, .score-container, .edit
margin-right: 2em
.date, .score-container, .edit, .delete
font-size: 95%
.edit, .delete, .score-container a, .nickname a .edit, .delete, .score-container a, .nickname a
&:not(.inactive) &:not(.inactive)
color: mix($main-color, $inactive-tab-text-color) color: mix($main-color, $inactive-link-color)
.edit, .delete
font-size: 80%
i i
margin-right: 0.3em margin-right: 0.3em
@ -96,21 +98,49 @@
display: inline-block display: inline-block
width: 2em width: 2em
.body
width: auto
margin: 1em
.keep-height
position: relative
textarea
position: absolute
width: 100%
height: 100%
.tab.edit
min-height: 150px
.messages .messages
margin: 1em 0 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 .comment-content
ul p
word-wrap: normal
word-break: break-word
ul, ol
list-style-position: inside list-style-position: inside
margin: 1em 0 margin: 1em 0
padding: 0 padding: 0 0 0 1.5em
.sjis .sjis
font-family: 'MS PGothic', ' ', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif font-family: 'MS PGothic', ' ', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
background: #fbfbfb background: #fbfbfb
color: #111 color: #111
font-size: 12pt font-size: 1em
line-height: 1 line-height: 1
margin: 0 margin: 0
padding: 4px padding: 4px
@ -118,9 +148,6 @@
white-space: pre white-space: pre
word-wrap: normal word-wrap: normal
p:first-child
margin-top: 0
.spoiler .spoiler
background: #eee background: #eee
color: #eee color: #eee
@ -140,5 +167,7 @@
background: #fafafa background: #fafafa
color: #444 color: #444
blockquote :last-child :first-child
margin-bottom: 0 margin-top: 0
:last-child
margin-bottom: 0

View file

@ -1,4 +1,9 @@
.comments>ul .comments>ul
list-style-type: none list-style-type: none
margin: 0 0 2em 0 margin: 0
padding: 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 .global-comment-list
text-align: left text-align: left
&>ul &>ul
list-style-type: none list-style-type: none
margin: 1em 0 margin: 1em 0 0
padding: 0 padding: 0
&>li
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) @media (max-width: 700px)
&>li
margin-bottom: 5em
padding: 1vw
.post-thumbnail .post-thumbnail
margin-bottom: 1em margin-bottom: 1em
.thumbnail .thumbnail
@ -18,14 +28,17 @@
@media (min-width: 700px) @media (min-width: 700px)
&>li &>li
display: flex padding-left: 13em
margin-bottom: 2em
.post-thumbnail .post-thumbnail
float: left float: left
margin: 0 0 1em -13em
.thumbnail .thumbnail
width: 12em width: 12em
height: 8em height: 8em
&>li
clear: both
.post-thumbnail .post-thumbnail
vertical-align: top vertical-align: top
margin-right: 1em margin-right: 1em
@ -34,3 +47,8 @@
.comments-container .comments-container
width: 100% 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 display: block
width: 20em width: 20em
ul .input
list-style-type: none list-style-type: none
margin: 0 0 1em 0 margin: 0 0 2em 0
padding: 0 padding: 0
li li
margin-top: 1.2em margin-top: 1.2em
label label
display: block display: block
padding: 0.3em 0 padding: 0.3em 0
.input
margin-bottom: 2em
.input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper), .input li:first-child label:not(.radio):not(.checkbox):not(.file-dropper),
.input li:first-child .input li:first-child
padding-top: 0 padding-top: 0
margin-top: 0 margin-top: 0
form:not(.horizontal)
.hint .hint
margin-top: 0.2em margin-top: 0.2em
margin-bottom: 0 margin-bottom: 0
@ -26,18 +26,31 @@ form
font-size: 80% font-size: 80%
line-height: 120% line-height: 120%
.darktheme form:not(.horizontal)
.hint
color: $inactive-link-color-darktheme
form.horizontal form.horizontal
display: inline-block display: inline-block
margin-bottom: 1em margin-bottom: 1em
.input, .buttons, ul .input, .buttons, ul
display: inline-block display: inline-block
vertical-align: middle vertical-align: top
margin: 0 margin: 0
padding: 0 padding: 0
input input
vertical-align: middle vertical-align: top
.buttons .buttons
margin-right: 0.5em 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
@ -128,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 * Regular inputs
*/ */
@ -163,6 +218,21 @@ input[type=number]
background: $input-disabled-background-color background: $input-disabled-background-color
color: $input-disabled-text-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],
input[readonly]+.radio, input[readonly]+.radio,
input[readonly]+.checkbox, input[readonly]+.checkbox,
@ -172,13 +242,25 @@ input:disabled
cursor: not-allowed cursor: not-allowed
label.color label.color
white-space: nowrap
position: relative position: relative
display: flex
input[type=text] input[type=text]
margin-right: 0.25em
width: auto
.preview
display: inline-block
text-align: center text-align: center
pointer-events: none padding: 0 0.5em
input[type=color] border: 2px solid black
position: absolute &:after
opacity: 0 content: 'A'
.background-preview
border-right: 0
color: transparent
.text-preview
border-left: 0
form.show-validation .input form.show-validation .input
input:invalid input:invalid
@ -189,8 +271,9 @@ form.show-validation .input
outline: 0 outline: 0
border: 2px solid $input-good-border-color border: 2px solid $input-good-border-color
background: $input-good-background-color background: $input-good-background-color
.darktheme form.show-validation .input
input:valid
background: darken($input-good-background-color, 75%)
/* /*
* Buttons * Buttons
@ -201,10 +284,13 @@ input[type=submit]
cursor: pointer cursor: pointer
font-size: 100% font-size: 100%
padding: 0.2em 0.7em padding: 0.2em 0.7em
border-radius: 0
border: 2px solid $button-enabled-background-color border: 2px solid $button-enabled-background-color
background: $button-enabled-background-color background: $button-enabled-background-color
color: $button-enabled-text-color color: $button-enabled-text-color
outline: 0 /* something on Chrome */ outline: 0 /* something on Chrome */
-moz-appearance: none
-webkit-appearance: none
&:disabled &:disabled
cursor: default cursor: default
@ -233,25 +319,30 @@ input::-moz-focus-inner
* File dropper * File dropper
*/ */
.file-dropper-holder .file-dropper-holder
display: flex
flex-wrap: wrap
.file-dropper .file-dropper
display: block display: block
width: 100%
background: $window-color background: $window-color
border: 3px dashed #eee border: 3px dashed #eee
padding: 0.3em 0.5em padding: 0.3em 0.5em
line-height: 140% line-height: 140%
text-align: center text-align: center
cursor: pointer cursor: pointer
overflow: hidden
word-wrap: break-word word-wrap: break-word
input .url-holder
display: flex
margin-top: 0.5em margin-top: 0.5em
width: auto input, button
flex: 1 min-width: 0 /* firefox being sassy */
button width: auto !important /* don't inherit anything weird */
margin-top: 0.5em input
width: 8em flex: 1
button
margin-left: 0.5em
.darktheme .file-dropper-holder
.file-dropper
background: $window-color-darktheme
input[type=file]:disabled+.file-dropper input[type=file]:disabled+.file-dropper
cursor: default cursor: default
@ -262,8 +353,6 @@ input[type=file]:focus+.file-dropper,
.file-dropper.active .file-dropper.active
border-color: $main-color border-color: $main-color
.autocomplete .autocomplete
position: absolute position: absolute
z-index: 10 z-index: 10
@ -288,6 +377,10 @@ input[type=file]:focus+.file-dropper,
.disabled .disabled
color: $inactive-link-color color: $inactive-link-color
.darktheme .autocomplete
background: $window-color-darktheme
ul li .disabled
color: $inactive-link-color-darktheme
.anticomplete .anticomplete
display: none display: none

View file

@ -1,12 +1,17 @@
@import colors @import colors
@import mixins @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 */ /* latin */
@font-face @font-face
font-family: 'Open Sans'; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 400; 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; 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 */ /* make <body> cover entire viewport */
@ -21,19 +26,32 @@ body
margin: 0 margin: 0
color: $text-color color: $text-color
font-family: 'Open Sans', sans-serif font-family: 'Open Sans', sans-serif
font-size: 12pt font-size: 1em
line-height: 18pt line-height: 1.4
@media (max-width: 800px) @media (max-width: 800px)
font-size: 10pt font-size: 0.875em
line-height: 15pt
@media (max-width: 1200px) @media (max-width: 1200px)
font-size: 11pt font-size: 0.95em
line-height: 16.5pt
body.darktheme
color: $text-color-darktheme
background: $window-color-darktheme
h1, h2, h3 h1, h2, h3
font-weight: normal font-weight: normal
margin-bottom: 1em margin-bottom: 1em
h1
font-size: 2em
h2
font-size: 1.5em
p,
ol,
ul
margin: 1em 0
th th
font-weight: normal font-weight: normal
@ -53,6 +71,11 @@ a
.vim-nav-hint .vim-nav-hint
position: absolute position: absolute
visibility: hidden visibility: hidden
.darktheme a
&.inactive
color: $inactive-link-color-darktheme
&.icon
color: $inactive-link-color-darktheme
a.append, span.append a.append, span.append
margin-left: 1em margin-left: 1em
@ -61,8 +84,10 @@ form .fa-question-circle-o
vertical-align: middle vertical-align: middle
#content-holder #content-holder
padding: 1.5vw padding: 1.5em
text-align: center text-align: center
@media (max-width: 1000px)
padding: 1em
>.content-wrapper >.content-wrapper
box-sizing: border-box /* make max-width: 100% on this element include padding */ box-sizing: border-box /* make max-width: 100% on this element include padding */
text-align: left text-align: left
@ -70,9 +95,30 @@ form .fa-question-circle-o
margin: 0 auto margin: 0 auto
>*:first-child, form h1 >*:first-child, form h1
margin-top: 0 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) >.content-wrapper:not(.transparent)
background: $top-navigation-color 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 hr
border: 0 border: 0
@ -80,6 +126,9 @@ hr
margin: 1em 0 margin: 1em 0
padding: 0 padding: 0
.darktheme hr
border-top: 1px solid darken($line-color, 25%)
nav nav
ul ul
list-style-type: none list-style-type: none
@ -125,6 +174,39 @@ nav
li li
display: inline-block display: inline-block
float: left 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=account],
ul li[data-name=register], ul li[data-name=register],
ul li[data-name=login], ul li[data-name=login],
@ -141,6 +223,26 @@ nav
margin-right: 0.6em margin-right: 0.6em
margin-left: calc(0.6em - 1.2em) margin-left: calc(0.6em - 1.2em)
float: left 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 a .access-key
text-decoration: underline text-decoration: underline
@ -166,6 +268,18 @@ a .access-key
border: 1px solid $message-success-border-color border: 1px solid $message-success-border-color
background: $message-success-background-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 .thumbnail
/*background-image: attr(data-src url)*/ /* not available yet */ /*background-image: attr(data-src url)*/ /* not available yet */
vertical-align: middle vertical-align: middle
@ -176,9 +290,14 @@ a .access-key
width: 20px width: 20px
height: 20px height: 20px
&.empty &.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-repeat: repeat
background-size: initial background-size: 20px 20px
img img
opacity: 0 opacity: 0
width: 100% width: 100%
@ -194,6 +313,14 @@ a .access-key
margin-top: 0 !important margin-top: 0 !important
margin-bottom: 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 */ /* hack to prevent text from being copied */
[data-pseudo-content]:before { [data-pseudo-content]:before {
content: attr(data-pseudo-content) content: attr(data-pseudo-content)

View file

@ -16,9 +16,17 @@
color: mix($text-color, $inactive-link-color) color: mix($text-color, $inactive-link-color)
font-size: 120% font-size: 120%
i i
font-size: 12pt font-size: 1em
color: $inactive-link-color color: $inactive-link-color
float: right float: right
line-height: 2em line-height: 2em
.expander-content .expander-content
padding: 0.5em 0.5em 2em 0.5em 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 font-size: 1.6em
&:first-child &:first-child
margin-top: 0 margin-top: 0
@media (max-width: 1000px)
margin-top: 1.5em
&:first-child
margin-top: 0
nav nav
ul ul
margin: 0 auto margin: 0 auto

View file

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

View file

@ -8,7 +8,7 @@
.page .page
position: relative position: relative
.page-header .page-header
margin: 0.5em 0.5em 0.5em 0 margin: 0.5em 0
position: relative position: relative
&:before &:before
display: block display: block
@ -22,6 +22,14 @@
z-index: 1 z-index: 1
span span
position: relative position: relative
background: white background: $window-color
padding: 0 1em padding: 0 1em
z-index: 2 z-index: 2
.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-container
.post-content.transparency-grid img .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 text-align: center
.post-content .post-content
@ -8,20 +16,14 @@
margin: 0 auto margin: 0 auto
position: relative position: relative
img, object, video, .post-overlay .resize-listener
position: absolute position: absolute
height: 100%
width: 100%
left: 0 left: 0
right: 0 right: 0
top: 0 top: 0
bottom: 0 bottom: 0
width: 100%
height: 100%
.post-overlay>* img
position: absolute image-orientation: from-image
left: 0
right: 0
top: 0
bottom: 0
width: 100%
height: 100%

View file

@ -14,9 +14,6 @@
.right-post-container .right-post-container
width: 47% width: 47%
float: right float: right
input[type=text]
width: 8em
margin-top: -2px
.post-mirror .post-mirror
margin-bottom: 1em margin-bottom: 1em
&:after &:after
@ -31,3 +28,10 @@
margin-right: 0.35em margin-right: 0.35em
.target-post, .target-post-content .target-post, .target-post-content
margin: 1em 0 margin: 1em 0
header
margin-bottom: 1em
label
display: inline-block
margin-top: 2px
input[type=text]
width: 6em

View file

@ -54,39 +54,96 @@
.icon:not(:first-of-type) .icon:not(:first-of-type)
margin-left: 1em margin-left: 1em
.masstag .edit-overlay
position: absolute position: absolute
top: 0.5em top: 0.5em
left: 0.5em left: 0.5em
display: inline-block
padding: 0.5em .tag-flipper
box-sizing: border-box
border: 0
&:after
display: inline-block display: inline-block
width: 1em padding: 0.5em
height: 1em box-sizing: border-box
border: 0
&:after
display: inline-block
width: 1em
height: 1em
text-align: center
line-height: 1em
font-size: 2.2em
&.tagged
background: rgba(0, 230, 0, 0.7)
&:after
color: white
content: '-'
&:not(.tagged)
background: rgba(255, 0, 0, 0.7)
&:after
color: white
content: '+'
&[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 text-align: center
line-height: 1em line-height: 1em
font-size: 20pt font-size: 1.6em
&.tagged border: 3px solid
background: rgba(0, 230, 0, 0.7) &.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 &:after
color: white display: inline-block
content: '-' width: 1em
&:not(.tagged) height: 1em
background: rgba(255, 0, 0, 0.7) text-align: center
&:after line-height: 1em
color: white font-size: 2.2em
content: '+' &.delete
&[data-disabled] background: rgba(255, 0, 0, 0.7)
background: rgba(200, 200, 200, 0.7) &:after
color: white
font-family: FontAwesome;
content: "\f1f8"; // fa-trash
&:not(.delete)
background: rgba(200, 200, 200, 0.7)
&:after
color: white
content: '-'
.thumbnail .thumbnail
background-position: 50% 30%
width: 100% width: 100%
height: 100% height: 100%
outline-offset: -3px outline-offset: -3px
&:not(.empty)
background-position: 50% 30%
.thumbnail-wrapper.no-tags .thumbnail-wrapper.no-tags
.thumbnail .thumbnail
@ -101,41 +158,102 @@
.thumbnail .thumbnail
outline: 4px solid $main-color !important 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 .post-list-header
white-space: nowrap white-space: nowrap
text-align: left text-align: left
label label
display: none display: none !important
form form
width: auto width: auto
margin-bottom: 0.75em margin-bottom: 0.75em
* *
vertical-align: top 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 input
margin-bottom: 0.25em margin-bottom: 0.25em
margin-right: 0.25em margin-right: 0.25em
input[name=search-text] input[name=search-text]
width: 25em width: 25em
input[name=masstag] @media (max-width: 1000px)
width: 12em display: block
.masstag-hint, .open-masstag width: 100%
margin-right: 1em margin-bottom: 0.5em
.append .append
vertical-align: middle
font-size: 0.95em font-size: 0.95em
color: $inactive-link-color color: $inactive-link-color
.masstag .bulk-edit
&:not(.active) &: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], [type=text],
.start-tagging, .start
.stop-tagging
display: none display: none
.masstag-hint .hint
display: none display: none
&.active input[name=tag]
.open-masstag 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 display: none
.append.open
@media (max-width: 1000px)
margin-left: 0
.start
margin-left: 1em
.safety .safety
margin-right: 0.25em margin-right: 0.25em
&.safety-safe &.safety-safe

View file

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

View file

@ -1,5 +1,7 @@
@import colors @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 $cancel-button-color = tomato
#post-upload #post-upload
@ -11,8 +13,12 @@ $cancel-button-color = tomato
&.inactive input[type=submit], &.inactive input[type=submit],
&.inactive .skip-duplicates &.inactive .skip-duplicates
&.inactive .always-upload-similar
&.inactive .pause-remain-on-error
&.uploading input[type=submit], &.uploading input[type=submit],
&.uploading .skip-duplicates, &.uploading .skip-duplicates,
&.uploading .always-upload-similar
&.uploading .pause-remain-on-error
&:not(.uploading) .cancel &:not(.uploading) .cancel
display: none display: none
@ -21,6 +27,8 @@ $cancel-button-color = tomato
.file-dropper .file-dropper
font-size: 150% font-size: 150%
padding: 2em padding: 2em
small
font-size: 60%
input[type=submit] input[type=submit]
margin-top: 1em margin-top: 1em
@ -35,42 +43,140 @@ $cancel-button-color = tomato
.skip-duplicates .skip-duplicates
margin-left: 1em margin-left: 1em
.messages .always-upload-similar
margin-left: 1em
.pause-remain-on-error
margin-left: 1em
form>.messages
margin-top: 1em margin-top: 1em
.uploadables-container .uploadables-container
li list-style-type: none
margin: 0
padding: 0
.uploadable-container
clear: both
margin: 0 0 1.2em 0 margin: 0 0 1.2em 0
padding-left: 13em
.uploadable img
.file width: 100%
margin: 0.3em 0 height: 100%
overflow: hidden
white-space: nowrap video
text-align: left width: 100%
text-overflow: ellipsis height: 100%
.anonymous &>.thumbnail-wrapper
margin: 0.3em 0
.safety
margin: 0.3em 0
label
display: inline-block
margin-right: 1em
.thumbnail-wrapper
float: left float: left
width: 12.5em width: 12em
height: 7em height: 8em
margin: 0.2em 1em 0 0 margin: 0 0 0 -13em
.thumbnail .thumbnail
width: 100% width: 100%
height: 100% height: 100%
.controls .uploadable
float: right border: 1px solid $upload-border-color
a min-height: 8em
color: $inactive-link-color box-sizing: border-box
margin-left: 0.5em
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
&: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 ul
margin: 0 auto margin: 0 auto
padding: 0
width: 100% width: 100%
max-width: 35em max-width: 35em
list-style-type: none list-style-type: none
li li
margin-bottom: 1em
&:last-child
margin-bottom: 0
.time .time
float: right float: right
@ -26,19 +31,34 @@ $snapshot-merged-background-color = #FEC
div.operation-created div.operation-created
background: $snapshot-created-background-color background: $snapshot-created-background-color
&+.details &+.details
background: lighten($snapshot-created-background-color, 50%) background: alpha(@background, 50%)
div.operation-modified div.operation-modified
background: $snapshot-modified-background-color background: $snapshot-modified-background-color
&+.details &+.details
background: lighten($snapshot-modified-background-color, 50%) background: alpha(@background, 50%)
div.operation-deleted div.operation-deleted
background: $snapshot-deleted-background-color background: $snapshot-deleted-background-color
&+.details &+.details
background: lighten($snapshot-deleted-background-color, 50%) background: alpha(@background, 50%)
div.operation-merged div.operation-merged
background: $snapshot-merged-background-color background: $snapshot-merged-background-color
&+.details &+.details
background: lighten($snapshot-merged-background-color, 50%) background: alpha(@background, 50%)
div.details .darktheme .snapshot-list ul li
margin-bottom: 2em 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 .content-wrapper.tag-categories
width: 100% width: 100%
max-width: 40em max-width: 45em
table table
border-spacing: 0 border-spacing: 0
width: 100% width: 100%
@ -11,13 +11,19 @@
td, th td, th
padding: .4em padding: .4em
&.color &.color
text-align: center input[type=text]
width: 8em
&.usages &.usages
text-align: center text-align: center
&.remove, &.set-default &.remove, &.set-default
white-space: pre white-space: pre
th
white-space: nowrap
&:first-child
padding-left: 0
&:last-child
padding-right: 0
tfoot tfoot
display: none display: none
form form
width: auto width: auto

View file

@ -46,7 +46,7 @@ div.tag-input
.wrapper .wrapper
margin-left: 0.5em margin-left: 0.5em
background: $tag-suggestions-background-color background: $window-color
border: 1px solid $tag-suggestions-border-color border: 1px solid $tag-suggestions-border-color
width: 15em width: 15em
word-break: break-all word-break: break-all
@ -55,13 +55,14 @@ div.tag-input
padding: 0.2em 1em padding: 0.2em 1em
margin: 0 margin: 0
ul ul
list-style-type: none
margin: 0 margin: 0
overflow-y: auto overflow-y: auto
overflow-x: none overflow-x: none
max-height: 20em max-height: 20em
padding: 0.5em 1em 0 1em padding: 0.5em 1em 0 1em
li:last-child li:last-child
border-bottom: 0.5em solid alpha($tag-suggestions-background-color, 0) border-bottom: 0.5em solid alpha($window-color, 0)
li li
margin: 0 margin: 0
font-size: 90% font-size: 90%
@ -85,9 +86,16 @@ div.tag-input
font-size: 90% font-size: 90%
unselectable() unselectable()
@keyframes tag-added-to-post
from
max-height: 0
to
max-height: 5em
ul.compact-tags ul.compact-tags
width: 100% width: 100%
margin-top: 0.5em margin: 0.5em 0 0 0
padding: 0
li li
margin: 0 margin: 0
width: 100% width: 100%
@ -101,18 +109,30 @@ ul.compact-tags
a:focus a:focus
outline: 0 outline: 0
box-shadow: inset 0 0 0 2px $main-color box-shadow: inset 0 0 0 2px $main-color
// these 3 added when tag is added to ul
&.added, &.new, &.implication
animation: tag-added-to-post 1s ease forwards
&.implication &.implication
background: $implied-tag-background-color
color: $implied-tag-text-color color: $implied-tag-text-color
background-color: $implied-tag-background-color
&.new &.new
background: $new-tag-background-color
color: $new-tag-text-color color: $new-tag-text-color
background-color: $new-tag-background-color
&.duplicate &.duplicate
background: $duplicate-tag-background-color
color: $duplicate-tag-text-color color: $duplicate-tag-text-color
background-color: $duplicate-tag-background-color
i i
padding-right: 0.4em padding-right: 0.4em
.darktheme ul.compact-tags
li
&.new
background-color: darken($new-tag-background-color, 80%)
&.implication
background-color: darken($implied-tag-background-color, 85%)
&.duplicate
background-color: darken($duplicate-tag-background-color, 80%)
div.tag-input, ul.compact-tags div.tag-input, ul.compact-tags
.tag-usages, .tag-weight, .remove-tag .tag-usages, .tag-weight, .remove-tag
color: $inactive-link-color color: $inactive-link-color
@ -123,3 +143,19 @@ div.tag-input, ul.compact-tags
margin-left: 0.7em margin-left: 0.7em
.remove-tag .remove-tag
margin-right: 0.5em 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 th, td
padding: 0.1em 0.5em padding: 0.1em 0.5em
th th
white-space: nowrap
background: $top-navigation-color background: $top-navigation-color
.names .names
width: 28% width: 28%
@ -39,14 +40,28 @@
.implications, .suggestions .implications, .suggestions
display: none display: none
.darktheme .tag-list
table
tr:hover td
background: $top-navigation-color-darktheme
th
background: $top-navigation-color-darktheme
.tag-list-header .tag-list-header
label label
display: none display: none !important
text-align: left text-align: left
form form
width: auto width: auto
input[name=search-text] input[name=search-text]
max-width: 15em width: 25em
@media (max-width: 1000px)
width: 100%
.append .append
vertical-align: middle
font-size: 0.95em font-size: 0.95em
color: $inactive-link-color color: $inactive-link-color
.darktheme .tag-list-header
.append
color: $inactive-link-color-darktheme

View file

@ -21,19 +21,31 @@
.details .details
font-size: 90% font-size: 90%
line-height: 130% line-height: 130%
.image
margin: 0.25em 0.6em 0.25em 0
.thumbnail .thumbnail
width: 3em width: 3em
height: 3em height: 3em
margin: 0.25em 0.6em 0 0
.darktheme .user-list
ul li
background: $top-navigation-color-darktheme
.user-list-header .user-list-header
label label
display: none display: none !important
text-align: left text-align: left
form form
width: auto width: auto
input[name=search-text] input[name=search-text]
max-width: 15em width: 25em
@media (max-width: 1000px)
width: 100%
.append .append
vertical-align: middle
font-size: 0.95em font-size: 0.95em
color: $inactive-link-color 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 #user
width: 100% width: 100%
max-width: 35em max-width: 35em
@ -37,7 +40,43 @@
height: 1px height: 1px
clear: both clear: both
#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 #user-delete form
width: 100% 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'> <div class='avatar'>
<% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %> <% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %>
<a href='/user/<%- encodeURIComponent(ctx.comment.user.name) %>'> <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> </a>
<% } %> <% } %>
</div> </div>
<div class='body'> <div class='comment'>
<header><% <header>
%><span class='nickname'><% <nav class='edit tabs'>
%><% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %><% <ul>
%><a href='/user/<%- encodeURIComponent(ctx.comment.user.name) %>'><% <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.user ? ctx.user.name : 'Deleted user' %><%
%><% if (ctx.user && ctx.user.name && ctx.canViewUsers) { %><%
%></a><%
%><% } %><%
%></span></strong>
<span class='date'><%
%>commented <%= ctx.makeRelativeTime(ctx.comment ? ctx.comment.creationTime : null) %><%
%></span><%
%><wbr><%
%><span class='score-container'></span><%
%><% if (ctx.canEditComment || ctx.canDeleteComment) { %><%
%><span class='action-container'><%
%><% if (ctx.canEditComment) { %><%
%><a href class='edit'><%
%><i class='fa fa-pencil'></i>&nbsp;edit<%
%></a><%
%><% } %><%
%><% if (ctx.canDeleteComment) { %><%
%><a href class='delete'><%
%><i class='fa fa-remove'></i>&nbsp;delete<%
%></a><%
%><% } %><%
%></span><%
%><% } %><% %><% } %><%
%></nav><%
%><%- ctx.comment.user ? ctx.comment.user.name : 'Deleted user' %><%
%><% if (ctx.comment.user && ctx.comment.user.name && ctx.canViewUsers) { %><%
%></a><%
%><% } %><%
%></span><%
%><wbr><%
%><span class='date'><%
%><%= ctx.makeRelativeTime(ctx.comment.creationTime) %><%
%></span><%
%><wbr><%
%><span class='score-container'></span><%
%><wbr><%
%><% if (ctx.canEditComment) { %><%
%><a href class='edit'><%
%><i class='fa fa-pencil'></i> edit<%
%></a><%
%><% } %><%
%><wbr><%
%><% if (ctx.canDeleteComment) { %><%
%><a href class='delete'><%
%><i class='fa fa-remove'></i> delete<%
%></a><%
%><% } %><%
%></header> %></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>
</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'> <div class='global-comment-list'>
<ul><!-- <ul><!--
--><% for (let post of ctx.results) { %><!-- --><% for (let post of ctx.response.results) { %><!--
--><li><!-- --><li><!--
--><div class='post-thumbnail'><!-- --><div class='post-thumbnail'><!--
--><% if (ctx.canViewPosts) { %><!-- --><% if (ctx.canViewPosts) { %><!--
--><a href='/post/<%- encodeURIComponent(post.id) %>'><!-- --><a href='<%- ctx.formatClientLink('post', post.id) %>'><!--
--><% } %><!-- --><% } %><!--
--><%= ctx.makeThumbnail(post.thumbnailUrl) %><!-- --><%= ctx.makeThumbnail(post.thumbnailUrl) %><!--
--><% if (ctx.canViewPosts) { %><!-- --><% if (ctx.canViewPosts) { %><!--

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -80,6 +80,9 @@ take following form:</p>
<code>,desc</code> to control the sort direction, which can be also controlled <code>,desc</code> to control the sort direction, which can be also controlled
by negating the whole token.</p> 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> <h1>Example</h1>
<p>Searching for posts with following query:</p> <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 <p>will show flash files tagged as sea, that were liked by seven people at
most, uploaded by user Pirate.</p> 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>
<tr> <tr>
<td><code>tag</code></td> <td><code>tag</code></td>
<td>having given tag</td> <td>having given tag (accepts wildcards)</td>
</tr> </tr>
<tr> <tr>
<td><code>score</code></td> <td><code>score</code></td>
@ -20,23 +20,31 @@
</tr> </tr>
<tr> <tr>
<td><code>uploader</code></td> <td><code>uploader</code></td>
<td>uploaded by given user</td> <td>uploaded by given user (accepts wildcards)</td>
</tr> </tr>
<tr> <tr>
<td><code>upload</code></td> <td><code>upload</code></td>
<td>alias of <code>upload</code></td> <td>alias of <code>uploader</code></td>
</tr> </tr>
<tr> <tr>
<td><code>submit</code></td> <td><code>submit</code></td>
<td>alias of <code>upload</code></td> <td>alias of <code>uploader</code></td>
</tr> </tr>
<tr> <tr>
<td><code>comment</code></td> <td><code>comment</code></td>
<td>commented by given user</td> <td>commented by given user (accepts wildcards)</td>
</tr> </tr>
<tr> <tr>
<td><code>fav</code></td> <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>
<tr> <tr>
<td><code>tag-count</code></td> <td><code>tag-count</code></td>
@ -54,6 +62,10 @@
<td><code>note-count</code></td> <td><code>note-count</code></td>
<td>having given number of annotations</td> <td>having given number of annotations</td>
</tr> </tr>
<tr>
<td><code>note-text</code></td>
<td>having given note text (accepts wildcards)</td>
</tr>
<tr> <tr>
<td><code>relation-count</code></td> <td><code>relation-count</code></td>
<td>having given number of relations</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> <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>
<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> <td>having given SHA1 checksum</td>
</tr> </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> <tr>
<td><code>file-size</code></td> <td><code>file-size</code></td>
<td>having given file size (in bytes)</td> <td>having given file size (in bytes)</td>
@ -86,6 +110,14 @@
<td><code>image-area</code></td> <td><code>image-area</code></td>
<td>having given number of pixels (image width * image height)</td> <td>having given number of pixels (image width * image height)</td>
</tr> </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> <tr>
<td><code>width</code></td> <td><code>width</code></td>
<td>alias of <code>image-width</code></td> <td>alias of <code>image-width</code></td>
@ -98,6 +130,14 @@
<td><code>area</code></td> <td><code>area</code></td>
<td>alias of <code>image-area</code></td> <td>alias of <code>image-area</code></td>
</tr> </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> <tr>
<td><code>creation-date</code></td> <td><code>creation-date</code></td>
<td>posted at given date</td> <td>posted at given date</td>

View file

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

View file

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

View file

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

View file

@ -2,16 +2,31 @@
<html> <html>
<head> <head>
<meta charset='utf-8'/> <meta charset='utf-8'/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1'> <meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title><!-- configured in the config file --></title> <meta name='theme-color' content='#24aadd'/>
<link href='/css/app.min.css' rel='stylesheet' type='text/css'/> <meta name='apple-mobile-web-app-capable' content='yes'/>
<link href='/css/vendor.min.css' rel='stylesheet' type='text/css'/> <meta name='apple-mobile-web-app-status-bar-style' content='black'/>
<link rel='shortcut icon' type='image/png' href='/img/favicon.png'/> <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> </head>
<body> <body>
<div id='top-navigation-holder'></div> <div id='top-navigation-holder'></div>
<div id='content-holder'></div> <div id='content-holder'></div>
<script type='text/javascript' src='/js/vendor.min.js'></script> <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/app.min.js'></script>
</body> </body>
</html> </html>

View file

@ -1,38 +1,36 @@
<div class='content-wrapper' id='login'> <div class='content-wrapper' id='login'>
<h1>Log in</h1> <h1>Log in</h1>
<form> <form>
<div class='input'> <ul class='input'>
<ul> <li>
<li> <%= ctx.makeTextInput({
<%= ctx.makeTextInput({ text: 'User name',
text: 'User name', name: 'name',
name: 'name', required: true,
required: true, pattern: ctx.userNamePattern,
pattern: ctx.userNamePattern, }) %>
}) %> </li>
</li> <li>
<li> <%= ctx.makePasswordInput({
<%= ctx.makePasswordInput({ text: 'Password',
text: 'Password', name: 'password',
name: 'password', required: true,
required: true, pattern: ctx.passwordPattern,
pattern: ctx.passwordPattern, }) %>
}) %> </li>
</li> <li>
<li> <%= ctx.makeCheckbox({
<%= ctx.makeCheckbox({ text: 'Remember me',
text: 'Remember me', name: 'remember-user',
name: 'remember-user', }) %>
}) %> </li>
</li> </ul>
</ul>
</div>
<div class='messages'></div> <div class='messages'></div>
<div class='buttons'> <div class='buttons'>
<input type='submit' value='Log in'/> <input type='submit' value='Log in'/>
<% if (ctx.canSendMails) { %> <a class='append' href='<%- ctx.formatClientLink('password-reset') %>'>Forgot the password?</a>
<a class='append' href='/password-reset'>Forgot the password?</a>
<% } %>
</div> </div>
</form> </form>
</div> </div>

View file

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

View file

@ -1,5 +1,5 @@
<div class='not-found'> <div class='not-found'>
<h1>Not found</h1> <h1>Not found</h1>
<p><%- ctx.path %> is not a valid URL.</p> <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> </div>

View file

@ -1,8 +1,8 @@
<div class='content-wrapper' id='password-reset'> <div class='content-wrapper' id='password-reset'>
<h1>Password reset</h1> <h1>Password reset</h1>
<form autocomplete='off'> <% if (ctx.canSendMails) { %>
<div class='input'> <form autocomplete='off'>
<ul> <ul class='input'>
<li> <li>
<%= ctx.makeTextInput({ <%= ctx.makeTextInput({
text: 'User name or e-mail address', text: 'User name or e-mail address',
@ -11,13 +11,20 @@
}) %> }) %>
</li> </li>
</ul> </ul>
</div>
<p><small>Proceeding will send an e-mail that contains a password reset <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. 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> It is recommended to change that password to something else.</small></p>
<div class='messages'></div>
<div class='buttons'> <div class='messages'></div>
<input type='submit' value='Proceed'/> <div class='buttons'>
</div> <input type='submit' value='Proceed'/>
</form> </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> </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 %>'> <div class='post-content post-type-<%- ctx.post.type %>'>
<% if (['image', 'animation'].includes(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') { %> <% } 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='wmode' value='opaque'/>
<param name='movie' value='<%- ctx.post.contentUrl %>'/> <param name='movie' value='<%- ctx.post.contentUrl %>'/>
</object> </object>
<% } else if (ctx.post.type === 'video') { %> <% } else if (ctx.post.type === 'video') { %>
<% if ((ctx.post.flags || []).includes('loop')) { %> <%= ctx.makeElement(
<video id='video' controls loop='loop'> 'video', {
<% } else { %> class: 'resize-listener',
<video id='video' controls> controls: true,
<% } %> loop: (ctx.post.flags || []).includes('loop'),
playsinline: true,
<source type='<%- ctx.post.mimeType %>' src='<%- ctx.post.contentUrl %>'/> autoplay: ctx.autoplay,
},
Your browser doesn't support HTML5 videos. ctx.makeElement('source', {
</video> type: ctx.post.mimeType,
src: ctx.post.contentUrl,
}),
'Your browser doesn\'t support HTML5 videos.')
%>
<% } else { console.log(new Error('Unknown post type')); } %> <% } else { console.log(new Error('Unknown post type')); } %>
<div class='post-overlay'> <div class='post-overlay resize-listener'>
</div> </div>
</div> </div>

View file

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

View file

@ -4,7 +4,7 @@
<div class='messages'></div> <div class='messages'></div>
<% if (ctx.canEditPostSafety) { %> <% if (ctx.enableSafety && ctx.canEditPostSafety) { %>
<section class='safety'> <section class='safety'>
<label>Safety</label> <label>Safety</label>
<div class='radio-wrapper'> <div class='radio-wrapper'>
@ -50,14 +50,32 @@
name: 'loop', name: 'loop',
checked: ctx.post.flags.includes('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> </section>
<% } %> <% } %>
<% if (ctx.canEditPostTags) { %> <% if (ctx.canEditPostTags) { %>
<section class='tags'> <section class='tags'>
<%= ctx.makeTextInput({ <%= ctx.makeTextInput({}) %>
value: ctx.post.tags.join(' '), </section>
}) %> <% } %>
<% if (ctx.canEditPoolPosts) { %>
<section class='pools'>
<%= ctx.makeTextInput({}) %>
</section> </section>
<% } %> <% } %>
@ -66,6 +84,12 @@
<a href class='add'>Add a note</a> <a href class='add'>Add a note</a>
<%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %> <%= ctx.makeTextarea({disabled: true, text: 'Content (supports Markdown)', rows: '8'}) %>
<a href class='delete inactive'>Delete selected note</a> <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> </section>
<% } %> <% } %>

View file

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

View file

@ -1,6 +1,6 @@
<div class='post-merge'> <div class='post-merge'>
<form> <form>
<ul> <ul class='input'>
<li class='post-mirror'> <li class='post-mirror'>
<div class='left-post-container'></div> <div class='left-post-container'></div>
<div class='right-post-container'></div> <div class='right-post-container'></div>

View file

@ -1,8 +1,12 @@
<% if (ctx.editable) { %> <header>
<p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>'/></p> <label for='merge-id-<%- ctx.name %>'>Post #</label>
<% } else { %> <% if (ctx.editable) { %>
<p>Post # <input type='text' pattern='^[0-9]+$' value='<%- ctx.post ? ctx.post.id : '' %>' readonly/></p> <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) { %> <% if (ctx.post) { %>
<div class='post-thumbnail'> <div class='post-thumbnail'>
@ -31,7 +35,14 @@
'image/gif': 'GIF', 'image/gif': 'GIF',
'image/jpeg': 'JPEG', 'image/jpeg': 'JPEG',
'image/png': 'PNG', 'image/png': 'PNG',
'image/webp': 'WEBP',
'image/bmp': 'BMP',
'image/avif': 'AVIF',
'image/heif': 'HEIF',
'image/heic': 'HEIC',
'video/webm': 'WEBM', 'video/webm': 'WEBM',
'video/mp4': 'MPEG-4',
'video/quicktime': 'MOV',
'application/x-shockwave-flash': 'SWF', 'application/x-shockwave-flash': 'SWF',
}[ctx.post.mimeType] + }[ctx.post.mimeType] +
' (' + ' (' +

View file

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

View file

@ -7,12 +7,28 @@
<span class='skip-duplicates'> <span class='skip-duplicates'>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Skip duplicates', text: 'Skip duplicate',
name: 'skip-duplicates', name: 'skip-duplicates',
checked: false, checked: false,
}) %> }) %>
</span> </span>
<span class='always-upload-similar'>
<%= ctx.makeCheckbox({
text: 'Force upload similar',
name: 'always-upload-similar',
checked: false,
}) %>
</span>
<span class='pause-remain-on-error'>
<%= ctx.makeCheckbox({
text: 'Pause on error',
name: 'pause-remain-on-error',
checked: true,
}) %>
</span>
<input type='button' value='Cancel' class='cancel'/> <input type='button' value='Cancel' class='cancel'/>
</div> </div>

View file

@ -1,10 +1,4 @@
<li class='uploadable'> <li class='uploadable-container'>
<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>
<div class='thumbnail-wrapper'> <div class='thumbnail-wrapper'>
<% if (['image'].includes(ctx.uploadable.type)) { %> <% if (['image'].includes(ctx.uploadable.type)) { %>
@ -29,28 +23,74 @@
<% } %> <% } %>
</div> </div>
<div class='file'> <div class='uploadable'>
<strong><%= ctx.uploadable.name %></strong> <header>
</div> <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>
<div class='safety'> <span class='filename'><%= ctx.uploadable.name %></span>
<% for (let safety of ['safe', 'sketchy', 'unsafe']) { %> </header>
<%= ctx.makeRadio({
name: 'safety-' + ctx.uploadable.key,
value: safety,
text: safety[0].toUpperCase() + safety.substr(1),
selectedValue: ctx.uploadable.safety,
}) %>
<% } %>
</div>
<% if (ctx.canUploadAnonymously) { %> <div class='body'>
<div class='anonymous'> <% if (ctx.enableSafety) { %>
<%= ctx.makeCheckbox({ <div class='safety'>
text: 'Upload anonymously', <% for (let safety of ['safe', 'sketchy', 'unsafe']) { %>
name: 'anonymous', <%= ctx.makeRadio({
checked: ctx.uploadable.anonymous, name: 'safety-' + ctx.uploadable.key,
}) %> value: safety,
text: safety[0].toUpperCase() + safety.substr(1),
selectedValue: ctx.uploadable.safety,
}) %>
<% } %>
</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>
<% } %> </div>
</li> </li>

View file

@ -1,24 +1,37 @@
<div class='post-list-header'><% <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}) %><% %><%= ctx.makeTextInput({text: 'Search query', id: 'search-text', name: 'search-text', value: ctx.parameters.query}) %><%
%><wbr/><% %><wbr/><%
%><input class='mousetrap' type='submit' value='Search'/><% %><input class='mousetrap' type='submit' value='Search'/><%
%><wbr/><% %><wbr/><%
%><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><% %><% if (ctx.enableSafety) { %><%
%><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><% %><input data-safety=safe type='button' class='mousetrap safety safety-safe <%- ctx.settings.listPosts.safe ? '' : 'disabled' %>'/><%
%><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><% %><input data-safety=sketchy type='button' class='mousetrap safety safety-sketchy <%- ctx.settings.listPosts.sketchy ? '' : 'disabled' %>'/><%
%><wbr/><% %><input data-safety=unsafe type='button' class='mousetrap safety safety-unsafe <%- ctx.settings.listPosts.unsafe ? '' : 'disabled' %>'/><%
%><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><% %></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> %></div>

View file

@ -1,14 +1,18 @@
<div class='post-list'> <% if (ctx.postFlow) { %><div class='post-list post-flow'><% } else { %><div class='post-list'><% } %>
<% if (ctx.results.length) { %> <% if (ctx.response.results.length) { %>
<ul> <ul>
<% for (let post of ctx.results) { %> <% for (let post of ctx.response.results) { %>
<li> <li data-post-id='<%= post.id %>'>
<a class='thumbnail-wrapper <%= post.tags.length > 0 ? "tags" : "no-tags" %>' <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' %>' 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) : "" %>'> href='<%= ctx.canViewPosts ? ctx.getPostUrl(post.id, ctx.parameters) : '' %>'>
<%= ctx.makeThumbnail(post.thumbnailUrl) %> <%= ctx.makeThumbnail(post.thumbnailUrl) %>
<span class='type' data-type='<%- post.type %>'> <span class='type' data-type='<%- post.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> </span>
<% if (post.score || post.favoriteCount || post.commentCount) { %> <% if (post.score || post.favoriteCount || post.commentCount) { %>
<span class='stats'> <span class='stats'>
@ -33,10 +37,24 @@
</span> </span>
<% } %> <% } %>
</a> </a>
<% if (ctx.canMassTag && ctx.parameters && ctx.parameters.tag) { %> <span class='edit-overlay'>
<a href data-post-id='<%= post.id %>' class='masstag'> <% if (ctx.canBulkEditTags && ctx.parameters && ctx.parameters.tag) { %>
</a> <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> </li>
<% } %> <% } %>
<%= ctx.makeFlexboxAlign() %> <%= ctx.makeFlexboxAlign() %>

View file

@ -5,11 +5,10 @@
<ul class='input'> <ul class='input'>
<li> <li>
<%= ctx.makeCheckbox({ <%= 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', name: 'keyboard-shortcuts',
checked: ctx.browsingSettings.keyboardShortcuts, checked: ctx.browsingSettings.keyboardShortcuts,
}) %> }) %>
<a class='append icon' href='/help/keyboard'><i class='fa fa-question-circle-o'></i></a>
</li> </li>
<li> <li>
@ -23,6 +22,15 @@
}) %> }) %>
</li> </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> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Upscale small posts', text: 'Upscale small posts',
@ -39,6 +47,15 @@
<p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p> <p class='hint'>Rather than using a paged navigation, smoothly scrolls through the content.</p>
</li> </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> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
text: 'Enable transparency grid', text: 'Enable transparency grid',
@ -56,6 +73,23 @@
}) %> }) %>
<p class='hint'>Shows a popup with suggested tags in edit forms.</p> <p class='hint'>Shows a popup with suggested tags in edit forms.</p>
</li> </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> </ul>
<div class='messages'></div> <div class='messages'></div>

View file

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

View file

@ -1,16 +1,16 @@
<div class='content-wrapper' id='tag'> <div class='content-wrapper' id='tag'>
<h1><%- ctx.tag.names[0] %></h1> <h1><%- ctx.getPrettyName(ctx.tag.names[0]) %></h1>
<nav class='buttons'><!-- <nav class='buttons'><!--
--><ul><!-- --><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) { %><!-- --><% 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) { %><!-- --><% 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) { %><!-- --><% 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><!-- --></ul><!--
--></nav> --></nav>

View file

@ -1,17 +1,20 @@
<div class='content-wrapper tag-categories'> <div class='content-wrapper tag-categories'>
<form> <form>
<h1>Tag categories</h1> <h1>Tag categories</h1>
<table> <div class="table-wrap">
<thead> <table>
<tr> <thead>
<th class='name'>Category name</th> <tr>
<th class='color'>CSS color</th> <th class='name'>Category name</th>
<th class='usages'>Usages</th> <th class='color'>CSS color</th>
</tr> <th class='order'>Order</th>
</thead> <th class='usages'>Usages</th>
<tbody> </tr>
</tbody> </thead>
</table> <tbody>
</tbody>
</table>
</div>
<% if (ctx.canCreate) { %> <% if (ctx.canCreate) { %>
<p><a href class='add'>Add new category</a></p> <p><a href class='add'>Add new category</a></p>
@ -19,7 +22,7 @@
<div class='messages'></div> <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'> <div class='buttons'>
<input type='submit' class='save' value='Save changes'> <input type='submit' class='save' value='Save changes'>
</div> </div>

View file

@ -17,9 +17,16 @@
<%- ctx.tagCategory.color %> <%- ctx.tagCategory.color %>
<% } %> <% } %>
</td> </td>
<td class='order'>
<% if (ctx.canEditOrder) { %>
<%= ctx.makeNumericInput({value: ctx.tagCategory.order}) %>
<% } else { %>
<%- ctx.tagCategory.order %>
<% } %>
</td>
<td class='usages'> <td class='usages'>
<% if (ctx.tagCategory.name) { %> <% 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 %> <%- ctx.tagCategory.tagCount %>
</a> </a>
<% } else { %> <% } else { %>

View file

@ -1,8 +1,8 @@
<div class='tag-delete'> <div class='tag-delete'>
<form> <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> <li>
<%= ctx.makeCheckbox({ <%= ctx.makeCheckbox({
name: 'confirm-deletion', name: 'confirm-deletion',

View file

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

View file

@ -1,13 +1,15 @@
<div class='tag-merge'> <div class='tag-merge'>
<form> <form>
<ul> <ul class='input'>
<li class='target'> <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>
<li> <li>
<p>Usages in posts, suggestions and implications will be <p>Usages in posts, suggestions and implications will be
merged. Category and aliases need to be handled manually.</p> 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.'}) %> <%= ctx.makeCheckbox({required: true, text: 'I confirm that I want to merge this tag.'}) %>
</li> </li>

View file

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

View file

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

View file

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

View file

@ -1,5 +1,9 @@
<nav id='top-navigation' class='buttons'><!-- <nav id='top-navigation' class='buttons'><!--
--><ul><!-- --><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) { %><!-- --><% for (let item of ctx.items) { %><!--
--><% if (item.available) { %><!-- --><% if (item.available) { %><!--
--><li data-name='<%- item.key %>'><!-- --><li data-name='<%- item.key %>'><!--

View file

@ -2,12 +2,15 @@
<h1><%- ctx.user.name %></h1> <h1><%- ctx.user.name %></h1>
<nav class='buttons'><!-- <nav class='buttons'><!--
--><ul><!-- --><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) { %><!-- --><% 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) { %><!-- --><% 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><!-- --></ul><!--
--></nav> --></nav>

View file

@ -1,16 +1,15 @@
<div id='user-delete'> <div id='user-delete'>
<form> <form>
<div class='input'> <ul class='input'>
<ul> <li>
<li> <%= ctx.makeCheckbox({
<%= ctx.makeCheckbox({ name: 'confirm-deletion',
name: 'confirm-deletion', text: 'I confirm that I want to delete this account.',
text: 'I confirm that I want to delete this account.', required: true,
required: true, }) %>
}) %> </li>
</li> </ul>
</ul>
</div>
<div class='messages'></div> <div class='messages'></div>
<div class='buttons'> <div class='buttons'>
<input type='submit' value='Delete account'/> <input type='submit' value='Delete account'/>

View file

@ -4,44 +4,44 @@
<input class='anticomplete' type='text' name='fakeuser'/> <input class='anticomplete' type='text' name='fakeuser'/>
<input class='anticomplete' type='password' name='fakepass'/> <input class='anticomplete' type='password' name='fakepass'/>
<div class='input'> <ul class='input'>
<ul> <li>
<li> <%= ctx.makeTextInput({
<%= ctx.makeTextInput({ text: 'User name',
text: 'User name', name: 'name',
name: 'name', placeholder: 'letters, digits, _, -',
placeholder: 'letters, digits, _, -', required: true,
required: true, pattern: ctx.userNamePattern,
pattern: ctx.userNamePattern, }) %>
}) %> </li>
</li> <li>
<li> <%= ctx.makePasswordInput({
<%= ctx.makePasswordInput({ text: 'Password',
text: 'Password', name: 'password',
name: 'password', placeholder: '5+ characters',
placeholder: '5+ characters', required: true,
required: true, pattern: ctx.passwordPattern,
pattern: ctx.passwordPattern, }) %>
}) %> </li>
</li> <li>
<li> <%= ctx.makeEmailInput({
<%= ctx.makeEmailInput({ text: 'Email',
text: 'Email', name: 'email',
name: 'email', placeholder: 'optional',
placeholder: 'optional', }) %>
}) %> <p class='hint'>
<p class='hint'> Used for password reminder and to show a <a href='http://gravatar.com/'>Gravatar</a>.
Used for password reminder and to show a <a href='http://gravatar.com/'>Gravatar</a>. Leave blank for random Gravatar.
Leave blank for random Gravatar. </p>
</p> </li>
</li> </ul>
</ul>
</div>
<div class='messages'></div> <div class='messages'></div>
<div class='buttons'> <div class='buttons'>
<input type='submit' value='Create an account'/> <input type='submit' value='Create an account'/>
</div> </div>
</form> </form>
<div class='info'> <div class='info'>
<p>Registered users can:</p> <p>Registered users can:</p>
<ul> <ul>
@ -51,6 +51,6 @@
<li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li> <li><i class='fa fa-star-half-o'></i> vote up/down on posts and comments</li>
</ul> </ul>
<hr/> <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>
</div> </div>

View file

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