mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
232 Commits
Author | SHA1 | Date | |
---|---|---|---|
6660ee77e1 | |||
7f4bebe404 | |||
6a7792239e | |||
f248a6ab4e | |||
f12ce7a7c5 | |||
f8514bfdc7 | |||
01a256bbbc | |||
abae786748 | |||
d3b794c9da | |||
9d15bbfcce | |||
525f05b570 | |||
54d95c11c5 | |||
0a58e12827 | |||
818d9ac3c8 | |||
9eaab55dab | |||
36d2842b6e | |||
87681f8c0d | |||
f49a8fabab | |||
2bba57e8de | |||
f7df1cb536 | |||
2ab636e569 | |||
21ddb8a90b | |||
1ce16c80ec | |||
0eabc4ed41 | |||
965f772515 | |||
770dba8a41 | |||
0ea40ce6d0 | |||
e2bc5d3415 | |||
5df5a78df5 | |||
13d01dee27 | |||
9fd34f06aa | |||
92631df9a4 | |||
e623513e3d | |||
7e2e90ad3f | |||
7645c012a5 | |||
ecb3901bbe | |||
ee09a09833 | |||
13d77dd14a | |||
b8f90dbd95 | |||
5305bb68a4 | |||
5aa75a4150 | |||
b3c5212c84 | |||
96195f0efc | |||
d769eaed61 | |||
2df43201ba | |||
40e869b848 | |||
b7456463eb | |||
d49f76c9f1 | |||
645573a272 | |||
f5aed19bf3 | |||
28bba097c3 | |||
105a564c7d | |||
15739ac7cc | |||
0edbd9bf40 | |||
a31d5849fc | |||
180252cc64 | |||
48bb4fc803 | |||
58768acc1c | |||
42f37d8fee | |||
ec5ff5f230 | |||
7350b89a33 | |||
91f33c9e08 | |||
6b933132a5 | |||
8c87a93774 | |||
7ca582186b | |||
465a61ff4a | |||
1ad5d7475c | |||
ebd25cd9a9 | |||
b3def7fc21 | |||
5a537ba168 | |||
b4db90bcdc | |||
c6a17d33af | |||
37eabe1556 | |||
1969f0e3fa | |||
c0a474ed82 | |||
6380043a9a | |||
b75df289e9 | |||
8db72633f6 | |||
44ef66f65c | |||
7511430b2a | |||
362087ee63 | |||
5ad854e38a | |||
5882998c20 | |||
579e59e7df | |||
64ae9a7c74 | |||
6b6acb0bbf | |||
11648e055c | |||
3c83f711c9 | |||
bd7dd9a2ad | |||
02c8353175 | |||
77e51c2e10 | |||
fd448bac87 | |||
027b98ce76 | |||
edee487ff9 | |||
2702518e31 | |||
79df9b56d3 | |||
3b1544eff3 | |||
c74edbee51 | |||
8407a3f70e | |||
9c1db78b69 | |||
9b2238d423 | |||
b5d6e4837d | |||
d20fe3d95a | |||
a7a2f31dc2 | |||
b26fd88d6f | |||
a69f8563e8 | |||
24d8bf5295 | |||
5412ac14b9 | |||
38bfbfb8f3 | |||
4ba855871f | |||
0727433a9e | |||
627a8db5f3 | |||
740cc85775 | |||
48004f1117 | |||
4126de8e25 | |||
c569504ce7 | |||
8d119d2b62 | |||
f8851bf26d | |||
19e7fa94f7 | |||
06180f5b50 | |||
aa228d5125 | |||
5f4260d0a7 | |||
e7e50cfb3a | |||
09d8e5ae1c | |||
fce9c3483a | |||
a3157a48ec | |||
c35ed15946 | |||
f75b4505a1 | |||
d98474cc6a | |||
2e06422b62 | |||
0aad36228a | |||
7c77c7a87b | |||
0cf29a657a | |||
fdb029eb5c | |||
eb3b02c28d | |||
65bc6705d3 | |||
5f0706c0b4 | |||
e7ea60f293 | |||
b416868aa7 | |||
90406b1278 | |||
04a16a2a36 | |||
72e9400e1d | |||
d425b0df2e | |||
4cad09b85e | |||
a59a57fe70 | |||
0c4d984157 | |||
ea5262fa2b | |||
2ab4da11fc | |||
9090ac6fb9 | |||
eb77b6811a | |||
0945ed64ee | |||
4d9fc51819 | |||
1897297127 | |||
970b9bf06d | |||
e5f2e293f0 | |||
43334b33e1 | |||
839e97b1e2 | |||
8a33b9581d | |||
7067d8e13d | |||
5769034223 | |||
c0a5c800e0 | |||
b70231bd7e | |||
bc757dd883 | |||
924592675c | |||
a3aea27a13 | |||
3c54671aeb | |||
d8df51f0c0 | |||
b693a5f4b3 | |||
303f91e15c | |||
c350c47195 | |||
24ce67b4ff | |||
06d7c19556 | |||
728d1d65de | |||
4b27b8a85d | |||
bfe31d87a1 | |||
2fd371b10a | |||
f1647a5f7b | |||
cd688b25a3 | |||
a7c6e9f043 | |||
997c2a10ec | |||
95cf0ca37b | |||
116522498d | |||
01a84ee4e2 | |||
2458935fdf | |||
f2b1e3bedb | |||
77c51d9a8a | |||
d8d65ed24c | |||
58d3129548 | |||
0b032e7f94 | |||
4e6fe634e1 | |||
b14f02810e | |||
796c2d1b1f | |||
40197d6c39 | |||
736c0a66ff | |||
48230a64ad | |||
da6b37b14c | |||
4b4ccf365a | |||
2b0a4d1f76 | |||
2d1b5308f3 | |||
8fb1b87ae5 | |||
9621810332 | |||
bd33b09f7b | |||
3245c75187 | |||
e38152b921 | |||
2195b2c9a1 | |||
969f70318b | |||
06cc776438 | |||
76edbfeddb | |||
9de6e7e739 | |||
d8a4e1ec4e | |||
193d1c5f7a | |||
7ff961fc21 | |||
a11436aa8c | |||
a3b02adb7f | |||
40b16f586b | |||
602d7a1f45 | |||
333c538f1e | |||
7c182f57a0 | |||
2a7ca79b2d | |||
9e894bc41c | |||
cdd2726f30 | |||
029f0f00a4 | |||
c6fe7a4320 | |||
8bd4ae27c2 | |||
2a215ef51b | |||
6473ed74d3 | |||
44a4184eb8 | |||
847f248829 | |||
a0133ea632 | |||
0b15ca1b05 | |||
2996f27671 | |||
6a751ed0b2 |
201
INSTALL.md
201
INSTALL.md
@ -1,28 +1,40 @@
|
||||
Prerequisites
|
||||
-------------
|
||||
|
||||
In order to run szurubooru, you need to have installed following software:
|
||||
In order to run `szurubooru`, you need to have installed following software:
|
||||
|
||||
- Apache2
|
||||
- mod_rewrite
|
||||
- mod_mime_magic (recommended)
|
||||
- PHP 5.6.0
|
||||
- pdo_sqlite
|
||||
- imagick or gd
|
||||
- composer (PHP package manager)
|
||||
- npm (node.js package manager)
|
||||
- `Apache` 2.4+
|
||||
- `mod_rewrite`
|
||||
- `mod_mime_magic` (recommended)
|
||||
- `PHP` 5.6.0+
|
||||
- `pdo_mysql`
|
||||
- `imagick` or `gd`
|
||||
- `MySQL` or `MariaDB`
|
||||
- `composer` (`PHP` package manager)
|
||||
- `npm` (`node.js` package manager)
|
||||
|
||||
Optional modules:
|
||||
Optional software:
|
||||
|
||||
- dump-gnash or swfrender for flash thumbnails
|
||||
- ffmpegthumbnailer or ffmpeg for video thumbnails
|
||||
- `dump-gnash`, `swfrender` or `ffmpeg` for Flash thumbnails
|
||||
- `ffmpegthumbnailer` or `ffmpeg` for video thumbnails
|
||||
|
||||
|
||||
|
||||
Cloning the repository
|
||||
----------------------
|
||||
|
||||
Download the repository somewhere you will it run from, or better yet, clone it
|
||||
with `git`:
|
||||
|
||||
cd /srv/www/
|
||||
git clone https://github.com/rr-/szurubooru booru-test
|
||||
|
||||
|
||||
|
||||
Fetching dependencies
|
||||
---------------------
|
||||
|
||||
To fetch dependencies that szurubooru needs in order to run, enter following
|
||||
To fetch dependencies that `szurubooru` needs in order to run, enter following
|
||||
commands in the terminal:
|
||||
|
||||
composer update
|
||||
@ -30,11 +42,11 @@ commands in the terminal:
|
||||
|
||||
|
||||
|
||||
Running grunt tasks
|
||||
-------------------
|
||||
Running `grunt` tasks
|
||||
---------------------
|
||||
|
||||
Szurubooru uses grunt to run tasks like database ugprades and tests. In order
|
||||
to use grunt from the terminal, you can use:
|
||||
`szurubooru` uses `grunt` to run tasks like database upgrades and tests. In
|
||||
order to use `grunt` from the terminal, you can use:
|
||||
|
||||
node_modules/grunt-cli/bin/grunt [TASK]
|
||||
|
||||
@ -43,25 +55,25 @@ administrator:
|
||||
|
||||
npm install -g grunt-cli
|
||||
|
||||
This will add "grunt" to your PATH, making things much more human-friendly.
|
||||
This will add `grunt` to your PATH, making things much more human-friendly.
|
||||
|
||||
grunt [TASK]
|
||||
|
||||
|
||||
|
||||
Enabling required modules in PHP
|
||||
--------------------------------
|
||||
Enabling required modules in `PHP`
|
||||
----------------------------------
|
||||
|
||||
Enable required modules in php.ini (or other configuration file, depending on
|
||||
Enable required modules in `php.ini` (or other configuration file, depending on
|
||||
your setup):
|
||||
|
||||
;Linux
|
||||
extension=pdo_sqlite.so
|
||||
extension=pdo_mysql.so
|
||||
|
||||
;Windows
|
||||
extension=php_pdo_sqlite.dll
|
||||
extension=php_pdo_mysql.dll
|
||||
|
||||
In order to draw thumbnails, szurubooru needs either imagick or gd2:
|
||||
In order to draw thumbnails, `szurubooru` needs either `Imagick` or `gd2`:
|
||||
|
||||
;Linux
|
||||
extension=imagick.so
|
||||
@ -73,25 +85,16 @@ In order to draw thumbnails, szurubooru needs either imagick or gd2:
|
||||
|
||||
|
||||
|
||||
Upgrading the database
|
||||
----------------------
|
||||
|
||||
Every time database schema changes, you should upgrade the database by running
|
||||
following grunt task in the terminal:
|
||||
|
||||
grunt upgrade
|
||||
|
||||
|
||||
|
||||
Creating virtual server in Apache
|
||||
---------------------------------
|
||||
|
||||
In order to make Szurubooru visible in your browser, you need to create a
|
||||
virtual server. This guide focuses on Apache2 web server. Note that although it
|
||||
should be also possible to host szurubooru with nginx, you'd need to manually
|
||||
translate the rules inside public_html/.htaccess into nginx configuration.
|
||||
In order to make `szurubooru` visible in your browser, you need to create a
|
||||
virtual server. This guide focuses on `Apache` web server. Note that although
|
||||
it should be also possible to host `szurubooru` with `nginx`, you'd need to
|
||||
manually translate the rules inside `public_html/.htaccess` into `nginx`
|
||||
configuration.
|
||||
|
||||
Creating virtual server for Apache comes with no surprises, basically all you
|
||||
Creating virtual server for `Apache` comes with no surprises, basically all you
|
||||
need is the most basic configuration:
|
||||
|
||||
<VirtualHost *:80>
|
||||
@ -99,30 +102,34 @@ need is the most basic configuration:
|
||||
DocumentRoot /path/to/szurubooru/public_html/
|
||||
</VirtualHost>
|
||||
|
||||
ServerName specifies the domain under which szurubooru will be hosted.
|
||||
DocumentRoot should point to the public_html/ directory.
|
||||
`ServerName` specifies the domain under which `szurubooru` will be hosted.
|
||||
`DocumentRoot` should point to the `public_html/` directory.
|
||||
|
||||
Some environments / configurations require extra steps to make things work - in
|
||||
case you experience any problems, please consult the troubleshooting section
|
||||
later in this file.
|
||||
|
||||
|
||||
|
||||
Enabling required modules in Apache
|
||||
-----------------------------------
|
||||
|
||||
Enable required modules in httpd.conf (or other configuration file, depending
|
||||
Enable required modules in `httpd.conf` (or other configuration file, depending
|
||||
on your setup):
|
||||
|
||||
LoadModule rewrite_module mod_rewrite.so ;Linux
|
||||
LoadModule rewrite_module modules/mod_rewrite.so ;Windows
|
||||
|
||||
Enable PHP support:
|
||||
Enable `PHP` support:
|
||||
|
||||
LoadModule php5_module /usr/lib/apache2/modules/libphp5.so ;Linux
|
||||
LoadModule php5_module /path/to/php/php5apache2_4.dll ;Windows
|
||||
AddType application/x-httpd-php .php
|
||||
PHPIniDir /path/to/php/
|
||||
|
||||
Enable MIME auto-detection (not required, but recommended - szurubooru doesn't
|
||||
use file extensions, and reporting correct Content-Type to browser is always a
|
||||
good thing):
|
||||
Enable MIME auto-detection (not required, but recommended - `szurubooru`
|
||||
doesn't use file extensions, and reporting correct `Content-Type` to browser is
|
||||
always a good thing):
|
||||
|
||||
;Linux
|
||||
LoadModule mime_magic_module mod_mime_magic.so
|
||||
@ -137,21 +144,40 @@ good thing):
|
||||
</IfModule>
|
||||
|
||||
|
||||
Creating administrator account
|
||||
------------------------------
|
||||
|
||||
By now, you should be able to view szurubooru in the browser. Registering
|
||||
administrator account is simple - the first user to create an account
|
||||
automatically becomes administrator and doesn't need e-mail activation.
|
||||
|
||||
|
||||
|
||||
Overwriting configuration
|
||||
-------------------------
|
||||
|
||||
Everything that can be configured is stored in data/config.ini file. In order
|
||||
to make changes there, copy the file and name it local.ini. Make sure you don't
|
||||
edit the file itself, especially if you want to contribute.
|
||||
Everything that can be configured is stored in `data/config.ini` file. In order
|
||||
to make changes there, copy the file and name it `local.ini` and place it in
|
||||
`data/` directory as well. Make sure you don't edit the `data/config.ini` file
|
||||
itself, especially if you want to contribute.
|
||||
|
||||
|
||||
|
||||
Setting up the database
|
||||
-----------------------
|
||||
|
||||
Before running `szurubooru` for first time, you need to set up the database.
|
||||
`szurubooru` uses MySQL, so let's fire `mysql` and type following:
|
||||
|
||||
create user 'maria' identified by 'arkadia';
|
||||
create database booru_test;
|
||||
grant all privileges on *.* to 'maria'@'%' with grant option;
|
||||
|
||||
Then you need to provide the above credentials in the configuration files as
|
||||
described in the previous section. Example `local.ini` file:
|
||||
|
||||
[database]
|
||||
dsn = mysql:dbname=booru_test
|
||||
user = maria
|
||||
password = arkadia
|
||||
|
||||
After that, upgrade the database using following command:
|
||||
|
||||
grunt upgrade
|
||||
|
||||
This should be also executed every time database schema changes.
|
||||
|
||||
|
||||
|
||||
@ -165,21 +191,36 @@ smallest possible packages, run following command:
|
||||
|
||||
grunt build
|
||||
|
||||
This should create public_html/app.min.js, public_html/app.min.css and
|
||||
public_html/app.min.html. .htaccess is configured so that if these files exist,
|
||||
it will load them instead of development environment. To delete these
|
||||
This should create `public_html/app.min.js`, `public_html/app.min.css` and
|
||||
`public_html/app.min.html`. `.htaccess` is configured so that if these files
|
||||
exist, it will load them instead of development environment. To delete these
|
||||
conveniently, you can run:
|
||||
|
||||
grunt clean
|
||||
|
||||
If, for any reason, you do not wish to minify the resources, you should at
|
||||
least copy the dependencies fetched before to the `public_html/` directory with
|
||||
following:
|
||||
|
||||
grunt copy
|
||||
|
||||
|
||||
|
||||
Creating administrator account
|
||||
------------------------------
|
||||
|
||||
By now, you should be able to view `szurubooru` in the browser. Registering
|
||||
administrator account is simple - the first user to create an account
|
||||
automatically becomes administrator and doesn't need e-mail activation.
|
||||
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
1. Problems with Apache virtual servers
|
||||
1. Problems with `Apache` virtual servers
|
||||
|
||||
After reloading Apache configuration, if you find yourself unable to
|
||||
After reloading `Apache` configuration, if you find yourself unable to
|
||||
connect to the server, make sure that connections are open, for example,
|
||||
like this:
|
||||
|
||||
@ -187,40 +228,28 @@ Troubleshooting
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
(Note that Apache versions prior to 2.4 used "Allow from all" directive.)
|
||||
(Note that `Apache` versions prior to 2.4 used `Allow from all` directive.)
|
||||
|
||||
Additionally, in order to access virtual host from your machine, make sure
|
||||
the domain name "example.com" supplied in <VirtualHost/> section is
|
||||
included in your hosts file (usually /etc/hosts on Linux and
|
||||
C:/windows/system32/drivers/etc/hosts in Windows).
|
||||
Additionally, in order to access the virtual host from your machine, make
|
||||
sure the domain name `example.com` supplied in `<VirtualHost/>` section is
|
||||
included in your `hosts` file (usually `/etc/hosts` on Linux and
|
||||
`C:/windows/system32/drivers/etc/hosts` on Windows).
|
||||
|
||||
If the site doesn't work for you, make sure Apache can parse .htaccess
|
||||
files. If it can't, you need to set AllowOverride option to "yes", for
|
||||
example by putting following snippet inside <VirtualHost/> section:
|
||||
If the site doesn't work for you, make sure `Apache` can parse `.htaccess`
|
||||
files. If it can't, you need to set `AllowOverride` option to `yes`, for
|
||||
example by putting following snippet inside the `<VirtualHost/>` section:
|
||||
|
||||
<Directory /path/to/szurubooru/public_html/>
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
|
||||
2. Problems with PHP modules or registration
|
||||
2. Problems with `PHP` modules or registration
|
||||
|
||||
Make sure your php.ini path is correct. Make sure all the modules are
|
||||
actually loaded by inspecting phpinfo - create small file containing:
|
||||
Make sure your `php.ini` path is correct. Make sure all the modules are
|
||||
actually loaded by inspecting results of `phpinfo()` call - create small
|
||||
file containing:
|
||||
|
||||
<?php phpinfo(); ?>
|
||||
|
||||
Then, run it in your browser and inspect the output, looking for missing
|
||||
modules that were supposed to be loaded.
|
||||
|
||||
3. "Attempt to write to read-only database"
|
||||
|
||||
Make sure Apache has permission to access the database file AND directory
|
||||
it's stored in. (SQLite writes temporary journal files to the parent
|
||||
database directory). If you're the only user of the system, you can run
|
||||
these commands without worrying too much:
|
||||
|
||||
chmod 0777 data/
|
||||
chmod 0777 data/db.sqlite
|
||||
|
||||
Otherwise, if you're feeling fancy, you can experiment with setfacl on
|
||||
Linux or group policies on Windows.
|
||||
|
19
README.md
19
README.md
@ -5,19 +5,21 @@ szurubooru
|
||||
|
||||
## What is it?
|
||||
|
||||
Szurubooru is a Danbooru-style board, a gallery where users can upload, browse,
|
||||
tag and comment images, video clips and flash animations.
|
||||
`szurubooru` is a Danbooru-style board, a gallery where users can upload,
|
||||
browse, tag and comment images, video clips and flash animations.
|
||||
|
||||
Its name have its roots in Polish language and has onomatopoeic meaning of
|
||||
scraping or scrubbing. It is pronounced *"shoorubooru"* [ˌʃuruˈburu].
|
||||
|
||||
## Licensing
|
||||
|
||||
Please see the file named [`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
|
||||
Please see the file named
|
||||
[`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
|
||||
|
||||
## Installation
|
||||
|
||||
Please see the file named [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
|
||||
Please see the file named
|
||||
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
@ -29,8 +31,8 @@ please do following:
|
||||
to your problem, comment on that issue instead of opening a new one.
|
||||
2. If you found an issue and the issue is closed, feel free to reopen it.
|
||||
3. If you're reporting a bug, create an isolated and reproducible scenario.
|
||||
4. If you're filing a feature request, provide examples - what might be obvious
|
||||
to you, might not be so obvious to the developers.
|
||||
4. If you're filing a feature request, provide examples - what might be
|
||||
obvious to you, might not be so obvious to the developers.
|
||||
|
||||
## Contributing the code
|
||||
|
||||
@ -40,13 +42,14 @@ Here are some guidelines on how to contribute:
|
||||
- Respect coding standards - be consistent with existing code base.
|
||||
- Watch your whitespace - don't leave any characters at the end of the lines.
|
||||
- Always run tests before pushing.
|
||||
- Before starting, see [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
|
||||
- Before starting, see
|
||||
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
|
||||
- Use `grunt` to do automatic tasks like minifying Javascript files or running
|
||||
tests. Run `grunt --help` to see full list of available tasks.
|
||||
|
||||
## API
|
||||
|
||||
Szurubooru from version 0.9+ uses REST API. Currently there is no formal
|
||||
`szurubooru` from version 0.9+ uses REST API. Currently there is no formal
|
||||
documentation; source code behind REST layer lies in `src/Controllers/`
|
||||
directory. In order to use the API, bear in mind that you need to:
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"require": {
|
||||
"mnapoli/php-di": "~4.4"
|
||||
"mnapoli/php-di": "~4.4",
|
||||
"phpmailer/phpmailer": "~5.2"
|
||||
},
|
||||
|
||||
"require-dev": {
|
||||
|
@ -3,8 +3,12 @@ serviceName = szurubooru
|
||||
serviceBaseUrl = http://localhost/
|
||||
|
||||
[mail]
|
||||
botName = szurubooru bot
|
||||
botAddress = noreply@localhost
|
||||
smtpHost = localhost
|
||||
smtpPort = 25
|
||||
smtpUserName = bot
|
||||
smtpUserPass = groovy123
|
||||
smtpFrom = noreply@szurubooru
|
||||
smtpFromName = szurubooru bot
|
||||
passwordResetSubject = szurubooru - password reset
|
||||
passwordResetBodyPath = mail/password-reset.txt
|
||||
activationSubject = szurubooru - account activation
|
||||
@ -19,7 +23,7 @@ maxCustomThumbnailSize = 1048576 ;1mb
|
||||
|
||||
[database.tests]
|
||||
dsn = mysql:host=localhost
|
||||
user = szuru_test
|
||||
user = szuru-test
|
||||
password = cat
|
||||
|
||||
[security]
|
||||
@ -27,12 +31,13 @@ secret = change
|
||||
minPasswordLength = 5
|
||||
needEmailActivationToRegister = 1
|
||||
defaultAccessRank = restrictedUser
|
||||
forceHttpInPermalinks = 0
|
||||
|
||||
[security.privileges]
|
||||
register = anonymous
|
||||
listUsers = regularUser, powerUser, moderator, administrator
|
||||
viewUsers = regularUser, powerUser, moderator, administrator
|
||||
deleteOwnAccount = regularUser, powerUser, moderator, administrator
|
||||
deleteOwnAccount = restrictedUser, regularUser, powerUser, moderator, administrator
|
||||
deleteAllAccounts = administrator
|
||||
changeOwnName = regularUser, powerUser, moderator, administrator
|
||||
changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator
|
||||
@ -94,12 +99,13 @@ usersPerPage = 20
|
||||
postsPerPage = 40
|
||||
|
||||
[tags]
|
||||
categories[] = meta
|
||||
categories[] = artist
|
||||
categories[] = character
|
||||
categories[] = copyright
|
||||
categories[] = 'meta, meta, #aaa'
|
||||
categories[] = 'artist, artist, #a00'
|
||||
categories[] = 'character, character, #0a0'
|
||||
categories[] = 'copyright, copyright, #a0a'
|
||||
|
||||
[misc]
|
||||
thumbnailCropStyle = outside
|
||||
customFaviconUrl = /favicon.png
|
||||
dumpSqlIntoQueries = 0
|
||||
imageExtension = imagick
|
||||
|
@ -81,12 +81,13 @@ module.exports = function(grunt) {
|
||||
files: [
|
||||
{ src: 'node_modules/jquery/dist/jquery.min.js', dest: 'public_html/lib/jquery.min.js' },
|
||||
{ src: 'node_modules/jquery.cookie/jquery.cookie.js', dest: 'public_html/lib/jquery.cookie.js' },
|
||||
{ src: 'node_modules/Mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
|
||||
{ src: 'node_modules/mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
|
||||
{ src: 'node_modules/pathjs/path.js', dest: 'public_html/lib/path.js' },
|
||||
{ src: 'node_modules/underscore/underscore-min.js', dest: 'public_html/lib/underscore.min.js' },
|
||||
{ src: 'node_modules/marked/lib/marked.js', dest: 'public_html/lib/marked.js' },
|
||||
{ src: 'node_modules/nprogress/nprogress.js', dest: 'public_html/lib/nprogress.js' },
|
||||
{ src: 'node_modules/nprogress/nprogress.css', dest: 'public_html/lib/nprogress.css' },
|
||||
{ cwd: 'node_modules', src: 'font-awesome/**/*', dest: 'public_html/lib/', expand: true },
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -136,7 +137,7 @@ module.exports = function(grunt) {
|
||||
templates: readTemplates(grunt),
|
||||
timestamp: grunt.template.today('isoDateTime'),
|
||||
maxPostSize: config.database.maxPostSize,
|
||||
tagCategories: config.tags.categories,
|
||||
tagCategories: config.tags.categories.map(function(s) { return s.split(/,\s*/); }),
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
@ -162,7 +163,7 @@ module.exports = function(grunt) {
|
||||
});
|
||||
|
||||
grunt.registerTask('update', 'Upgrade database to newest version.', function() {
|
||||
exec('php scripts/upgrade.php');
|
||||
exec('php scripts/upgrade');
|
||||
});
|
||||
grunt.registerTask('upgrade', ['update']);
|
||||
|
||||
|
29
package.json
29
package.json
@ -1,24 +1,25 @@
|
||||
{
|
||||
"name": "szurubooru",
|
||||
"version": "0.9.0",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jquery.cookie": "1.4.1",
|
||||
"jquery": "~2.1.1",
|
||||
"underscore": "1.7.0",
|
||||
"Mousetrap": "git://github.com/ccampbell/mousetrap.git",
|
||||
"marked": "~0.3.2",
|
||||
"nprogress": "git://github.com/rstacruz/nprogress.git",
|
||||
|
||||
"requirejs": "*",
|
||||
"ini": "*",
|
||||
"font-awesome": "^4.3.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-processhtml": "*",
|
||||
"grunt-contrib-uglify": "*",
|
||||
"grunt-cli": "*",
|
||||
"grunt-contrib-copy": "*",
|
||||
"grunt-contrib-cssmin": "*",
|
||||
"grunt-contrib-jshint": "~0.10.0",
|
||||
"grunt-contrib-copy": "*",
|
||||
"grunt-cli": "*",
|
||||
"grunt-contrib-uglify": "*",
|
||||
"grunt-processhtml": "*",
|
||||
"ini": "*",
|
||||
"jquery": "~2.1.1",
|
||||
"jquery.cookie": "1.4.1",
|
||||
"marked": "~0.3.2",
|
||||
"nprogress": "git://github.com/rstacruz/nprogress.git",
|
||||
"requirejs": "*",
|
||||
"rimraf": "~2.1",
|
||||
"shelljs": "~0.3.0",
|
||||
"rimraf": "~2.1"
|
||||
"underscore": "1.7.0"
|
||||
}
|
||||
}
|
||||
|
@ -145,12 +145,6 @@
|
||||
<!-- Indentation -->
|
||||
<!-- **************** -->
|
||||
|
||||
<!-- Tests to make sure that a line does not contain the tab character. -->
|
||||
<test name="indentation"> <!-- noTabs -->
|
||||
<property name="type" value="tabs"/> <!-- tabs or spaces -->
|
||||
<property name="number" value="4"/> <!-- number of spaces if type = spaces -->
|
||||
</test>
|
||||
|
||||
<!-- Check the position of the open curly brace in a control structure (if) -->
|
||||
<!-- sl = same line -->
|
||||
<!-- nl = new line -->
|
||||
|
@ -4,6 +4,9 @@ DirectoryIndex index.html
|
||||
ErrorDocument 404 /404.html
|
||||
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
|
||||
RewriteRule ^(.*)$ http://%1/$1 [R=301,L]
|
||||
|
||||
RewriteRule ^/?404.html$ /#/404 [NE,R,L]
|
||||
|
||||
|
@ -10,27 +10,33 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.comments ul {
|
||||
ul.comments {
|
||||
list-style-type: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comment ul {
|
||||
list-style-position: inside;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
margin: 0 0 1em 0;
|
||||
padding: 0;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.comment .avatar {
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.2em;
|
||||
margin-right: 0.75em;
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.comment .content {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.comment .content p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -83,14 +89,18 @@
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
#global-comment-list .post-comment {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
@media all and (max-width: 40em) {
|
||||
#global-comment-list .post-comment {
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
#global-comment-list .post {
|
||||
-webkit-flex-shrink: 0;
|
||||
-webkit-flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin-right: 1em;
|
||||
@ -100,6 +110,19 @@
|
||||
#global-comment-list .comments>h1 {
|
||||
display: none;
|
||||
}
|
||||
#global-comment-list .post-small a {
|
||||
#global-comment-list .post-small .link {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sjis {
|
||||
font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif;
|
||||
background: #fbfbfb;
|
||||
color: #111;
|
||||
font-size: 12pt;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
}
|
||||
|
@ -5,13 +5,19 @@ body {
|
||||
background: #fff;
|
||||
color: #555;
|
||||
font-family: 'Droid Sans', sans-serif;
|
||||
font-size: 17px;
|
||||
font-size: 15px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@media all and (max-width: 40em) {
|
||||
body {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: normal;
|
||||
font-size: 30px;
|
||||
font-size: 160%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@ -21,7 +27,11 @@ h2 {
|
||||
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 87%;
|
||||
}
|
||||
|
||||
#middle {
|
||||
|
@ -40,7 +40,7 @@ input[type=password] {
|
||||
box-shadow: 0 1px 2px -1px #e0e0e0 inset;
|
||||
background: #fafafa;
|
||||
font-family: 'Inconsolata', monospace;
|
||||
font-size: 17px;
|
||||
font-size: 100%;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
@ -200,7 +200,6 @@ input[type=checkbox]:focus + label {
|
||||
font-family: 'Droid Sans', sans-serif;
|
||||
margin: 1px;
|
||||
padding: 2px 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.tag-input input {
|
||||
border: none;
|
||||
@ -210,13 +209,13 @@ input[type=checkbox]:focus + label {
|
||||
color: black;
|
||||
}
|
||||
.tag-input li a.close {
|
||||
font-size: 14px;
|
||||
margin-left: 0.75em;
|
||||
font-size: 85%;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.related-tags {
|
||||
line-height: 200%;
|
||||
font-size: 15px;
|
||||
font-size: 95%;
|
||||
display: none;
|
||||
margin: 0.5em 0.5em 1em 0.5em;
|
||||
}
|
||||
|
@ -10,18 +10,21 @@
|
||||
}
|
||||
|
||||
#home .post {
|
||||
text-align: left;
|
||||
text-align: center;
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
max-width: 60%;
|
||||
min-width: 40em;
|
||||
}
|
||||
#home .post .left {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
#home .post .right {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
#home .post-footer,
|
||||
@ -35,5 +38,7 @@
|
||||
|
||||
#home .version {
|
||||
opacity: .4;
|
||||
font-size: 12px;
|
||||
}
|
||||
#home .subheader, #home .post-footer {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.message {
|
||||
margin: 0 auto 0.2em auto;
|
||||
margin: 1em auto;
|
||||
padding: 0.4em 0.5em;
|
||||
text-align: center;
|
||||
max-width: 40em;
|
||||
|
@ -52,10 +52,16 @@
|
||||
.post-list ul.safety .safety-unsafe.disabled:before { background: linear-gradient(#DDB7B7, #C9A195); }
|
||||
|
||||
.post-list ul.posts {
|
||||
display: -webkit-flex;
|
||||
-webkit-justify-content: center;
|
||||
-webkit-align-content: center;
|
||||
-webkit-flex-wrap: wrap;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@ -68,9 +74,9 @@
|
||||
.post-small {
|
||||
position: relative;
|
||||
}
|
||||
.post-small a {
|
||||
display: inline-block;
|
||||
margin: 0.2em;
|
||||
.post-small .link {
|
||||
display: block;
|
||||
margin: 0.3em;
|
||||
border: 1px solid #999;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
@ -82,20 +88,20 @@
|
||||
}
|
||||
|
||||
|
||||
.post-small a:focus,
|
||||
.post-small a:hover {
|
||||
.post-small .link:focus,
|
||||
.post-small .link:hover {
|
||||
background: #64C2ED;
|
||||
border-color: #64C2ED;
|
||||
box-shadow: 0 0 0 2px #64C2ED;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.post-small a:focus img:not(.loading),
|
||||
.post-small a:hover img:not(.loading) {
|
||||
.post-small .link:focus img:not(.loading),
|
||||
.post-small .link:hover img:not(.loading) {
|
||||
opacity: .8 !important;
|
||||
}
|
||||
|
||||
.post-small a .info {
|
||||
.post-small .link .info {
|
||||
display: none;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
@ -105,22 +111,22 @@
|
||||
background: #64C2ED;
|
||||
color: black;
|
||||
}
|
||||
.post-small a .info ul {
|
||||
.post-small .link .info ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.post-small a .info li {
|
||||
.post-small .link .info li {
|
||||
display: inline-block;
|
||||
margin: 0.1em 0.5em;
|
||||
padding: 0;
|
||||
}
|
||||
.post-small a:focus .info,
|
||||
.post-small a:hover .info {
|
||||
.post-small .link:focus .info,
|
||||
.post-small .link:hover .info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-small:not(.post-type-image) a::before {
|
||||
.post-small:not(.post-type-image) .link::before {
|
||||
display: block;
|
||||
content: '';
|
||||
z-index: 2;
|
||||
@ -133,7 +139,8 @@
|
||||
border-left: 50px solid transparent;
|
||||
}
|
||||
|
||||
.post-small:not(.post-type-image) a::after {
|
||||
.post-small:not(.post-type-image) .link::after {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
content: '...';
|
||||
z-index: 3;
|
||||
@ -148,16 +155,19 @@
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
}
|
||||
.post-small.post-type-youtube a::after {
|
||||
.post-small.post-type-youtube .link::after {
|
||||
font-size: 13px;
|
||||
content: 'youtube';
|
||||
}
|
||||
.post-small.post-type-video a::after {
|
||||
.post-small.post-type-video .link::after {
|
||||
content: 'video';
|
||||
}
|
||||
.post-small.post-type-flash a::after {
|
||||
.post-small.post-type-flash .link::after {
|
||||
content: 'flash';
|
||||
}
|
||||
.post-small.post-type-animation .link::after {
|
||||
content: 'anim';
|
||||
}
|
||||
|
||||
.post-small .action {
|
||||
display: none;
|
||||
|
@ -14,7 +14,7 @@
|
||||
position: relative;
|
||||
}
|
||||
#post-upload-step1 .url-handler .input-wrapper {
|
||||
margin-right: 8.5em;
|
||||
margin-right: 9.5em;
|
||||
}
|
||||
#post-upload-step1 .url-handler button {
|
||||
position: absolute;
|
||||
@ -118,14 +118,14 @@
|
||||
text-align: left;
|
||||
}
|
||||
#post-upload-step2 .messages {
|
||||
margin-bottom: 1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
#post-upload-step2 .form-slider {
|
||||
text-align: center;
|
||||
}
|
||||
#post-upload-step2 .form-slider .thumbnail img {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
max-height: 450px;
|
||||
margin: 0 auto 1em auto;
|
||||
}
|
||||
|
||||
@ -140,35 +140,6 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#lightbox {
|
||||
display: none;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#lightbox img {
|
||||
max-width: 400px;
|
||||
max-height: 400px;
|
||||
background: white;
|
||||
border: 0.5em solid white;
|
||||
box-shadow: 0 0 0 1px #eee;
|
||||
position: relative;
|
||||
}
|
||||
#lightbox:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: white;
|
||||
border-left: 1px solid #eee;
|
||||
border-bottom: 1px solid #eee;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
#uploading-alert {
|
||||
display: none;
|
||||
text-align: left;
|
||||
|
@ -1,24 +1,3 @@
|
||||
.post-type-video video {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.post-type-image .image-wrapper {
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.post-type-image .image-wrapper img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-type-youtube iframe {
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
#post-current-search-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
@ -41,45 +20,52 @@
|
||||
|
||||
#post-view-wrapper #sidebar {
|
||||
line-height: 1.33em;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
#post-view-wrapper #sidebar h1 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
#post-view-wrapper #sidebar h1:first-of-type {
|
||||
margin-top: 0;
|
||||
#post-view-wrapper #sidebar .box {
|
||||
margin-bottom: 1.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media all and (min-width: 62.5em) {
|
||||
#post-view-wrapper {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#post-view-wrapper #sidebar {
|
||||
min-width: 15em;
|
||||
margin-right: 1em;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#post-view-wrapper #post-view {
|
||||
-webkit-flex: 5;
|
||||
flex: 5;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 62.5em) {
|
||||
#post-view-wrapper {
|
||||
display: -webkit-flex;
|
||||
-webkit-flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#post-view-wrapper #sidebar {
|
||||
order: 2;
|
||||
margin-bottom: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
#post-view-wrapper #sidebar .box {
|
||||
display: inline-block;
|
||||
width: 15em;
|
||||
vertical-align: top;
|
||||
}
|
||||
#post-view-wrapper #post-view {
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
order: 1;
|
||||
}
|
||||
}
|
||||
@ -135,11 +121,19 @@
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
#sidebar .fit-mode a {
|
||||
opacity: .25;
|
||||
}
|
||||
#sidebar .fit-mode a.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#sidebar .essential {
|
||||
display: -webkit-flex;
|
||||
-webkit-justify-content: space-around;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 2em;
|
||||
max-width: 30em;
|
||||
}
|
||||
#sidebar .essential li {
|
||||
display: block;
|
||||
@ -147,26 +141,34 @@
|
||||
vertical-align: top;
|
||||
}
|
||||
#sidebar .essential li i.fa {
|
||||
font-size: 30px;
|
||||
font-size: 200%;
|
||||
}
|
||||
#sidebar .essential li a {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-size: 87%;
|
||||
}
|
||||
|
||||
#post-view #post-edit-target {
|
||||
padding: 1em;
|
||||
width: 50%;
|
||||
min-width: 30em;
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 0 1em 0.5em rgba(255, 255, 255, 0.8);
|
||||
z-index: 2;
|
||||
display: none;
|
||||
}
|
||||
#post-edit-target .form-wrapper {
|
||||
min-width: 100%;
|
||||
}
|
||||
#post-view>* {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#post-edit-target .advanced-trigger .form-input {
|
||||
overflow: auto; /* fix browser's outline around the link being cut due to overflow: hidden; */
|
||||
}
|
||||
#post-edit-target .file-handler {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
@ -181,6 +183,25 @@
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.post-content .object-wrapper {
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.post-content .object-wrapper img,
|
||||
.post-content .object-wrapper object,
|
||||
.post-content .object-wrapper iframe,
|
||||
.post-content .object-wrapper video {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
border: 0;
|
||||
}
|
||||
.post-content .object-wrapper video {
|
||||
background: black;
|
||||
}
|
||||
|
||||
.post-notes-target {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
@ -220,10 +241,16 @@
|
||||
}
|
||||
|
||||
.post-note {
|
||||
outline: 0;
|
||||
pointer-events: auto;
|
||||
position: absolute;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
font-size: 12pt;
|
||||
}
|
||||
.post-note:focus {
|
||||
border-color: rgba(255, 0, 0, 0.3);
|
||||
background-color: rgba(255, 225, 225, 0.3);
|
||||
}
|
||||
.post-note .text-wrapper {
|
||||
position: absolute;
|
||||
@ -236,15 +263,13 @@
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
max-width: 22.5em;
|
||||
}
|
||||
.post-note .text {
|
||||
padding: 0.5em;
|
||||
background: lemonchiffon;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.post-note:hover .text-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-note .text p:first-of-type {
|
||||
margin-top: 0;
|
||||
|
@ -78,19 +78,6 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tag-category-character,
|
||||
.tag-category-character a {
|
||||
color: #0a0;
|
||||
}
|
||||
.tag-category-copyright,
|
||||
.tag-category-copyright a {
|
||||
color: #a0a;
|
||||
}
|
||||
.tag-category-artist,
|
||||
.tag-category-artist a {
|
||||
color: #a00;
|
||||
}
|
||||
.tag-category-meta,
|
||||
.tag-category-meta a {
|
||||
color: #aaa;
|
||||
*[class*='tag-category-']:not(.tag-category-default) a {
|
||||
color: inherit;
|
||||
}
|
||||
|
@ -28,7 +28,7 @@
|
||||
line-height: normal;
|
||||
}
|
||||
#tag-view small {
|
||||
font-size: 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
#tag-view .siblings ul {
|
||||
|
@ -17,7 +17,7 @@
|
||||
text-transform: lowercase;
|
||||
font-variant: small-caps;
|
||||
padding: 0.5em 1em;
|
||||
font-size: 15px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#top-navigation li a:focus,
|
||||
@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
#top-navigation i {
|
||||
font-size: 40px;
|
||||
font-size: 3em;
|
||||
margin: 0 10px 5px;
|
||||
}
|
||||
|
||||
|
@ -39,12 +39,14 @@
|
||||
|
||||
#user-list .user img {
|
||||
vertical-align: top;
|
||||
margin-right: 1em;
|
||||
display: block;
|
||||
}
|
||||
#user-list .user>a {
|
||||
display: block;
|
||||
#user-list .user .avatar {
|
||||
float: left;
|
||||
margin-right: 1em;
|
||||
}
|
||||
#user-list .user .avatar a {
|
||||
display: block;
|
||||
}
|
||||
#user-list .user .details {
|
||||
float: left;
|
||||
@ -54,5 +56,5 @@
|
||||
#user-list .user h1 {
|
||||
margin-top: 0;
|
||||
font-weight: normal;
|
||||
font-size: 16pt;
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
data-version="dev"
|
||||
data-build-time=""
|
||||
data-max-post-size="10485760"
|
||||
data-tag-categories='["meta","character","artist","copyright"]'>
|
||||
data-tag-categories='[["meta","meta","#aaa"],["character","character","#0a0"],["artist","artist","#a00"],["copyright","copyright","#a0a"]]'>
|
||||
<!-- /build -->
|
||||
<!-- build:template
|
||||
<head
|
||||
@ -15,6 +15,7 @@
|
||||
data-tag-categories='<%= JSON.stringify(tagCategories).replace(/'/g, ''') %>'>
|
||||
/build -->
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
|
||||
<!-- build:remove -->
|
||||
<title>szurubooru</title>
|
||||
@ -23,11 +24,26 @@
|
||||
<title><%= serviceName %></title>
|
||||
/build -->
|
||||
|
||||
<!-- build:remove -->
|
||||
<style type="text/css">
|
||||
.tag-category-character { color: #0a0; }
|
||||
.tag-category-copyright { color: #a0a; }
|
||||
.tag-category-artist { color: #a00; }
|
||||
.tag-category-meta { color: #aaa; }
|
||||
</style>
|
||||
<!-- /build -->
|
||||
<!-- build:template
|
||||
<link rel="stylesheet" type="text/css" href="app.min.css?<%= timestamp %>"/>
|
||||
<style type="text/css">
|
||||
<% _.each(tagCategories, function(item) {
|
||||
var type = item[0];
|
||||
var color = item[2];
|
||||
%>.tag-category-<%= type %>{color:<%=color%>;}<%
|
||||
}); %>
|
||||
</style>
|
||||
/build -->
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/lib/font-awesome/css/font-awesome.min.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Droid+Sans:400,700"/>
|
||||
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Inconsolata">
|
||||
|
||||
@ -138,7 +154,7 @@
|
||||
<script type="text/javascript" src="/js/Presenters/PostPresenter.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/js/Presenters/GlobalCommentListPresenter.js"></script>
|
||||
<script type="text/javascript" src="/js/Presenters/PostCommentListPresenter.js"></script>
|
||||
<script type="text/javascript" src="/js/Presenters/CommentListPresenter.js"></script>
|
||||
|
||||
<script type="text/javascript" src="/js/Presenters/TagListPresenter.js"></script>
|
||||
<script type="text/javascript" src="/js/Presenters/TagPresenter.js"></script>
|
||||
|
@ -73,7 +73,7 @@ App.API = function(_, jQuery, promise, appState) {
|
||||
|
||||
var xhr = null;
|
||||
var apiPromise = promise.make(function(resolve, reject) {
|
||||
xhr = jQuery.ajax({
|
||||
var options = {
|
||||
headers: {
|
||||
'X-Authorization-Token': appState.get('loginToken') || '',
|
||||
},
|
||||
@ -92,7 +92,13 @@ App.API = function(_, jQuery, promise, appState) {
|
||||
type: method,
|
||||
url: fullUrl,
|
||||
data: data,
|
||||
});
|
||||
cache: false,
|
||||
};
|
||||
if (data instanceof FormData) {
|
||||
options.processData = false;
|
||||
options.contentType = false;
|
||||
}
|
||||
xhr = jQuery.ajax(options);
|
||||
});
|
||||
apiPromise.xhr = xhr;
|
||||
return apiPromise;
|
||||
|
@ -141,6 +141,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
|
||||
appState.set('loginToken', response.json.token && response.json.token.name);
|
||||
appState.set('loggedIn', response.json.user && !!response.json.user.id);
|
||||
appState.set('loggedInUser', response.json.user);
|
||||
appState.set('config', response.json.config);
|
||||
}
|
||||
|
||||
function isLoggedIn(userName) {
|
||||
|
@ -32,6 +32,9 @@ App.BrowsingSettings = function(
|
||||
sketchy: true,
|
||||
unsafe: true,
|
||||
},
|
||||
keyboardShortcuts: true,
|
||||
fitMode: 'fit-width',
|
||||
upscale: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -70,7 +73,7 @@ App.BrowsingSettings = function(
|
||||
var formData = {
|
||||
browsingSettings: JSON.stringify(settings),
|
||||
};
|
||||
return api.put('/users/' + user.name, formData);
|
||||
return api.post('/users/' + user.name, formData);
|
||||
}
|
||||
|
||||
function save() {
|
||||
@ -90,7 +93,6 @@ App.BrowsingSettings = function(
|
||||
getSettings: getSettings,
|
||||
setSettings: setSettings,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
App.DI.registerSingleton('browsingSettings', ['promise', 'auth', 'api'], App.BrowsingSettings);
|
||||
|
@ -6,7 +6,9 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
var jQuery = App.DI.get('jQuery');
|
||||
var tagList = App.DI.get('tagList');
|
||||
|
||||
var KEY_TAB = 9;
|
||||
var KEY_RETURN = 13;
|
||||
var KEY_DELETE = 46;
|
||||
var KEY_ESCAPE = 27;
|
||||
var KEY_UP = 38;
|
||||
var KEY_DOWN = 40;
|
||||
@ -17,12 +19,15 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
maxResults: 15,
|
||||
minLengthToArbitrarySearch: 3,
|
||||
onApply: null,
|
||||
onDelete: null,
|
||||
onRender: null,
|
||||
additionalFilter: null,
|
||||
};
|
||||
var showTimeout = null;
|
||||
var cachedSource = null;
|
||||
var results = [];
|
||||
var activeResult = -1;
|
||||
var monitorInputHidingInterval = null;
|
||||
|
||||
if ($input.length === 0) {
|
||||
throw new Error('Input element was not found');
|
||||
@ -61,27 +66,30 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
}
|
||||
|
||||
$input.bind('keydown', function(e) {
|
||||
var func = null;
|
||||
if (isShown() && e.which === KEY_ESCAPE) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
hide();
|
||||
func = hide;
|
||||
} else if (isShown() && e.which === KEY_TAB) {
|
||||
if (e.shiftKey) {
|
||||
func = selectPrevious;
|
||||
} else {
|
||||
func = selectNext;
|
||||
}
|
||||
} else if (isShown() && e.which === KEY_DOWN) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
selectNext();
|
||||
func = selectNext;
|
||||
} else if (isShown() && e.which === KEY_UP) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
selectPrevious();
|
||||
func = selectPrevious;
|
||||
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) {
|
||||
func = function() { applyAutocomplete(); hide(); };
|
||||
} else if (isShown() && e.which === KEY_DELETE && activeResult >= 0) {
|
||||
func = function() { applyDelete(); hide(); };
|
||||
}
|
||||
|
||||
if (func !== null) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
applyAutocomplete();
|
||||
hide();
|
||||
func();
|
||||
} else {
|
||||
window.clearTimeout(showTimeout);
|
||||
showTimeout = window.setTimeout(showOrHide, 250);
|
||||
@ -133,6 +141,7 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
|
||||
function hide() {
|
||||
$div.hide();
|
||||
window.clearInterval(monitorInputHidingInterval);
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
@ -179,6 +188,12 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
}
|
||||
}
|
||||
|
||||
function applyDelete() {
|
||||
if (options.onDelete) {
|
||||
options.onDelete(results[activeResult].tag);
|
||||
}
|
||||
}
|
||||
|
||||
function applyAutocomplete() {
|
||||
if (options.onApply) {
|
||||
options.onApply(results[activeResult].tag);
|
||||
@ -222,12 +237,22 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
});
|
||||
$list.append($listItem);
|
||||
});
|
||||
if (options.onRender) {
|
||||
options.onRender($list);
|
||||
}
|
||||
refreshActiveResult();
|
||||
|
||||
var x = $input.offset().left;
|
||||
var y = $input.offset().top + $input.outerHeight() - 2;
|
||||
if (y + $div.height() > window.innerHeight) {
|
||||
y = $input.offset().top - $div.height();
|
||||
}
|
||||
$div.css({
|
||||
left: ($input.offset().left) + 'px',
|
||||
top: ($input.offset().top + $input.outerHeight() - 2) + 'px',
|
||||
left: x + 'px',
|
||||
top: y + 'px',
|
||||
});
|
||||
$div.show();
|
||||
monitorInputHiding();
|
||||
}
|
||||
|
||||
function refreshActiveResult() {
|
||||
@ -237,5 +262,13 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
}
|
||||
}
|
||||
|
||||
function monitorInputHiding() {
|
||||
monitorInputHidingInterval = window.setInterval(function() {
|
||||
if (!$input.is(':visible')) {
|
||||
hide();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
@ -14,6 +14,14 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
var tagConfirmKeys = [KEY_RETURN, KEY_SPACE];
|
||||
var inputConfirmKeys = [KEY_RETURN];
|
||||
|
||||
var SOURCE_INITIAL_TEXT = 1;
|
||||
var SOURCE_AUTOCOMPLETION = 2;
|
||||
var SOURCE_PASTE = 3;
|
||||
var SOURCE_IMPLICATIONS = 4;
|
||||
var SOURCE_INPUT_BLUR = 5;
|
||||
var SOURCE_INPUT_ENTER = 6;
|
||||
var SOURCE_SUGGESTIONS = 7;
|
||||
|
||||
var tags = [];
|
||||
var options = {
|
||||
beforeTagAdded: null,
|
||||
@ -23,7 +31,9 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
|
||||
var $wrapper = jQuery('<div class="tag-input">');
|
||||
var $tagList = jQuery('<ul class="tags">');
|
||||
var $input = jQuery('<input class="tag-real-input" type="text"/>');
|
||||
var tagInputId = 'tags' + Math.random();
|
||||
var $label = jQuery('<label for="' + tagInputId + '" style="display: none">Tags:</label>');
|
||||
var $input = jQuery('<input class="tag-real-input" type="text" id="' + tagInputId + '"/>');
|
||||
var $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
|
||||
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
|
||||
init();
|
||||
@ -46,6 +56,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
function render() {
|
||||
$underlyingInput.hide();
|
||||
$wrapper.append($tagList);
|
||||
$wrapper.append($label);
|
||||
$wrapper.append($input);
|
||||
$wrapper.insertAfter($underlyingInput);
|
||||
$wrapper.click(function(e) {
|
||||
@ -56,18 +67,22 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
$input.focus();
|
||||
});
|
||||
$input.attr('placeholder', $underlyingInput.attr('placeholder'));
|
||||
$suggestions.insertAfter($wrapper);
|
||||
$siblings.insertAfter($wrapper);
|
||||
$suggestions.insertAfter($wrapper);
|
||||
|
||||
processText($underlyingInput.val(), addTagDirectly);
|
||||
processText($underlyingInput.val(), SOURCE_INITIAL_TEXT);
|
||||
|
||||
$underlyingInput.val('');
|
||||
}
|
||||
|
||||
function initAutoComplete() {
|
||||
var autoComplete = new App.Controls.AutoCompleteInput($input);
|
||||
autoComplete.onDelete = function(text) {
|
||||
removeTag(text);
|
||||
$input.val('');
|
||||
};
|
||||
autoComplete.onApply = function(text) {
|
||||
processText(text, addTag);
|
||||
processText(text, SOURCE_AUTOCOMPLETION);
|
||||
$input.val('');
|
||||
};
|
||||
autoComplete.additionalFilter = function(results) {
|
||||
@ -75,6 +90,14 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
return !_.contains(getTags(), resultItem[0]);
|
||||
});
|
||||
};
|
||||
autoComplete.onRender = function($list) {
|
||||
$list.find('li').each(function() {
|
||||
var $li = jQuery(this);
|
||||
if (isTaggedWith($li.attr('data-key'))) {
|
||||
$li.css('opacity', '0.5');
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
$input.bind('focus', function(e) {
|
||||
@ -83,7 +106,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
$input.bind('blur', function(e) {
|
||||
$wrapper.removeClass('focused');
|
||||
var tagName = $input.val();
|
||||
addTag(tagName);
|
||||
addTag(tagName, SOURCE_INPUT_BLUR);
|
||||
$input.val('');
|
||||
});
|
||||
|
||||
@ -96,12 +119,12 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
pastedText = (e.originalEvent || e).clipboardData.getData('text/plain');
|
||||
}
|
||||
|
||||
if (pastedText.length > 200) {
|
||||
if (pastedText.length > 2000) {
|
||||
window.alert('Pasted text is too long.');
|
||||
return;
|
||||
}
|
||||
|
||||
processTextWithoutLast(pastedText, addTag);
|
||||
processTextWithoutLast(pastedText, SOURCE_PASTE);
|
||||
});
|
||||
|
||||
$input.bind('keydown', function(e) {
|
||||
@ -114,7 +137,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
var tagName = $input.val();
|
||||
e.preventDefault();
|
||||
$input.val('');
|
||||
addTag(tagName);
|
||||
addTag(tagName, SOURCE_INPUT_ENTER);
|
||||
} else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) {
|
||||
e.preventDefault();
|
||||
removeLastTag();
|
||||
@ -127,19 +150,19 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
});
|
||||
}
|
||||
|
||||
function processText(text, callback) {
|
||||
function processText(text, source) {
|
||||
var tagNamesToAdd = explodeText(text);
|
||||
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
|
||||
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
|
||||
}
|
||||
|
||||
function processTextWithoutLast(text, callback) {
|
||||
function processTextWithoutLast(text, source) {
|
||||
var tagNamesToAdd = explodeText(text);
|
||||
var lastTagName = tagNamesToAdd.pop();
|
||||
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
|
||||
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
|
||||
$input.val(lastTagName);
|
||||
}
|
||||
|
||||
function addTag(tagName) {
|
||||
function addTag(tagName, source) {
|
||||
tagName = tagName.trim();
|
||||
if (tagName.length === 0) {
|
||||
return;
|
||||
@ -157,41 +180,55 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
if (isTaggedWith(tagName)) {
|
||||
flashTagRed(tagName);
|
||||
} else {
|
||||
beforeTagAdded(tagName);
|
||||
beforeTagAdded(tagName, source);
|
||||
|
||||
var exportedTag = getExportedTag(tagName);
|
||||
if (!exportedTag || !exportedTag.banned) {
|
||||
addTagDirectly(tagName);
|
||||
}
|
||||
|
||||
afterTagAdded(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
function addTagDirectly(tagName) {
|
||||
tags.push(tagName);
|
||||
var $elem = createListElement(tagName);
|
||||
$tagList.append($elem);
|
||||
}
|
||||
|
||||
function beforeTagAdded(tagName) {
|
||||
afterTagAdded(tagName, source);
|
||||
}
|
||||
}
|
||||
|
||||
function beforeTagRemoved(tagName) {
|
||||
if (typeof(options.beforeTagRemoved) === 'function') {
|
||||
options.beforeTagRemoved(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
function afterTagRemoved(tagName) {
|
||||
refreshShownSiblings();
|
||||
}
|
||||
|
||||
function beforeTagAdded(tagName, source) {
|
||||
if (typeof(options.beforeTagAdded) === 'function') {
|
||||
options.beforeTagAdded(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
function afterTagAdded(tagName) {
|
||||
function afterTagAdded(tagName, source) {
|
||||
if (source === SOURCE_IMPLICATIONS) {
|
||||
flashTagYellow(tagName);
|
||||
} else if (source !== SOURCE_INITIAL_TEXT) {
|
||||
var tag = getExportedTag(tagName);
|
||||
if (tag) {
|
||||
_.each(tag.implications, function(impliedTagName) {
|
||||
addTag(impliedTagName);
|
||||
flashTagYellow(impliedTagName);
|
||||
if (!isTaggedWith(impliedTagName)) {
|
||||
addTag(impliedTagName, SOURCE_IMPLICATIONS);
|
||||
}
|
||||
});
|
||||
showOrHideSuggestions(tag.suggestions);
|
||||
if (source !== SOURCE_IMPLICATIONS && source !== SOURCE_SUGGESTIONS) {
|
||||
showOrHideSuggestions(tagName);
|
||||
refreshShownSiblings();
|
||||
}
|
||||
} else {
|
||||
flashTagGreen(tagName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExportedTag(tagName) {
|
||||
return _.first(_.filter(
|
||||
@ -205,10 +242,9 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
var oldTagNames = getTags();
|
||||
var newTagNames = _.without(oldTagNames, tagName);
|
||||
if (newTagNames.length !== oldTagNames.length) {
|
||||
if (typeof(options.beforeTagRemoved) === 'function') {
|
||||
options.beforeTagRemoved(tagName);
|
||||
}
|
||||
beforeTagRemoved(tagName);
|
||||
setTags(newTagNames);
|
||||
afterTagRemoved(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,10 +295,11 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
$elem.attr('data-tag', tagName.toLowerCase());
|
||||
|
||||
var $tagLink = jQuery('<a class="tag">');
|
||||
$tagLink.text(tagName);
|
||||
$tagLink.text(tagName + ' ' /* for easy copying */);
|
||||
$tagLink.click(function(e) {
|
||||
e.preventDefault();
|
||||
showOrHideTagSiblings(tagName);
|
||||
showOrHideSiblings(tagName);
|
||||
showOrHideSuggestions(tagName);
|
||||
});
|
||||
$elem.append($tagLink);
|
||||
|
||||
@ -276,19 +313,13 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
return $elem;
|
||||
}
|
||||
|
||||
function showOrHideSuggestions(suggestedTagNames) {
|
||||
if (_.size(suggestedTagNames) === 0) {
|
||||
return;
|
||||
function showOrHideSuggestions(tagName) {
|
||||
var tag = getExportedTag(tagName);
|
||||
var suggestions = tag ? tag.suggestions : [];
|
||||
updateSuggestions($suggestions, suggestions);
|
||||
}
|
||||
|
||||
var suggestions = filterSuggestions(suggestedTagNames);
|
||||
if (suggestions.length > 0) {
|
||||
attachTagsToSuggestionList($suggestions.find('ul'), suggestions);
|
||||
$suggestions.slideDown('fast');
|
||||
}
|
||||
}
|
||||
|
||||
function showOrHideTagSiblings(tagName) {
|
||||
function showOrHideSiblings(tagName) {
|
||||
if ($siblings.data('lastTag') === tagName && $siblings.is(':visible')) {
|
||||
$siblings.slideUp('fast');
|
||||
$siblings.data('lastTag', null);
|
||||
@ -298,22 +329,23 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
|
||||
$siblings.slideUp('fast', resolve);
|
||||
})).then(function(siblings) {
|
||||
siblings = _.pluck(siblings, 'name');
|
||||
$siblings.data('lastTag', tagName);
|
||||
|
||||
if (!_.size(siblings)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var suggestions = filterSuggestions(_.pluck(siblings, 'name'));
|
||||
if (suggestions.length > 0) {
|
||||
attachTagsToSuggestionList($siblings.find('ul'), suggestions);
|
||||
$siblings.slideDown('fast');
|
||||
}
|
||||
$siblings.data('siblings', siblings);
|
||||
updateSuggestions($siblings, siblings);
|
||||
}).fail(function() {
|
||||
});
|
||||
}
|
||||
|
||||
function refreshShownSiblings() {
|
||||
updateSuggestions($siblings, $siblings.data('siblings'));
|
||||
}
|
||||
|
||||
function updateSuggestions($target, suggestedTagNames) {
|
||||
function filterSuggestions(sourceTagNames) {
|
||||
if (!sourceTagNames) {
|
||||
return [];
|
||||
}
|
||||
var tagNames = _.filter(sourceTagNames.slice(), function(tagName) {
|
||||
return !isTaggedWith(tagName);
|
||||
});
|
||||
@ -329,7 +361,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
$a.text(tagName);
|
||||
$a.click(function(e) {
|
||||
e.preventDefault();
|
||||
addTag(tagName);
|
||||
addTag(tagName, SOURCE_SUGGESTIONS);
|
||||
$li.fadeOut('fast', function() {
|
||||
$li.remove();
|
||||
if ($list.children().length === 0) {
|
||||
@ -342,11 +374,20 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
});
|
||||
}
|
||||
|
||||
var suggestions = filterSuggestions(suggestedTagNames);
|
||||
if (suggestions.length > 0) {
|
||||
attachTagsToSuggestionList($target.find('ul'), suggestions);
|
||||
$target.slideDown('fast');
|
||||
} else {
|
||||
$target.slideUp('fast');
|
||||
}
|
||||
}
|
||||
|
||||
function getSiblings(tagName) {
|
||||
return promise.make(function(resolve, reject) {
|
||||
promise.wait(api.get('/tags/' + tagName + '/siblings'))
|
||||
.then(function(response) {
|
||||
resolve(response.json.data);
|
||||
resolve(response.json.tags);
|
||||
}).fail(function() {
|
||||
reject();
|
||||
});
|
||||
@ -364,6 +405,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
function hideSuggestions() {
|
||||
$siblings.hide();
|
||||
$suggestions.hide();
|
||||
$siblings.data('siblings', []);
|
||||
}
|
||||
|
||||
_.extend(options, {
|
||||
|
@ -1,7 +1,8 @@
|
||||
var App = App || {};
|
||||
|
||||
App.Keyboard = function(jQuery, mousetrap) {
|
||||
App.Keyboard = function(jQuery, mousetrap, browsingSettings) {
|
||||
|
||||
var enabled = browsingSettings.getSettings().keyboardShortcuts;
|
||||
var oldStopCallback = mousetrap.stopCallback;
|
||||
mousetrap.stopCallback = function(e, element, combo, sequence) {
|
||||
if (combo.indexOf('ctrl') === -1 && e.ctrlKey) {
|
||||
@ -14,21 +15,31 @@ App.Keyboard = function(jQuery, mousetrap) {
|
||||
return false;
|
||||
}
|
||||
var $focused = jQuery(':focus').eq(0);
|
||||
if ($focused.length && $focused.prop('tagName').match(/embed|object/i)) {
|
||||
if ($focused.length) {
|
||||
if ($focused.prop('tagName').match(/embed|object/i)) {
|
||||
return true;
|
||||
}
|
||||
if ($focused.prop('tagName').toLowerCase() === 'input' &&
|
||||
$focused.attr('type').match(/checkbox|radio/i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return oldStopCallback.apply(mousetrap, arguments);
|
||||
};
|
||||
|
||||
function keyup(key, callback) {
|
||||
unbind(key);
|
||||
if (enabled) {
|
||||
mousetrap.bind(key, callback, 'keyup');
|
||||
}
|
||||
}
|
||||
|
||||
function keydown(key, callback) {
|
||||
unbind(key);
|
||||
if (enabled) {
|
||||
mousetrap.bind(key, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
mousetrap.reset();
|
||||
@ -47,4 +58,4 @@ App.Keyboard = function(jQuery, mousetrap) {
|
||||
};
|
||||
};
|
||||
|
||||
App.DI.register('keyboard', ['jQuery', 'mousetrap'], App.Keyboard);
|
||||
App.DI.register('keyboard', ['jQuery', 'mousetrap', 'browsingSettings'], App.Keyboard);
|
||||
|
@ -71,10 +71,7 @@ App.Pager = function(
|
||||
var totalRecords = response.json.totalRecords;
|
||||
totalPages = Math.ceil(totalRecords / pageSize);
|
||||
|
||||
resolve({
|
||||
entities: response.json.data,
|
||||
totalRecords: totalRecords,
|
||||
totalPages: totalPages});
|
||||
resolve(response);
|
||||
|
||||
}).fail(function(response) {
|
||||
reject(response);
|
||||
|
232
public_html/js/Presenters/CommentListPresenter.js
Normal file
232
public_html/js/Presenters/CommentListPresenter.js
Normal file
@ -0,0 +1,232 @@
|
||||
var App = App || {};
|
||||
App.Presenters = App.Presenters || {};
|
||||
|
||||
App.Presenters.CommentListPresenter = function(
|
||||
_,
|
||||
jQuery,
|
||||
util,
|
||||
promise,
|
||||
api,
|
||||
auth,
|
||||
topNavigationPresenter,
|
||||
messagePresenter) {
|
||||
|
||||
var $el;
|
||||
var privileges;
|
||||
var templates = {};
|
||||
|
||||
var post;
|
||||
var comments = [];
|
||||
|
||||
function init(params, loaded) {
|
||||
$el = params.$target;
|
||||
post = params.post;
|
||||
comments = params.comments || [];
|
||||
|
||||
privileges = {
|
||||
canListComments: auth.hasPrivilege(auth.privileges.listComments),
|
||||
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
|
||||
canEditOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
|
||||
canEditAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
|
||||
canDeleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
|
||||
canDeleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
|
||||
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
|
||||
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
|
||||
};
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('comment-list'),
|
||||
util.promiseTemplate('comment-list-item'),
|
||||
util.promiseTemplate('comment-form'))
|
||||
.then(function(
|
||||
commentListTemplate,
|
||||
commentListItemTemplate,
|
||||
commentFormTemplate)
|
||||
{
|
||||
templates.commentList = commentListTemplate;
|
||||
templates.commentListItem = commentListItemTemplate;
|
||||
templates.commentForm = commentFormTemplate;
|
||||
|
||||
render();
|
||||
loaded();
|
||||
|
||||
if (comments.length === 0) {
|
||||
promise.wait(api.get('/comments/' + params.post.id))
|
||||
.then(function(response) {
|
||||
comments = response.json.comments;
|
||||
render();
|
||||
}).fail(function() {
|
||||
console.log(arguments);
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
console.log(arguments);
|
||||
loaded();
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
$el.html(templates.commentList(
|
||||
_.extend(
|
||||
{
|
||||
commentListItemTemplate: templates.commentListItem,
|
||||
commentFormTemplate: templates.commentForm,
|
||||
util: util,
|
||||
comments: comments,
|
||||
post: post,
|
||||
},
|
||||
privileges)));
|
||||
|
||||
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
|
||||
renderComments(comments);
|
||||
}
|
||||
|
||||
function renderComments(comments) {
|
||||
var $target = $el.find('.comments');
|
||||
var $targetList = $el.find('ul');
|
||||
|
||||
if (comments.length > 0) {
|
||||
$target.show();
|
||||
} else {
|
||||
$target.hide();
|
||||
}
|
||||
|
||||
$targetList.empty();
|
||||
_.each(comments, function(comment) {
|
||||
renderComment($targetList, comment);
|
||||
});
|
||||
}
|
||||
|
||||
function renderComment($targetList, comment) {
|
||||
var $item = jQuery('<li>' + templates.commentListItem({
|
||||
comment: comment,
|
||||
util: util,
|
||||
canVote: auth.isLoggedIn(),
|
||||
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.canEditOwnComments : privileges.canEditAllComments,
|
||||
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.canDeleteOwnComments : privileges.canDeleteAllComments,
|
||||
canViewUsers: privileges.canViewUsers,
|
||||
canViewPosts: privileges.canViewPosts,
|
||||
}) + '</li>');
|
||||
util.loadImagesNicely($item.find('img'));
|
||||
$targetList.append($item);
|
||||
|
||||
$item.find('a.edit').click(function(e) {
|
||||
e.preventDefault();
|
||||
editCommentStart($item, comment);
|
||||
});
|
||||
|
||||
$item.find('a.delete').click(function(e) {
|
||||
e.preventDefault();
|
||||
deleteComment(comment);
|
||||
});
|
||||
|
||||
$item.find('a.score-up').click(function(e) {
|
||||
e.preventDefault();
|
||||
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
|
||||
});
|
||||
|
||||
$item.find('a.score-down').click(function(e) {
|
||||
e.preventDefault();
|
||||
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
function commentFormSubmitted(e, comment) {
|
||||
e.preventDefault();
|
||||
var $button = jQuery(e.target);
|
||||
var $form = $button.parents('form');
|
||||
var sender = $button.val();
|
||||
if (sender === 'preview') {
|
||||
previewComment($form);
|
||||
} else {
|
||||
submitComment($form, comment);
|
||||
}
|
||||
}
|
||||
|
||||
function previewComment($form) {
|
||||
var $preview = $form.find('.preview');
|
||||
$preview.slideUp('fast', function() {
|
||||
$preview.html(util.formatMarkdown($form.find('textarea').val()));
|
||||
$preview.slideDown('fast');
|
||||
});
|
||||
}
|
||||
|
||||
function updateComment(comment) {
|
||||
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
|
||||
render();
|
||||
}
|
||||
|
||||
function addComment(comment) {
|
||||
comments.push(comment);
|
||||
render();
|
||||
}
|
||||
|
||||
function submitComment($form, commentToEdit) {
|
||||
$form.find('.preview').slideUp();
|
||||
var $textarea = $form.find('textarea');
|
||||
|
||||
var data = {text: $textarea.val()};
|
||||
var p;
|
||||
if (commentToEdit) {
|
||||
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
|
||||
} else {
|
||||
p = promise.wait(api.post('/comments/' + post.id, data));
|
||||
}
|
||||
|
||||
p.then(function(response) {
|
||||
$textarea.val('');
|
||||
var comment = response.json.comment;
|
||||
|
||||
if (commentToEdit) {
|
||||
$form.slideUp(function() {
|
||||
$form.remove();
|
||||
});
|
||||
updateComment(comment);
|
||||
} else {
|
||||
addComment(comment);
|
||||
}
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function editCommentStart($item, comment) {
|
||||
if ($item.find('.comment-form').length > 0) {
|
||||
return;
|
||||
}
|
||||
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
|
||||
$item.find('.body').append($form);
|
||||
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
|
||||
}
|
||||
|
||||
function deleteComment(comment) {
|
||||
if (!window.confirm('Are you sure you want to delete this comment?')) {
|
||||
return;
|
||||
}
|
||||
promise.wait(api.delete('/comments/' + comment.id))
|
||||
.then(function(response) {
|
||||
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
|
||||
renderComments(comments);
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function score(comment, scoreValue) {
|
||||
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
|
||||
.then(function(response) {
|
||||
comment.score = parseInt(response.json.score);
|
||||
comment.ownScore = parseInt(response.json.ownScore);
|
||||
updateComment(comment);
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function showGenericError(response) {
|
||||
window.alert(response.json && response.json.error || response);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
render: render,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('commentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.CommentListPresenter);
|
@ -5,17 +5,23 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
_,
|
||||
jQuery,
|
||||
util,
|
||||
auth,
|
||||
promise,
|
||||
pagerPresenter,
|
||||
topNavigationPresenter) {
|
||||
|
||||
var $el;
|
||||
var privileges;
|
||||
var templates = {};
|
||||
|
||||
function init(params, loaded) {
|
||||
$el = jQuery('#content');
|
||||
topNavigationPresenter.select('comments');
|
||||
|
||||
privileges = {
|
||||
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
|
||||
};
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('global-comment-list'),
|
||||
util.promiseTemplate('global-comment-list-item'),
|
||||
@ -32,8 +38,8 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
baseUri: '#/comments',
|
||||
backendUri: '/comments',
|
||||
$target: $el.find('.pagination-target'),
|
||||
updateCallback: function($page, data) {
|
||||
renderComments($page, data.entities);
|
||||
updateCallback: function($page, response) {
|
||||
renderComments($page, response.json.comments);
|
||||
},
|
||||
},
|
||||
function() {
|
||||
@ -47,7 +53,7 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
|
||||
|
||||
function reinit(params, loaded) {
|
||||
pagerPresenter.reinit({query: params.query});
|
||||
pagerPresenter.reinit({query: params.query || {}});
|
||||
loaded();
|
||||
}
|
||||
|
||||
@ -59,20 +65,20 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
$el.html(templates.list());
|
||||
}
|
||||
|
||||
function renderComments($page, data) {
|
||||
function renderComments($page, postComments) {
|
||||
var $target = $page.find('.posts');
|
||||
_.each(data, function(data) {
|
||||
var post = data.post;
|
||||
var comments = data.comments;
|
||||
|
||||
_.each(postComments, function(postComments) {
|
||||
var post = postComments.post;
|
||||
var comments = postComments.comments;
|
||||
var $post = jQuery('<li>' + templates.listItem({
|
||||
util: util,
|
||||
post: post,
|
||||
postTemplate: templates.post,
|
||||
canViewPosts: privileges.canViewPosts,
|
||||
}) + '</li>');
|
||||
|
||||
util.loadImagesNicely($post.find('img'));
|
||||
var presenter = App.DI.get('postCommentListPresenter');
|
||||
var presenter = App.DI.get('commentListPresenter');
|
||||
|
||||
presenter.init({
|
||||
post: post,
|
||||
@ -95,4 +101,4 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);
|
||||
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'auth', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);
|
||||
|
@ -31,8 +31,8 @@ App.Presenters.HistoryPresenter = function(
|
||||
baseUri: '#/history',
|
||||
backendUri: '/history',
|
||||
$target: $el.find('.pagination-target'),
|
||||
updateCallback: function($page, data) {
|
||||
renderHistory($page, data.entities);
|
||||
updateCallback: function($page, response) {
|
||||
renderHistory($page, response.json.history);
|
||||
},
|
||||
},
|
||||
function() {
|
||||
@ -62,7 +62,7 @@ App.Presenters.HistoryPresenter = function(
|
||||
|
||||
function renderHistory($page, historyItems) {
|
||||
$page.append(templates.history({
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
util: util,
|
||||
history: historyItems}));
|
||||
}
|
||||
|
||||
@ -72,7 +72,6 @@ App.Presenters.HistoryPresenter = function(
|
||||
deinit: deinit,
|
||||
render: render,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('historyPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.HistoryPresenter);
|
||||
|
@ -41,7 +41,14 @@ App.Presenters.HomePresenter = function(
|
||||
if ($el.find('#post-content-target').length > 0) {
|
||||
presenterManager.initPresenters([
|
||||
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}]],
|
||||
function() {});
|
||||
function() {
|
||||
var $wrapper = $el.find('.object-wrapper');
|
||||
$wrapper.css({
|
||||
maxWidth: $wrapper.attr('data-width') + 'px',
|
||||
width: 'auto',
|
||||
margin: '0 auto'});
|
||||
postContentPresenter.updatePostNotesSize();
|
||||
});
|
||||
}
|
||||
|
||||
}).fail(function(response) {
|
||||
@ -58,8 +65,7 @@ App.Presenters.HomePresenter = function(
|
||||
title: topNavigationPresenter.getBaseTitle(),
|
||||
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
|
||||
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatFileSize: util.formatFileSize,
|
||||
util: util,
|
||||
version: jQuery('head').attr('data-version'),
|
||||
buildTime: jQuery('head').attr('data-build-time'),
|
||||
}));
|
||||
|
@ -62,16 +62,8 @@ App.Presenters.PagerPresenter = function(
|
||||
.fail(loaded);
|
||||
|
||||
if (!endlessScroll) {
|
||||
keyboard.keydown('a', function() {
|
||||
if (pager.prevPage()) {
|
||||
syncUrl({page: pager.getPage()});
|
||||
}
|
||||
});
|
||||
keyboard.keydown('d', function() {
|
||||
if (pager.nextPage()) {
|
||||
syncUrl({page: pager.getPage()});
|
||||
}
|
||||
});
|
||||
keyboard.keydown(['a', 'left'], navigateToPrevPage);
|
||||
keyboard.keydown(['d', 'right'], navigateToNextPage);
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,11 +74,12 @@ App.Presenters.PagerPresenter = function(
|
||||
function getUrl(options) {
|
||||
return util.appendComplexRouteParam(
|
||||
baseUri,
|
||||
util.simplifySearchQuery(
|
||||
_.extend(
|
||||
{},
|
||||
pager.getSearchParams(),
|
||||
{page: pager.getPage()},
|
||||
options));
|
||||
options)));
|
||||
}
|
||||
|
||||
function syncUrl(options) {
|
||||
@ -121,7 +114,15 @@ App.Presenters.PagerPresenter = function(
|
||||
updateCallback($page, response);
|
||||
|
||||
refreshPageList();
|
||||
if (!response.entities.length) {
|
||||
|
||||
var entities =
|
||||
response.json.posts ||
|
||||
response.json.users ||
|
||||
response.json.comments ||
|
||||
response.json.tags ||
|
||||
response.json.history;
|
||||
|
||||
if (!entities.length) {
|
||||
messagePresenter.showInfo($messages, 'No data to show');
|
||||
if (pager.getVisiblePages().length === 1) {
|
||||
hidePageList();
|
||||
@ -132,7 +133,7 @@ App.Presenters.PagerPresenter = function(
|
||||
showPageList();
|
||||
}
|
||||
|
||||
if (pager.getPage() < response.totalPages) {
|
||||
if (pager.getPage() < pager.getTotalPages()) {
|
||||
attachNextPageLoader();
|
||||
}
|
||||
|
||||
@ -182,13 +183,28 @@ App.Presenters.PagerPresenter = function(
|
||||
$pageList.hide();
|
||||
}
|
||||
|
||||
function navigateToPrevPage() {
|
||||
console.log('!');
|
||||
if (pager.prevPage()) {
|
||||
syncUrl({page: pager.getPage()});
|
||||
}
|
||||
}
|
||||
|
||||
function navigateToNextPage() {
|
||||
if (pager.nextPage()) {
|
||||
syncUrl({page: pager.getPage()});
|
||||
}
|
||||
}
|
||||
|
||||
function refreshPageList() {
|
||||
var $lastItem = $pageList.find('li:last-child');
|
||||
var currentPage = pager.getPage();
|
||||
var pages = pager.getVisiblePages();
|
||||
$pageList.empty();
|
||||
$pageList.find('li.page').remove();
|
||||
var lastPage = 0;
|
||||
_.each(pages, function(page) {
|
||||
if (page - lastPage > 1) {
|
||||
$pageList.append(jQuery('<li><a>…</a></li>'));
|
||||
jQuery('<li class="page ellipsis"><a>…</a></li>').insertBefore($lastItem);
|
||||
}
|
||||
lastPage = page;
|
||||
|
||||
@ -199,12 +215,19 @@ App.Presenters.PagerPresenter = function(
|
||||
});
|
||||
$a.addClass('big-button');
|
||||
$a.text(page);
|
||||
if (page === pager.getPage()) {
|
||||
if (page === currentPage) {
|
||||
$a.addClass('active');
|
||||
}
|
||||
var $li = jQuery('<li/>');
|
||||
$li.append($a);
|
||||
$pageList.append($li);
|
||||
jQuery('<li class="page"/>').append($a).insertBefore($lastItem);
|
||||
});
|
||||
|
||||
$pageList.find('li.next a').unbind('click').bind('click', function(e) {
|
||||
e.preventDefault();
|
||||
navigateToNextPage();
|
||||
});
|
||||
$pageList.find('li.prev a').unbind('click').bind('click', function(e) {
|
||||
e.preventDefault();
|
||||
navigateToPrevPage();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,230 +0,0 @@
|
||||
var App = App || {};
|
||||
App.Presenters = App.Presenters || {};
|
||||
|
||||
App.Presenters.PostCommentListPresenter = function(
|
||||
_,
|
||||
jQuery,
|
||||
util,
|
||||
promise,
|
||||
api,
|
||||
auth,
|
||||
topNavigationPresenter,
|
||||
messagePresenter) {
|
||||
|
||||
var $el;
|
||||
var privileges;
|
||||
var templates = {};
|
||||
|
||||
var post;
|
||||
var comments = [];
|
||||
|
||||
function init(params, loaded) {
|
||||
$el = params.$target;
|
||||
post = params.post;
|
||||
comments = params.comments || [];
|
||||
|
||||
privileges = {
|
||||
canListComments: auth.hasPrivilege(auth.privileges.listComments),
|
||||
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
|
||||
editOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
|
||||
editAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
|
||||
deleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
|
||||
deleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
|
||||
};
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('post-comment-list'),
|
||||
util.promiseTemplate('comment-list-item'),
|
||||
util.promiseTemplate('comment-form'))
|
||||
.then(function(
|
||||
commentListTemplate,
|
||||
commentListItemTemplate,
|
||||
commentFormTemplate)
|
||||
{
|
||||
templates.commentList = commentListTemplate;
|
||||
templates.commentListItem = commentListItemTemplate;
|
||||
templates.commentForm = commentFormTemplate;
|
||||
|
||||
render();
|
||||
loaded();
|
||||
|
||||
if (comments.length === 0) {
|
||||
promise.wait(api.get('/comments/' + params.post.id))
|
||||
.then(function(response) {
|
||||
comments = response.json.data;
|
||||
render();
|
||||
}).fail(function() {
|
||||
console.log(arguments);
|
||||
});
|
||||
}
|
||||
})
|
||||
.fail(function() {
|
||||
console.log(arguments);
|
||||
loaded();
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
$el.html(templates.commentList(
|
||||
_.extend(
|
||||
{
|
||||
commentListItemTemplate: templates.commentListItem,
|
||||
commentFormTemplate: templates.commentForm,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatMarkdown: util.formatMarkdown,
|
||||
comments: comments,
|
||||
post: post,
|
||||
},
|
||||
privileges)));
|
||||
|
||||
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
|
||||
renderComments(comments);
|
||||
}
|
||||
|
||||
function renderComments(comments) {
|
||||
var $target = $el.find('.comments');
|
||||
var $targetList = $el.find('ul');
|
||||
|
||||
if (comments.length > 0) {
|
||||
$target.show();
|
||||
} else {
|
||||
$target.hide();
|
||||
}
|
||||
|
||||
$targetList.empty();
|
||||
_.each(comments, function(comment) {
|
||||
renderComment($targetList, comment);
|
||||
});
|
||||
}
|
||||
|
||||
function renderComment($targetList, comment) {
|
||||
var $item = jQuery('<li>' + templates.commentListItem({
|
||||
comment: comment,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatMarkdown: util.formatMarkdown,
|
||||
canVote: auth.isLoggedIn(),
|
||||
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
|
||||
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
|
||||
}) + '</li>');
|
||||
util.loadImagesNicely($item.find('img'));
|
||||
$targetList.append($item);
|
||||
|
||||
$item.find('a.edit').click(function(e) {
|
||||
e.preventDefault();
|
||||
editCommentStart($item, comment);
|
||||
});
|
||||
|
||||
$item.find('a.delete').click(function(e) {
|
||||
e.preventDefault();
|
||||
deleteComment(comment);
|
||||
});
|
||||
|
||||
$item.find('a.score-up').click(function(e) {
|
||||
e.preventDefault();
|
||||
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
|
||||
});
|
||||
|
||||
$item.find('a.score-down').click(function(e) {
|
||||
e.preventDefault();
|
||||
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
function commentFormSubmitted(e, comment) {
|
||||
e.preventDefault();
|
||||
var $button = jQuery(e.target);
|
||||
var $form = $button.parents('form');
|
||||
var sender = $button.val();
|
||||
if (sender === 'preview') {
|
||||
previewComment($form);
|
||||
} else {
|
||||
submitComment($form, comment);
|
||||
}
|
||||
}
|
||||
|
||||
function previewComment($form) {
|
||||
var $preview = $form.find('.preview');
|
||||
$preview.slideUp('fast', function() {
|
||||
$preview.html(util.formatMarkdown($form.find('textarea').val()));
|
||||
$preview.slideDown('fast');
|
||||
});
|
||||
}
|
||||
|
||||
function updateComment(comment) {
|
||||
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
|
||||
render();
|
||||
}
|
||||
|
||||
function addComment(comment) {
|
||||
comments.push(comment);
|
||||
render();
|
||||
}
|
||||
|
||||
function submitComment($form, commentToEdit) {
|
||||
$form.find('.preview').slideUp();
|
||||
var $textarea = $form.find('textarea');
|
||||
|
||||
var data = {text: $textarea.val()};
|
||||
var p;
|
||||
if (commentToEdit) {
|
||||
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
|
||||
} else {
|
||||
p = promise.wait(api.post('/comments/' + post.id, data));
|
||||
}
|
||||
|
||||
p.then(function(response) {
|
||||
$textarea.val('');
|
||||
var comment = response.json;
|
||||
|
||||
if (commentToEdit) {
|
||||
$form.slideUp(function() {
|
||||
$form.remove();
|
||||
});
|
||||
updateComment(comment);
|
||||
} else {
|
||||
addComment(comment);
|
||||
}
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function editCommentStart($item, comment) {
|
||||
if ($item.find('.comment-form').length > 0) {
|
||||
return;
|
||||
}
|
||||
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
|
||||
$item.find('.body').append($form);
|
||||
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
|
||||
}
|
||||
|
||||
function deleteComment(comment) {
|
||||
if (!window.confirm('Are you sure you want to delete this comment?')) {
|
||||
return;
|
||||
}
|
||||
promise.wait(api.delete('/comments/' + comment.id))
|
||||
.then(function(response) {
|
||||
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
|
||||
renderComments(comments);
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function score(comment, scoreValue) {
|
||||
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
|
||||
.then(function(response) {
|
||||
comment.score = parseInt(response.json.score);
|
||||
comment.ownScore = parseInt(response.json.ownScore);
|
||||
updateComment(comment);
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function showGenericError(response) {
|
||||
window.alert(response.json && response.json.error || response);
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
render: render,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('postCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostCommentListPresenter);
|
@ -5,12 +5,15 @@ App.Presenters.PostContentPresenter = function(
|
||||
jQuery,
|
||||
util,
|
||||
promise,
|
||||
keyboard,
|
||||
presenterManager,
|
||||
postNotesPresenter) {
|
||||
postNotesPresenter,
|
||||
browsingSettings) {
|
||||
|
||||
var post;
|
||||
var templates = {};
|
||||
var $target;
|
||||
var $wrapper;
|
||||
|
||||
function init(params, loaded) {
|
||||
$target = params.$target;
|
||||
@ -27,14 +30,95 @@ App.Presenters.PostContentPresenter = function(
|
||||
});
|
||||
}
|
||||
|
||||
function getFitters() {
|
||||
var originalWidth = $wrapper.attr('data-width');
|
||||
var originalHeight = $wrapper.attr('data-height');
|
||||
var ratio = originalWidth / originalHeight;
|
||||
var containerHeight = jQuery(window).height() - $wrapper.offset().top - 10;
|
||||
var containerWidth = $wrapper.parent().outerWidth() - 10;
|
||||
|
||||
return {
|
||||
'fit-both': function(allowUpscale) {
|
||||
var width = containerWidth;
|
||||
var height = containerWidth / ratio;
|
||||
if (height > containerHeight) {
|
||||
width = containerHeight * ratio;
|
||||
height = containerHeight;
|
||||
}
|
||||
if (!allowUpscale) {
|
||||
if (width > originalWidth) {
|
||||
width = originalWidth;
|
||||
height = originalWidth / ratio;
|
||||
}
|
||||
if (height > originalHeight) {
|
||||
width = originalHeight * ratio;
|
||||
height = originalHeight;
|
||||
}
|
||||
}
|
||||
$wrapper.css({maxWidth: width + 'px'});
|
||||
},
|
||||
'fit-height': function(allowUpscale) {
|
||||
var width = containerHeight * ratio;
|
||||
if (width > originalWidth && !allowUpscale) {
|
||||
width = originalWidth;
|
||||
}
|
||||
$wrapper.css({maxWidth: width + 'px'});
|
||||
},
|
||||
'fit-width': function(allowUpscale) {
|
||||
if (allowUpscale) {
|
||||
$wrapper.css({maxWidth: containerWidth + 'px'});
|
||||
} else {
|
||||
$wrapper.css({maxWidth: originalWidth + 'px'});
|
||||
}
|
||||
},
|
||||
'original': function(allowUpscale) {
|
||||
$wrapper.css({
|
||||
minWidth: originalWidth + 'px',
|
||||
width: originalWidth + 'px'});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFitMode() {
|
||||
return $wrapper.data('fit-mode');
|
||||
}
|
||||
|
||||
function changeFitMode(fitMode) {
|
||||
$wrapper.data('fit-mode', fitMode);
|
||||
$wrapper.css({
|
||||
width: '', height: '',
|
||||
minWidth: '', minHeight: '',
|
||||
maxWidth: '', maxHeight: '',
|
||||
});
|
||||
getFitters()[fitMode.style](fitMode.upscale);
|
||||
updatePostNotesSize();
|
||||
}
|
||||
|
||||
function cycleFitMode() {
|
||||
var oldMode = getFitMode();
|
||||
var fitterNames = Object.keys(getFitters());
|
||||
var newMode = {
|
||||
style: fitterNames[(fitterNames.indexOf(oldMode.style) + 1) % fitterNames.length],
|
||||
upscale: oldMode.upscale,
|
||||
};
|
||||
changeFitMode(newMode);
|
||||
}
|
||||
|
||||
function render() {
|
||||
$target.html(templates.postContent({post: post}));
|
||||
$wrapper = $target.find('.object-wrapper');
|
||||
|
||||
if (post.contentType === 'image') {
|
||||
if (post.contentType === 'image' || post.contentType === 'animation') {
|
||||
loadPostNotes();
|
||||
updatePostNotesSize();
|
||||
}
|
||||
|
||||
changeFitMode({
|
||||
style: browsingSettings.getSettings().fitMode,
|
||||
upscale: browsingSettings.getSettings().upscale,
|
||||
});
|
||||
keyboard.keyup('f', cycleFitMode);
|
||||
|
||||
jQuery(window).resize(updatePostNotesSize);
|
||||
}
|
||||
|
||||
@ -45,8 +129,14 @@ App.Presenters.PostContentPresenter = function(
|
||||
}
|
||||
|
||||
function updatePostNotesSize() {
|
||||
$target.find('.post-notes-target').width($target.find('.image-wrapper').outerWidth());
|
||||
$target.find('.post-notes-target').height($target.find('.image-wrapper').outerHeight());
|
||||
var $postNotes = $target.find('.post-notes-target');
|
||||
var $wrapper = $target.find('.object-wrapper');
|
||||
$postNotes.css({
|
||||
width: $wrapper.outerWidth() + 'px',
|
||||
height: $wrapper.outerHeight() + 'px',
|
||||
left: ($wrapper.offset().left - $wrapper.parent().offset().left) + 'px',
|
||||
top: ($wrapper.offset().top - $wrapper.parent().offset().top) + 'px',
|
||||
});
|
||||
}
|
||||
|
||||
function addNewPostNote() {
|
||||
@ -57,14 +147,19 @@ App.Presenters.PostContentPresenter = function(
|
||||
init: init,
|
||||
render: render,
|
||||
addNewPostNote: addNewPostNote,
|
||||
updatePostNotesSize: updatePostNotesSize,
|
||||
getFitMode: getFitMode,
|
||||
changeFitMode: changeFitMode,
|
||||
cycleFitMode: cycleFitMode,
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('postContentPresenter', [
|
||||
'jQuery',
|
||||
'util',
|
||||
'promise',
|
||||
'keyboard',
|
||||
'presenterManager',
|
||||
'postNotesPresenter'],
|
||||
'postNotesPresenter',
|
||||
'browsingSettings'],
|
||||
App.Presenters.PostContentPresenter);
|
||||
|
@ -2,6 +2,7 @@ var App = App || {};
|
||||
App.Presenters = App.Presenters || {};
|
||||
|
||||
App.Presenters.PostEditPresenter = function(
|
||||
jQuery,
|
||||
util,
|
||||
promise,
|
||||
api,
|
||||
@ -46,7 +47,20 @@ App.Presenters.PostEditPresenter = function(
|
||||
}
|
||||
|
||||
function render() {
|
||||
$target.html(templates.postEdit({post: post, privileges: privileges}));
|
||||
var $template = jQuery(templates.postEdit({post: post, privileges: privileges}));
|
||||
|
||||
var $advanced = $template.find('.advanced');
|
||||
var $advancedTrigger = $template.find('.advanced-trigger');
|
||||
$advanced.hide();
|
||||
if (!$advanced.length) {
|
||||
$advancedTrigger.hide();
|
||||
} else {
|
||||
$advancedTrigger.find('a').click(function(e) {
|
||||
advancedTriggerClicked(e, $advanced, $advancedTrigger);
|
||||
});
|
||||
}
|
||||
|
||||
$target.html($template);
|
||||
|
||||
postContentFileDropper = new App.Controls.FileDropper($target.find('form [name=content]'));
|
||||
postContentFileDropper.onChange = postContentChanged;
|
||||
@ -63,6 +77,12 @@ App.Presenters.PostEditPresenter = function(
|
||||
$target.find('form').submit(editFormSubmitted);
|
||||
}
|
||||
|
||||
function advancedTriggerClicked(e, $advanced, $advancedTrigger) {
|
||||
$advancedTrigger.hide();
|
||||
$advanced.show();
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function focus() {
|
||||
if (tagInput) {
|
||||
tagInput.focus();
|
||||
@ -75,15 +95,11 @@ App.Presenters.PostEditPresenter = function(
|
||||
}
|
||||
|
||||
function postContentChanged(files) {
|
||||
postContentFileDropper.readAsDataURL(files[0], function(content) {
|
||||
postContent = content;
|
||||
});
|
||||
postContent = files[0];
|
||||
}
|
||||
|
||||
function postThumbnailChanged(files) {
|
||||
postThumbnailFileDropper.readAsDataURL(files[0], function(content) {
|
||||
postThumbnail = content;
|
||||
});
|
||||
postThumbnail = files[0];
|
||||
}
|
||||
|
||||
function getPrivileges() {
|
||||
@ -92,37 +108,36 @@ App.Presenters.PostEditPresenter = function(
|
||||
|
||||
function editPost() {
|
||||
var $form = $target.find('form');
|
||||
var formData = {};
|
||||
formData.seenEditTime = post.lastEditTime;
|
||||
formData.flags = {};
|
||||
var formData = new FormData();
|
||||
formData.append('lastEditTime', post.lastEditTime);
|
||||
|
||||
if (privileges.canChangeContent && postContent) {
|
||||
formData.content = postContent;
|
||||
formData.append('content', postContent);
|
||||
}
|
||||
|
||||
if (privileges.canChangeThumbnail && postThumbnail) {
|
||||
formData.thumbnail = postThumbnail;
|
||||
formData.append('thumbnail', postThumbnail);
|
||||
}
|
||||
|
||||
if (privileges.canChangeSource) {
|
||||
formData.source = $form.find('[name=source]').val();
|
||||
formData.append('source', $form.find('[name=source]').val());
|
||||
}
|
||||
|
||||
if (privileges.canChangeSafety) {
|
||||
formData.safety = $form.find('[name=safety]:checked').val();
|
||||
formData.append('safety', $form.find('[name=safety]:checked').val());
|
||||
}
|
||||
|
||||
if (privileges.canChangeTags) {
|
||||
formData.tags = tagInput.getTags().join(' ');
|
||||
formData.append('tags', tagInput.getTags().join(' '));
|
||||
}
|
||||
|
||||
if (privileges.canChangeRelations) {
|
||||
formData.relations = $form.find('[name=relations]').val();
|
||||
formData.append('relations', $form.find('[name=relations]').val());
|
||||
}
|
||||
|
||||
if (privileges.canChangeFlags) {
|
||||
if (post.contentType === 'video') {
|
||||
formData.flags.loop = $form.find('[name=loop]').is(':checked') ? 1 : 0;
|
||||
formData.append('loop', $form.find('[name=loop]').is(':checked') ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,11 +146,14 @@ App.Presenters.PostEditPresenter = function(
|
||||
return;
|
||||
}
|
||||
|
||||
promise.wait(api.put('/posts/' + post.id, formData))
|
||||
jQuery(document.activeElement).blur();
|
||||
|
||||
promise.wait(api.post('/posts/' + post.id, formData))
|
||||
.then(function(response) {
|
||||
tagList.refreshTags();
|
||||
post = response.json.post;
|
||||
if (typeof(updateCallback) !== 'undefined') {
|
||||
updateCallback(post = response.json);
|
||||
updateCallback(post);
|
||||
}
|
||||
}).fail(function(response) {
|
||||
showEditError(response);
|
||||
@ -155,4 +173,4 @@ App.Presenters.PostEditPresenter = function(
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('postEditPresenter', ['util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);
|
||||
App.DI.register('postEditPresenter', ['jQuery', 'util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);
|
||||
|
@ -29,6 +29,7 @@ App.Presenters.PostListPresenter = function(
|
||||
params.query = params.query || {};
|
||||
|
||||
privileges.canMassTag = auth.hasPrivilege(auth.privileges.massTag);
|
||||
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('post-list'),
|
||||
@ -44,8 +45,8 @@ App.Presenters.PostListPresenter = function(
|
||||
baseUri: '#/posts',
|
||||
backendUri: '/posts',
|
||||
$target: $el.find('.pagination-target'),
|
||||
updateCallback: function($page, data) {
|
||||
renderPosts($page, data.entities);
|
||||
updateCallback: function($page, response) {
|
||||
renderPosts($page, response.json.posts);
|
||||
},
|
||||
},
|
||||
function() {
|
||||
@ -165,6 +166,7 @@ App.Presenters.PostListPresenter = function(
|
||||
util: util,
|
||||
query: params.query,
|
||||
post: post,
|
||||
canViewPosts: privileges.canViewPosts,
|
||||
}) + '</li>');
|
||||
$post.data('post', post);
|
||||
util.loadImagesNicely($post.find('img'));
|
||||
@ -215,11 +217,11 @@ App.Presenters.PostListPresenter = function(
|
||||
tags.push(params.query.massTag);
|
||||
}
|
||||
var formData = {};
|
||||
formData.seenEditTime = post.lastEditTime;
|
||||
formData.lastEditTime = post.lastEditTime;
|
||||
formData.tags = tags.join(' ');
|
||||
promise.wait(api.put('/posts/' + post.id, formData))
|
||||
promise.wait(api.post('/posts/' + post.id, formData))
|
||||
.then(function(response) {
|
||||
post = response.json;
|
||||
post = response.json.post;
|
||||
$post.data('post', post);
|
||||
softRenderPost($post);
|
||||
}).fail(function(response) {
|
||||
|
@ -50,7 +50,7 @@ App.Presenters.PostNotesPresenter = function(
|
||||
privileges: privileges,
|
||||
post: post,
|
||||
notes: notes,
|
||||
formatMarkdown: util.formatMarkdown}));
|
||||
util: util}));
|
||||
|
||||
$form = $target.find('.post-note-edit');
|
||||
var $postNotes = $target.find('.post-note');
|
||||
@ -61,8 +61,10 @@ App.Presenters.PostNotesPresenter = function(
|
||||
$postNote.data('postNote', postNote);
|
||||
$postNote.find('.text-wrapper').click(postNoteClicked);
|
||||
postNote.$element = $postNote;
|
||||
draggable.makeDraggable($postNote, draggable.relativeDragStrategy);
|
||||
resizable.makeResizable($postNote);
|
||||
draggable.makeDraggable($postNote, draggable.relativeDragStrategy, true);
|
||||
resizable.makeResizable($postNote, true);
|
||||
$postNote.mouseenter(function() { postNoteMouseEnter(postNote); });
|
||||
$postNote.mouseleave(function() { postNoteMouseLeave(postNote); });
|
||||
});
|
||||
|
||||
$form.find('button').click(formSubmitted);
|
||||
@ -97,7 +99,10 @@ App.Presenters.PostNotesPresenter = function(
|
||||
promise.wait(api.delete('/notes/' + postNote.id))
|
||||
.then(function() {
|
||||
hideForm();
|
||||
postNote.$element.remove();
|
||||
notes = jQuery.grep(notes, function(otherNote) {
|
||||
return otherNote.id !== postNote.id;
|
||||
});
|
||||
render();
|
||||
}).fail(function(response) {
|
||||
window.alert(response.json && response.json.error || response);
|
||||
});
|
||||
@ -125,7 +130,7 @@ App.Presenters.PostNotesPresenter = function(
|
||||
promise.wait(p)
|
||||
.then(function(response) {
|
||||
hideForm();
|
||||
postNote.id = response.json.id;
|
||||
postNote.id = response.json.note.id;
|
||||
postNote.$element.data('postNote', postNote);
|
||||
render();
|
||||
}).fail(function(response) {
|
||||
@ -141,13 +146,25 @@ App.Presenters.PostNotesPresenter = function(
|
||||
}
|
||||
|
||||
function showPostNoteText(postNote) {
|
||||
postNote.$element.find('.text-wrapper').show();
|
||||
var $textWrapper = postNote.$element.find('.text-wrapper');
|
||||
$textWrapper.show();
|
||||
if ($textWrapper.offset().left + $textWrapper.width() > jQuery(window).outerWidth()) {
|
||||
$textWrapper.offset({left: jQuery(window).outerWidth() - $textWrapper.width()});
|
||||
}
|
||||
}
|
||||
|
||||
function hidePostNoteText(postNote) {
|
||||
postNote.$element.find('.text-wrapper').css('display', '');
|
||||
}
|
||||
|
||||
function postNoteMouseEnter(postNote) {
|
||||
showPostNoteText(postNote);
|
||||
}
|
||||
|
||||
function postNoteMouseLeave(postNote) {
|
||||
hidePostNoteText(postNote);
|
||||
}
|
||||
|
||||
function postNoteClicked(e) {
|
||||
e.preventDefault();
|
||||
var $postNote = jQuery(e.currentTarget).parents('.post-note');
|
||||
@ -163,7 +180,7 @@ App.Presenters.PostNotesPresenter = function(
|
||||
$form.data('postNote', postNote);
|
||||
$form.find('textarea').val(postNote.text);
|
||||
$form.show();
|
||||
draggable.makeDraggable($form, draggable.absoluteDragStrategy);
|
||||
draggable.makeDraggable($form, draggable.absoluteDragStrategy, false);
|
||||
}
|
||||
|
||||
function hideForm() {
|
||||
|
@ -4,6 +4,7 @@ App.Presenters = App.Presenters || {};
|
||||
App.Presenters.PostPresenter = function(
|
||||
_,
|
||||
jQuery,
|
||||
appState,
|
||||
util,
|
||||
promise,
|
||||
api,
|
||||
@ -14,7 +15,7 @@ App.Presenters.PostPresenter = function(
|
||||
postsAroundCalculator,
|
||||
postEditPresenter,
|
||||
postContentPresenter,
|
||||
postCommentListPresenter,
|
||||
commentListPresenter,
|
||||
topNavigationPresenter,
|
||||
messagePresenter) {
|
||||
|
||||
@ -70,8 +71,10 @@ App.Presenters.PostPresenter = function(
|
||||
presenterManager.initPresenters([
|
||||
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}],
|
||||
[postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}],
|
||||
[postCommentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
|
||||
function() { });
|
||||
[commentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
|
||||
function() {
|
||||
syncFitModeButtons();
|
||||
});
|
||||
|
||||
}).fail(function() {
|
||||
console.log(arguments);
|
||||
@ -89,25 +92,25 @@ App.Presenters.PostPresenter = function(
|
||||
if (nextPostUrl) {
|
||||
$nextPost.addClass('enabled');
|
||||
$nextPost.attr('href', nextPostUrl);
|
||||
keyboard.keyup('a', function() {
|
||||
keyboard.keyup(['a', 'left'], function() {
|
||||
router.navigate(nextPostUrl);
|
||||
});
|
||||
} else {
|
||||
$nextPost.removeClass('enabled');
|
||||
$nextPost.removeAttr('href');
|
||||
keyboard.unbind('a');
|
||||
keyboard.unbind(['a', 'left']);
|
||||
}
|
||||
|
||||
if (prevPostUrl) {
|
||||
$prevPost.addClass('enabled');
|
||||
$prevPost.attr('href', prevPostUrl);
|
||||
keyboard.keyup('d', function() {
|
||||
keyboard.keyup(['d', 'right'], function() {
|
||||
router.navigate(prevPostUrl);
|
||||
});
|
||||
} else {
|
||||
$prevPost.removeClass('enabled');
|
||||
$prevPost.removeAttr('href');
|
||||
keyboard.unbind('d');
|
||||
keyboard.unbind(['d', 'right']);
|
||||
}
|
||||
}).fail(function() {
|
||||
});
|
||||
@ -117,7 +120,7 @@ App.Presenters.PostPresenter = function(
|
||||
return promise.make(function(resolve, reject) {
|
||||
promise.wait(api.get('/posts/' + postNameOrId))
|
||||
.then(function(postResponse) {
|
||||
post = postResponse.json;
|
||||
post = postResponse.json.post;
|
||||
resolve();
|
||||
}).fail(function(response) {
|
||||
showGenericError(response);
|
||||
@ -135,7 +138,6 @@ App.Presenters.PostPresenter = function(
|
||||
});
|
||||
|
||||
attachSidebarEvents();
|
||||
|
||||
attachLinksToPostsAround();
|
||||
}
|
||||
|
||||
@ -147,6 +149,7 @@ App.Presenters.PostPresenter = function(
|
||||
|
||||
function softRender() {
|
||||
renderSidebar();
|
||||
syncFitModeButtons();
|
||||
$el.find('video').prop('loop', post.flags.loop);
|
||||
}
|
||||
|
||||
@ -159,12 +162,12 @@ App.Presenters.PostPresenter = function(
|
||||
return templates.post({
|
||||
query: params.query,
|
||||
post: post,
|
||||
forceHttpInPermalinks: appState.get('config').forceHttpInPermalinks,
|
||||
ownScore: post.ownScore,
|
||||
postFavorites: post.favorites,
|
||||
postHistory: post.history,
|
||||
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatFileSize: util.formatFileSize,
|
||||
util: util,
|
||||
|
||||
historyTemplate: templates.history,
|
||||
|
||||
@ -178,6 +181,7 @@ App.Presenters.PostPresenter = function(
|
||||
function attachSidebarEvents() {
|
||||
$el.find('#sidebar .delete').click(deleteButtonClicked);
|
||||
$el.find('#sidebar .feature').click(featureButtonClicked);
|
||||
$el.find('#sidebar .fit-mode a').click(fitModeButtonsClicked);
|
||||
$el.find('#sidebar .edit').click(editButtonClicked);
|
||||
$el.find('#sidebar .history').click(historyButtonClicked);
|
||||
$el.find('#sidebar .add-favorite').click(addFavoriteButtonClicked);
|
||||
@ -207,6 +211,14 @@ App.Presenters.PostPresenter = function(
|
||||
}).fail(showGenericError);
|
||||
}
|
||||
|
||||
function syncFitModeButtons() {
|
||||
var fitStyle = postContentPresenter.getFitMode().style;
|
||||
$el.find('#sidebar .fit-mode a').each(function(i, item) {
|
||||
var $item = jQuery(item);
|
||||
$item.toggleClass('active', $item.attr('data-fit-mode') === fitStyle);
|
||||
});
|
||||
}
|
||||
|
||||
function featureButtonClicked(e) {
|
||||
e.preventDefault();
|
||||
messagePresenter.hideMessages($messages);
|
||||
@ -215,6 +227,17 @@ App.Presenters.PostPresenter = function(
|
||||
}
|
||||
}
|
||||
|
||||
function fitModeButtonsClicked(e) {
|
||||
e.preventDefault();
|
||||
var oldMode = postContentPresenter.getFitMode();
|
||||
var newMode = {
|
||||
style: jQuery(e.target).attr('data-fit-mode'),
|
||||
upscale: oldMode.upscale,
|
||||
};
|
||||
postContentPresenter.changeFitMode(newMode);
|
||||
syncFitModeButtons();
|
||||
}
|
||||
|
||||
function featurePost() {
|
||||
promise.wait(api.post('/posts/' + post.id + '/feature'))
|
||||
.then(function(response) {
|
||||
@ -323,6 +346,7 @@ App.Presenters.PostPresenter = function(
|
||||
App.DI.register('postPresenter', [
|
||||
'_',
|
||||
'jQuery',
|
||||
'appState',
|
||||
'util',
|
||||
'promise',
|
||||
'api',
|
||||
@ -333,7 +357,7 @@ App.DI.register('postPresenter', [
|
||||
'postsAroundCalculator',
|
||||
'postEditPresenter',
|
||||
'postContentPresenter',
|
||||
'postCommentListPresenter',
|
||||
'commentListPresenter',
|
||||
'topNavigationPresenter',
|
||||
'messagePresenter'],
|
||||
App.Presenters.PostPresenter);
|
||||
|
@ -64,6 +64,8 @@ App.Presenters.PostUploadPresenter = function(
|
||||
$el.find('.remove').click(removeButtonClicked);
|
||||
$el.find('.move-up').click(moveUpButtonClicked);
|
||||
$el.find('.move-down').click(moveDownButtonClicked);
|
||||
$el.find('.previous').click(selectPrevPostTableRow);
|
||||
$el.find('.next').click(selectNextPostTableRow);
|
||||
$el.find('.upload').click(uploadButtonClicked);
|
||||
$el.find('.stop').click(stopButtonClicked);
|
||||
}
|
||||
@ -78,7 +80,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
fileName: null,
|
||||
content: null,
|
||||
url: null,
|
||||
thumbnail: null,
|
||||
getThumbnail: function() { return promise.makeSilent(function(resolve, reject) { resolve(null); }); },
|
||||
$tableRow: null,
|
||||
};
|
||||
}
|
||||
@ -111,7 +113,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
}
|
||||
}
|
||||
$input.val('');
|
||||
var post = addPostFromUrl(url);
|
||||
var post = addPostFromURL(url);
|
||||
selectPostTableRow(post);
|
||||
}
|
||||
|
||||
@ -137,20 +139,13 @@ App.Presenters.PostUploadPresenter = function(
|
||||
allPosts.push(post);
|
||||
setAllPosts(allPosts);
|
||||
createPostTableRow(post);
|
||||
updatePostThumbnailInTable(post);
|
||||
}
|
||||
|
||||
function postChanged(post) {
|
||||
updatePostTableRow(post);
|
||||
}
|
||||
|
||||
function postThumbnailLoaded(post) {
|
||||
var selectedPosts = getSelectedPosts();
|
||||
if (selectedPosts.length === 1 && selectedPosts[0] === post && post.thumbnail !== null) {
|
||||
updatePostThumbnailInForm(post);
|
||||
}
|
||||
updatePostThumbnailInTable(post);
|
||||
}
|
||||
|
||||
function postTableRowClicked(e) {
|
||||
e.preventDefault();
|
||||
if (!interactionEnabled) {
|
||||
@ -161,6 +156,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
$allCheckboxes.prop('checked', false);
|
||||
$myCheckbox.prop('checked', true);
|
||||
postTableCheckboxesChanged(e);
|
||||
tagInput.focus();
|
||||
}
|
||||
|
||||
function postTableCheckboxClicked(e) {
|
||||
@ -209,24 +205,6 @@ App.Presenters.PostUploadPresenter = function(
|
||||
postTableSelectionChanged(selectedPosts);
|
||||
}
|
||||
|
||||
function postTableRowImageHovered(e) {
|
||||
var $img = jQuery(this);
|
||||
if ($img.parents('tr').data('post').thumbnail) {
|
||||
var $lightbox = jQuery('#lightbox');
|
||||
$lightbox.find('img').attr('src', $img.attr('src'));
|
||||
$lightbox
|
||||
.show()
|
||||
.css({
|
||||
left: ($img.position().left + $img.outerWidth()) + 'px',
|
||||
top: ($img.position().top + ($img.outerHeight() - $lightbox.outerHeight()) / 2) + 'px',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function postTableRowImageUnhovered(e) {
|
||||
jQuery('#lightbox').hide();
|
||||
}
|
||||
|
||||
function removeButtonClicked(e) {
|
||||
e.preventDefault();
|
||||
removePosts(getSelectedPosts());
|
||||
@ -255,35 +233,75 @@ App.Presenters.PostUploadPresenter = function(
|
||||
stopUpload();
|
||||
}
|
||||
|
||||
function addPostFromFile(file) {
|
||||
var post = _.extend({}, getDefaultPost(), {fileName: file.name});
|
||||
|
||||
fileDropper.readAsDataURL(file, function(content) {
|
||||
post.content = content;
|
||||
if (file.type.match('image.*')) {
|
||||
post.thumbnail = content;
|
||||
postThumbnailLoaded(post);
|
||||
function makeThumbnail(thumbnailWidth, thumbnailHeight, file) {
|
||||
return promise.makeSilent(function(resolve, reject) {
|
||||
var canvas = document.createElement('canvas');
|
||||
var img = new Image();
|
||||
canvas.width = thumbnailWidth;
|
||||
canvas.height = thumbnailHeight;
|
||||
var context = canvas.getContext('2d');
|
||||
img.onload = function() {
|
||||
//memory still leaks...
|
||||
img.onload = null;
|
||||
context.drawImage(img, 0, 0, thumbnailWidth, thumbnailHeight);
|
||||
URL.revokeObjectURL(img.src);
|
||||
img.src = null;
|
||||
resolve(canvas.toDataURL());
|
||||
};
|
||||
img.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function addPostFromFile(file) {
|
||||
var post = _.extend({}, getDefaultPost(), {
|
||||
fileName: file.name,
|
||||
file: file,
|
||||
getThumbnail: function(thumbnailWidth, thumbnailHeight) {
|
||||
return promise.makeSilent(function(resolve, reject) {
|
||||
if (!file.type.match('image.*')) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
if (thumbnailWidth === null || thumbnailHeight === null) {
|
||||
resolve(URL.createObjectURL(post.file));
|
||||
return;
|
||||
}
|
||||
makeThumbnail(thumbnailWidth, thumbnailHeight, post.file)
|
||||
.then(function(thumbnailDataURL) {
|
||||
resolve(thumbnailDataURL);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
postAdded(post);
|
||||
return post;
|
||||
}
|
||||
|
||||
function addPostFromUrl(url) {
|
||||
var post = _.extend({}, getDefaultPost(), {url: url, fileName: url});
|
||||
postAdded(post);
|
||||
setPostsSource([post], url);
|
||||
function addPostFromURL(url) {
|
||||
var post = _.extend({}, getDefaultPost(), {
|
||||
url: url,
|
||||
fileName: url,
|
||||
});
|
||||
|
||||
var matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/);
|
||||
if (matches) {
|
||||
var youtubeThumbnailUrl = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
|
||||
post.thumbnail = youtubeThumbnailUrl;
|
||||
postThumbnailLoaded(post);
|
||||
var youtubeThumbnailURL = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
|
||||
post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
|
||||
return promise.makeSilent(function(resolve, reject) {
|
||||
resolve(youtubeThumbnailURL);
|
||||
});
|
||||
};
|
||||
} else if (url.match(/image|img|jpg|png|gif/i)) {
|
||||
post.thumbnail = url;
|
||||
postThumbnailLoaded(post);
|
||||
post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
|
||||
return promise.makeSilent(function(resolve, reject) {
|
||||
resolve(url);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
postAdded(post);
|
||||
setPostsSource([post], url);
|
||||
return post;
|
||||
}
|
||||
|
||||
@ -295,9 +313,8 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
$row.removeClass('template');
|
||||
$row.find('td:not(.checkbox)').click(postTableRowClicked);
|
||||
$row.find('a').click(postTableRowClicked);
|
||||
$row.find('td.checkbox').click(postTableCheckboxClicked);
|
||||
$row.find('img').mouseenter(postTableRowImageHovered);
|
||||
$row.find('img').mouseleave(postTableRowImageUnhovered);
|
||||
$row.data('post', post);
|
||||
$table.find('tbody').append($row);
|
||||
$row.find('td.checkbox input').attr('id', _.uniqueId());
|
||||
@ -315,21 +332,31 @@ App.Presenters.PostUploadPresenter = function(
|
||||
}
|
||||
|
||||
function updatePostThumbnailInForm(post) {
|
||||
if (post.thumbnail === null) {
|
||||
$el.find('.form-slider .thumbnail img').hide();
|
||||
post.getThumbnail(null, null).then(function(thumbnailDataURL) {
|
||||
var $thumbnail = $el.find('.form-slider .thumbnail');
|
||||
var $img = $thumbnail.find('img');
|
||||
var $link = $thumbnail.find('a');
|
||||
if (thumbnailDataURL === null) {
|
||||
$img.hide();
|
||||
$link.hide();
|
||||
} else {
|
||||
$el.find('.form-slider .thumbnail img').show()[0].setAttribute('src', post.thumbnail);
|
||||
$img.show();
|
||||
$img.attr('src', thumbnailDataURL);
|
||||
$link.show();
|
||||
$link.attr('href', thumbnailDataURL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatePostThumbnailInTable(post) {
|
||||
post.getThumbnail(30, 30).then(function(thumbnailDataURL) {
|
||||
var $row = post.$tableRow;
|
||||
if (post.thumbnail === null) {
|
||||
$row.find('img')[0].setAttribute('src', util.transparentPixel());
|
||||
//huge speedup thanks to this condition
|
||||
} else if ($row.find('img').attr('src') !== post.thumbnail) {
|
||||
$row.find('img')[0].setAttribute('src', post.thumbnail);
|
||||
if (thumbnailDataURL === null) {
|
||||
$row.find('img').attr('src', util.transparentPixel());
|
||||
} else {
|
||||
$row.find('img').attr('src', thumbnailDataURL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getAllPosts() {
|
||||
@ -361,6 +388,9 @@ App.Presenters.PostUploadPresenter = function(
|
||||
showPostEditForm(selectedPosts);
|
||||
}
|
||||
$el.find('.post-table-op').prop('disabled', selectedPosts.length === 0);
|
||||
if (selectedPosts.length === 1) {
|
||||
updatePostThumbnailInForm(selectedPosts[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function hidePostEditForm() {
|
||||
@ -410,6 +440,17 @@ App.Presenters.PostUploadPresenter = function(
|
||||
};
|
||||
}
|
||||
|
||||
function getTagIndex(post, tag) {
|
||||
var tags = jQuery.map(post.tags, function(tag) {
|
||||
return tag.toLowerCase();
|
||||
});
|
||||
return tags.indexOf(tag.toLowerCase());
|
||||
}
|
||||
|
||||
function hasTag(post, tag) {
|
||||
return getTagIndex(post, tag) !== -1;
|
||||
}
|
||||
|
||||
function getCombinedPost(posts) {
|
||||
var combinedPost = _.extend({}, getDefaultPost());
|
||||
if (posts.length === 0) {
|
||||
@ -419,7 +460,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
var tagFilter = function(post) {
|
||||
return function(tag) {
|
||||
return post.tags.indexOf(tag) !== -1;
|
||||
return hasTag(post, tag);
|
||||
};
|
||||
};
|
||||
|
||||
@ -442,7 +483,6 @@ App.Presenters.PostUploadPresenter = function(
|
||||
function setPostsSource(posts, newSource) {
|
||||
_.each(posts, function(post) {
|
||||
var maxSourceLength = 200;
|
||||
console.log(newSource);
|
||||
if (newSource.length > maxSourceLength) {
|
||||
newSource = newSource.substring(0, maxSourceLength - 5) + '(...)';
|
||||
}
|
||||
@ -467,8 +507,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
function addTagToPosts(posts, tag) {
|
||||
jQuery.each(posts, function(i, post) {
|
||||
var index = post.tags.indexOf(tag);
|
||||
if (index === -1) {
|
||||
if (!hasTag(post, tag)) {
|
||||
post.tags.push(tag);
|
||||
}
|
||||
postChanged(post);
|
||||
@ -477,9 +516,8 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
function removeTagFromPosts(posts, tag) {
|
||||
jQuery.each(posts, function(i, post) {
|
||||
var index = post.tags.indexOf(tag);
|
||||
if (index !== -1) {
|
||||
post.tags.splice(index, 1);
|
||||
if (hasTag(post, tag)) {
|
||||
post.tags.splice(getTagIndex(post, tag), 1);
|
||||
}
|
||||
postChanged(post);
|
||||
});
|
||||
@ -502,10 +540,12 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
function selectPrevPostTableRow() {
|
||||
selectPostTableRow($el.find('tbody tr.selected:eq(0)').prev().data('post'));
|
||||
return false;
|
||||
}
|
||||
|
||||
function selectNextPostTableRow() {
|
||||
selectPostTableRow($el.find('tbody tr.selected:eq(0)').next().data('post'));
|
||||
return false;
|
||||
}
|
||||
|
||||
function showOrHidePostsTable() {
|
||||
@ -576,17 +616,17 @@ App.Presenters.PostUploadPresenter = function(
|
||||
var post = posts[0];
|
||||
var $row = post.$tableRow;
|
||||
|
||||
var formData = {};
|
||||
var formData = new FormData();
|
||||
if (post.url) {
|
||||
formData.url = post.url;
|
||||
formData.append('url', post.url);
|
||||
} else {
|
||||
formData.content = post.content;
|
||||
formData.contentFileName = post.fileName;
|
||||
formData.append('content', post.file);
|
||||
formData.append('contentFileName', post.fileName);
|
||||
}
|
||||
formData.source = post.source;
|
||||
formData.safety = post.safety;
|
||||
formData.anonymous = (post.anonymous | 0);
|
||||
formData.tags = post.tags.join(' ');
|
||||
formData.append('source', post.source || '');
|
||||
formData.append('safety', post.safety);
|
||||
formData.append('anonymous', (post.anonymous | 0));
|
||||
formData.append('tags', post.tags.join(' '));
|
||||
|
||||
if (post.tags.length === 0) {
|
||||
showUploadError('No tags set.');
|
||||
|
@ -60,7 +60,7 @@ App.Presenters.RegistrationPresenter = function(
|
||||
function registrationSuccess(apiResponse) {
|
||||
$el.find('form').slideUp(function() {
|
||||
var message = 'Registration complete! ';
|
||||
if (!apiResponse.json.confirmed) {
|
||||
if (!apiResponse.json.user.confirmed) {
|
||||
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
|
||||
} else {
|
||||
message += '<a href="#/login">Click here</a> to login.';
|
||||
|
@ -10,8 +10,6 @@ App.Presenters.TagListPresenter = function(
|
||||
pagerPresenter,
|
||||
topNavigationPresenter) {
|
||||
|
||||
var KEY_RETURN = 13;
|
||||
|
||||
var $el = jQuery('#content');
|
||||
var $searchInput;
|
||||
var templates = {};
|
||||
@ -38,8 +36,8 @@ App.Presenters.TagListPresenter = function(
|
||||
baseUri: '#/tags',
|
||||
backendUri: '/tags',
|
||||
$target: $el.find('.pagination-target'),
|
||||
updateCallback: function($page, data) {
|
||||
renderTags($page, data.entities);
|
||||
updateCallback: function($page, response) {
|
||||
renderTags($page, response.json.tags);
|
||||
},
|
||||
},
|
||||
function() {
|
||||
@ -78,8 +76,8 @@ App.Presenters.TagListPresenter = function(
|
||||
function render() {
|
||||
$el.html(templates.list());
|
||||
$searchInput = $el.find('input[name=query]');
|
||||
$searchInput.keydown(searchInputKeyPressed);
|
||||
$el.find('form').submit(searchFormSubmitted);
|
||||
App.Controls.AutoCompleteInput($searchInput);
|
||||
softRender();
|
||||
}
|
||||
|
||||
@ -88,13 +86,6 @@ App.Presenters.TagListPresenter = function(
|
||||
}
|
||||
|
||||
|
||||
function searchInputKeyPressed(e) {
|
||||
if (e.which !== KEY_RETURN) {
|
||||
return;
|
||||
}
|
||||
updateSearch();
|
||||
}
|
||||
|
||||
function searchFormSubmitted(e) {
|
||||
e.preventDefault();
|
||||
updateSearch();
|
||||
@ -117,7 +108,7 @@ App.Presenters.TagListPresenter = function(
|
||||
_.each(tags, function(tag) {
|
||||
var $item = jQuery(templates.listItem({
|
||||
tag: tag,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
util: util,
|
||||
}));
|
||||
$target.append($item);
|
||||
});
|
||||
|
@ -38,6 +38,7 @@ App.Presenters.TagPresenter = function(
|
||||
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
|
||||
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
|
||||
privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags);
|
||||
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('tag'),
|
||||
@ -65,9 +66,9 @@ App.Presenters.TagPresenter = function(
|
||||
api.get('tags/' + tagName + '/siblings'),
|
||||
api.get('posts', {query: tagName}))
|
||||
.then(function(tagResponse, siblingsResponse, postsResponse) {
|
||||
tag = tagResponse.json;
|
||||
siblings = siblingsResponse.json.data;
|
||||
posts = postsResponse.json.data;
|
||||
tag = tagResponse.json.tag;
|
||||
siblings = siblingsResponse.json.tags;
|
||||
posts = postsResponse.json.posts;
|
||||
posts = posts.slice(0, 8);
|
||||
|
||||
render();
|
||||
@ -80,13 +81,22 @@ App.Presenters.TagPresenter = function(
|
||||
});
|
||||
}
|
||||
|
||||
function getTagCategories() {
|
||||
var tagCategories = JSON.parse(jQuery('head').attr('data-tag-categories'));
|
||||
var result = {};
|
||||
jQuery.each(tagCategories, function(i, item) {
|
||||
result[item[0]] = item[1];
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function render() {
|
||||
$el.html(templates.tag({
|
||||
privileges: privileges,
|
||||
tag: tag,
|
||||
siblings: siblings,
|
||||
tagCategories: JSON.parse(jQuery('head').attr('data-tag-categories')),
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
tagCategories: getTagCategories(),
|
||||
util: util,
|
||||
historyTemplate: templates.history,
|
||||
}));
|
||||
$el.find('.post-list').hide();
|
||||
@ -125,7 +135,8 @@ App.Presenters.TagPresenter = function(
|
||||
|
||||
promise.wait(api.put('/tags/' + tag.name, formData))
|
||||
.then(function(response) {
|
||||
router.navigateInplace('#/tag/' + response.json.name);
|
||||
router.navigateInplace('#/tag/' + response.json.tag.name);
|
||||
tagList.refreshTags();
|
||||
}).fail(function(response) {
|
||||
window.alert(response.json && response.json.error || 'An error occured.');
|
||||
});
|
||||
@ -138,6 +149,7 @@ App.Presenters.TagPresenter = function(
|
||||
promise.wait(api.delete('/tags/' + tag.name))
|
||||
.then(function(response) {
|
||||
router.navigate('#/tags');
|
||||
tagList.refreshTags();
|
||||
}).fail(function(response) {
|
||||
window.alert(response.json && response.json.error || 'An error occured.');
|
||||
});
|
||||
@ -149,6 +161,7 @@ App.Presenters.TagPresenter = function(
|
||||
promise.wait(api.put('/tags/' + tag.name + '/merge', {targetTag: targetTag}))
|
||||
.then(function(response) {
|
||||
router.navigate('#/tags');
|
||||
tagList.refreshTags();
|
||||
}).fail(function(response) {
|
||||
window.alert(response.json && response.json.error || 'An error occured.');
|
||||
});
|
||||
@ -162,6 +175,7 @@ App.Presenters.TagPresenter = function(
|
||||
util: util,
|
||||
post: post,
|
||||
query: {query: tag.name},
|
||||
canViewPosts: privileges.canViewPosts,
|
||||
}) + '</li>');
|
||||
$target.append($post);
|
||||
});
|
||||
@ -180,4 +194,16 @@ App.Presenters.TagPresenter = function(
|
||||
|
||||
};
|
||||
|
||||
App.DI.register('tagPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'api', 'tagList', 'router', 'keyboard', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.TagPresenter);
|
||||
App.DI.register('tagPresenter', [
|
||||
'_',
|
||||
'jQuery',
|
||||
'util',
|
||||
'promise',
|
||||
'auth',
|
||||
'api',
|
||||
'tagList',
|
||||
'router',
|
||||
'keyboard',
|
||||
'topNavigationPresenter',
|
||||
'messagePresenter'],
|
||||
App.Presenters.TagPresenter);
|
||||
|
@ -58,7 +58,9 @@ App.Presenters.UserAccountRemovalPresenter = function(
|
||||
}
|
||||
promise.wait(api.delete('/users/' + user.name))
|
||||
.then(function() {
|
||||
if (user.name === auth.getCurrentUser().name) {
|
||||
auth.logout();
|
||||
}
|
||||
var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. <a href="">Back to main page</a>');
|
||||
$messageDiv.find('a').click(mainPageLinkClicked);
|
||||
}).fail(function(response) {
|
||||
|
@ -77,11 +77,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
||||
}
|
||||
|
||||
function avatarContentChanged(files) {
|
||||
if (files.length === 1) {
|
||||
fileDropper.readAsDataURL(files[0], function(content) {
|
||||
avatarContent = content;
|
||||
});
|
||||
}
|
||||
avatarContent = files[0];
|
||||
}
|
||||
|
||||
function accountSettingsFormSubmitted(e) {
|
||||
@ -89,41 +85,45 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
||||
var $el = jQuery(target);
|
||||
var $messages = jQuery(target).find('.messages');
|
||||
messagePresenter.hideMessages($messages);
|
||||
var formData = {};
|
||||
var formData = new FormData();
|
||||
|
||||
if (privileges.canChangeAvatarStyle) {
|
||||
formData.avatarStyle = $el.find('[name=avatar-style]:checked').val();
|
||||
formData.append('avatarStyle', $el.find('[name=avatar-style]:checked').val());
|
||||
if (avatarContent) {
|
||||
formData.avatarContent = avatarContent;
|
||||
formData.append('avatarContent', avatarContent);
|
||||
}
|
||||
}
|
||||
if (privileges.canChangeName) {
|
||||
formData.userName = $el.find('[name=userName]').val();
|
||||
}
|
||||
if (privileges.canChangeEmailAddress) {
|
||||
formData.email = $el.find('[name=email]').val();
|
||||
}
|
||||
if (privileges.canChangePassword) {
|
||||
formData.password = $el.find('[name=password]').val();
|
||||
formData.passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
|
||||
}
|
||||
if (privileges.canChangeAccessRank) {
|
||||
formData.accessRank = $el.find('[name=access-rank]:checked').val();
|
||||
}
|
||||
if (privileges.canBan) {
|
||||
formData.banned = $el.find('[name=ban]').is(':checked') ? 1 : 0;
|
||||
formData.append('userName', $el.find('[name=userName]').val());
|
||||
}
|
||||
|
||||
if (!validateAccountSettingsFormData(formData)) {
|
||||
if (privileges.canChangeEmailAddress) {
|
||||
formData.append('email', $el.find('[name=email]').val());
|
||||
}
|
||||
|
||||
if (privileges.canChangePassword) {
|
||||
var password = $el.find('[name=password]').val();
|
||||
var passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
|
||||
|
||||
if (password) {
|
||||
if (password !== passwordConfirmation) {
|
||||
messagePresenter.showError($messages, 'Passwords must be the same.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.password) {
|
||||
delete formData.password;
|
||||
delete formData.passwordConfirmation;
|
||||
formData.append('password', password);
|
||||
}
|
||||
}
|
||||
|
||||
promise.wait(api.put('/users/' + user.name, formData))
|
||||
if (privileges.canChangeAccessRank) {
|
||||
formData.append('accessRank', $el.find('[name=access-rank]:checked').val());
|
||||
}
|
||||
|
||||
if (privileges.canBan) {
|
||||
formData.append('banned', $el.find('[name=ban]').is(':checked') ? 1 : 0);
|
||||
}
|
||||
|
||||
promise.wait(api.post('/users/' + user.name, formData))
|
||||
.then(function(response) {
|
||||
editSuccess(response);
|
||||
}).fail(function(response) {
|
||||
@ -133,7 +133,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
||||
|
||||
function editSuccess(apiResponse) {
|
||||
var wasLoggedIn = auth.isLoggedIn(user.name);
|
||||
user = apiResponse.json;
|
||||
user = apiResponse.json.user;
|
||||
if (wasLoggedIn) {
|
||||
auth.updateCurrentUser(user);
|
||||
}
|
||||
@ -142,7 +142,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
||||
|
||||
var $messages = jQuery(target).find('.messages');
|
||||
var message = 'Account settings updated!';
|
||||
if (!apiResponse.json.confirmed) {
|
||||
if (!apiResponse.json.user.confirmed) {
|
||||
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
|
||||
}
|
||||
messagePresenter.showInfo($messages, message);
|
||||
@ -153,16 +153,6 @@ App.Presenters.UserAccountSettingsPresenter = function(
|
||||
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
|
||||
}
|
||||
|
||||
function validateAccountSettingsFormData(formData) {
|
||||
var $messages = jQuery(target).find('.messages');
|
||||
if (formData.password !== formData.passwordConfirmation) {
|
||||
messagePresenter.showError($messages, 'Passwords must be the same.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
render: render,
|
||||
|
@ -51,6 +51,9 @@ App.Presenters.UserBrowsingSettingsPresenter = function(
|
||||
sketchy: $el.find('[name=listSketchyPosts]').is(':checked'),
|
||||
unsafe: $el.find('[name=listUnsafePosts]').is(':checked'),
|
||||
},
|
||||
keyboardShortcuts: $el.find('[name=keyboardShortcuts]').is(':checked'),
|
||||
fitMode: $el.find('[name=fitMode]:checked').val(),
|
||||
upscale: $el.find('[name=upscale]').is(':checked'),
|
||||
};
|
||||
|
||||
promise.wait(browsingSettings.setSettings(newSettings))
|
||||
|
@ -13,11 +13,14 @@ App.Presenters.UserListPresenter = function(
|
||||
var $el = jQuery('#content');
|
||||
var templates = {};
|
||||
var params;
|
||||
var privileges = {};
|
||||
|
||||
function init(params, loaded) {
|
||||
topNavigationPresenter.select('users');
|
||||
topNavigationPresenter.changeTitle('Users');
|
||||
|
||||
privileges.canViewUsers = auth.hasPrivilege(auth.privileges.viewUsers);
|
||||
|
||||
promise.wait(
|
||||
util.promiseTemplate('user-list'),
|
||||
util.promiseTemplate('user-list-item'))
|
||||
@ -32,8 +35,8 @@ App.Presenters.UserListPresenter = function(
|
||||
baseUri: '#/users',
|
||||
backendUri: '/users',
|
||||
$target: $el.find('.pagination-target'),
|
||||
updateCallback: function($page, data) {
|
||||
renderUsers($page, data.entities);
|
||||
updateCallback: function($page, response) {
|
||||
renderUsers($page, response.json.users);
|
||||
},
|
||||
},
|
||||
function() {
|
||||
@ -60,7 +63,7 @@ App.Presenters.UserListPresenter = function(
|
||||
}
|
||||
|
||||
function render() {
|
||||
$el.html(templates.list());
|
||||
$el.html(templates.list(privileges));
|
||||
}
|
||||
|
||||
function updateActiveOrder(activeOrder) {
|
||||
@ -71,10 +74,10 @@ App.Presenters.UserListPresenter = function(
|
||||
function renderUsers($page, users) {
|
||||
var $target = $page.find('.users');
|
||||
_.each(users, function(user) {
|
||||
var $item = jQuery('<li>' + templates.listItem({
|
||||
var $item = jQuery('<li>' + templates.listItem(_.extend({
|
||||
user: user,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
}) + '</li>');
|
||||
util: util,
|
||||
}, privileges)) + '</li>');
|
||||
$target.append($item);
|
||||
});
|
||||
_.map(_.map($target.find('img'), jQuery), util.loadImagesNicely);
|
||||
|
@ -41,7 +41,7 @@ App.Presenters.UserPresenter = function(
|
||||
|
||||
promise.wait(api.get('/users/' + userName))
|
||||
.then(function(response) {
|
||||
user = response.json;
|
||||
user = response.json.user;
|
||||
var extendedContext = _.extend(params, {user: user});
|
||||
|
||||
presenterManager.initPresenters([
|
||||
@ -74,7 +74,7 @@ App.Presenters.UserPresenter = function(
|
||||
$el.html(templates.user({
|
||||
user: user,
|
||||
isLoggedIn: auth.isLoggedIn(user.name),
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
util: util,
|
||||
canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings,
|
||||
canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()),
|
||||
canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount}));
|
||||
|
@ -2,21 +2,32 @@ var App = App || {};
|
||||
|
||||
App.Promise = function(_, jQuery, progress) {
|
||||
|
||||
function BrokenPromiseError(promiseId) {
|
||||
this.name = 'BrokenPromiseError';
|
||||
this.message = 'Broken promise (promise ID: ' + promiseId + ')';
|
||||
}
|
||||
BrokenPromiseError.prototype = new Error();
|
||||
|
||||
var active = [];
|
||||
var promiseId = 0;
|
||||
|
||||
function make(callback) {
|
||||
function make(callback, useProgress) {
|
||||
var deferred = jQuery.Deferred();
|
||||
var promise = deferred.promise();
|
||||
promise.promiseId = ++ promiseId;
|
||||
|
||||
if (useProgress === true) {
|
||||
progress.start();
|
||||
}
|
||||
callback(function() {
|
||||
try {
|
||||
deferred.resolve.apply(deferred, arguments);
|
||||
active = _.without(active, promise.promiseId);
|
||||
progress.done();
|
||||
} catch (e) {
|
||||
if (!(e instanceof BrokenPromiseError)) {
|
||||
console.log(e);
|
||||
}
|
||||
progress.reset();
|
||||
}
|
||||
}, function() {
|
||||
@ -25,6 +36,9 @@ App.Promise = function(_, jQuery, progress) {
|
||||
active = _.without(active, promise.promiseId);
|
||||
progress.done();
|
||||
} catch (e) {
|
||||
if (!(e instanceof BrokenPromiseError)) {
|
||||
console.log(e);
|
||||
}
|
||||
progress.reset();
|
||||
}
|
||||
});
|
||||
@ -33,7 +47,7 @@ App.Promise = function(_, jQuery, progress) {
|
||||
|
||||
promise.always(function() {
|
||||
if (!_.contains(active, promise.promiseId)) {
|
||||
throw new Error('Broken promise (promise ID: ' + promise.promiseId + ')');
|
||||
throw new BrokenPromiseError(promise.promiseId);
|
||||
}
|
||||
});
|
||||
|
||||
@ -60,7 +74,8 @@ App.Promise = function(_, jQuery, progress) {
|
||||
}
|
||||
|
||||
return {
|
||||
make: make,
|
||||
make: function(callback) { return make(callback, true); },
|
||||
makeSilent: function(callback) { return make(callback, false); },
|
||||
wait: wait,
|
||||
getActive: getActive,
|
||||
abortAll: abortAll,
|
||||
|
@ -93,7 +93,7 @@ App.Router = function(_, jQuery, promise, util, appState, presenterManager) {
|
||||
}
|
||||
|
||||
function dispatch() {
|
||||
var url = document.location.hash;
|
||||
var url = decodeURI(document.location.hash);
|
||||
for (var i = 0; i < routes.length; i ++) {
|
||||
var route = routes[i];
|
||||
if (route.match(url)) {
|
||||
|
@ -15,7 +15,7 @@ App.Services.PostsAroundCalculator = function(_, promise, util, pager) {
|
||||
pager.setPage(query.page);
|
||||
promise.wait(pager.retrieveCached())
|
||||
.then(function(response) {
|
||||
var postIds = _.pluck(response.entities, 'id');
|
||||
var postIds = _.pluck(response.json.posts, 'id');
|
||||
var position = _.indexOf(postIds, postId);
|
||||
|
||||
if (position === -1) {
|
||||
@ -41,20 +41,28 @@ App.Services.PostsAroundCalculator = function(_, promise, util, pager) {
|
||||
if (position + direction >= 0 && position + direction < postIds.length) {
|
||||
var url = util.appendComplexRouteParam(
|
||||
'#/post/' + postIds[position + direction],
|
||||
_.extend({page: page}, pager.getSearchParams()));
|
||||
util.simplifySearchQuery(
|
||||
_.extend(
|
||||
{page: page},
|
||||
pager.getSearchParams())));
|
||||
|
||||
resolve(url);
|
||||
} else if (page + direction >= 1) {
|
||||
pager.setPage(page + direction);
|
||||
promise.wait(pager.retrieveCached())
|
||||
.then(function(response) {
|
||||
if (response.entities.length) {
|
||||
if (response.json.posts.length) {
|
||||
var post = direction === - 1 ?
|
||||
_.last(response.entities) :
|
||||
_.first(response.entities);
|
||||
_.last(response.json.posts) :
|
||||
_.first(response.json.posts);
|
||||
|
||||
var url = util.appendComplexRouteParam(
|
||||
'#/post/' + post.id,
|
||||
_.extend({page: page + direction}, pager.getSearchParams()));
|
||||
util.simplifySearchQuery(
|
||||
_.extend(
|
||||
{page: page + direction},
|
||||
pager.getSearchParams())));
|
||||
|
||||
resolve(url);
|
||||
} else {
|
||||
resolve(null);
|
||||
|
@ -2,75 +2,139 @@ var App = App || {};
|
||||
App.Util = App.Util || {};
|
||||
|
||||
App.Util.Draggable = function(jQuery) {
|
||||
var KEY_LEFT = 37;
|
||||
var KEY_UP = 38;
|
||||
var KEY_RIGHT = 39;
|
||||
var KEY_DOWN = 40;
|
||||
|
||||
function relativeDragStrategy($element) {
|
||||
var $parent = $element.parent();
|
||||
var delta;
|
||||
var x = $element.offset().left - $parent.offset().left;
|
||||
var y = $element.offset().top - $parent.offset().top;
|
||||
|
||||
var getPosition = function() {
|
||||
return {x: x, y: y};
|
||||
};
|
||||
|
||||
var setPosition = function(newX, newY) {
|
||||
x = newX;
|
||||
y = newY;
|
||||
var screenX = Math.min(Math.max(newX, 0), $parent.outerWidth() - $element.outerWidth());
|
||||
var screenY = Math.min(Math.max(newY, 0), $parent.outerHeight() - $element.outerHeight());
|
||||
screenX *= 100.0 / $parent.outerWidth();
|
||||
screenY *= 100.0 / $parent.outerHeight();
|
||||
$element.css({
|
||||
left: screenX + '%',
|
||||
top: screenY + '%'});
|
||||
};
|
||||
|
||||
return {
|
||||
click: function(e) {
|
||||
mouseClicked: function(e) {
|
||||
delta = {
|
||||
x: $element.offset().left - e.clientX,
|
||||
y: $element.offset().top - e.clientY,
|
||||
};
|
||||
},
|
||||
|
||||
update: function(e) {
|
||||
var x = e.clientX + delta.x - $parent.offset().left;
|
||||
var y = e.clientY + delta.y - $parent.offset().top;
|
||||
x = Math.min(Math.max(x, 0), $parent.outerWidth() - $element.outerWidth());
|
||||
y = Math.min(Math.max(y, 0), $parent.outerHeight() - $element.outerHeight());
|
||||
x *= 100.0 / $parent.outerWidth();
|
||||
y *= 100.0 / $parent.outerHeight();
|
||||
$element.css({
|
||||
left: x + '%',
|
||||
top: y + '%'});
|
||||
mouseMoved: function(e) {
|
||||
setPosition(
|
||||
e.clientX + delta.x - $parent.offset().left,
|
||||
e.clientY + delta.y - $parent.offset().top);
|
||||
},
|
||||
|
||||
getPosition: getPosition,
|
||||
setPosition: setPosition,
|
||||
};
|
||||
}
|
||||
|
||||
function absoluteDragStrategy($element) {
|
||||
var delta;
|
||||
var x = $element.offset().left;
|
||||
var y = $element.offset().top;
|
||||
|
||||
var getPosition = function() {
|
||||
return {x: x, y: y};
|
||||
};
|
||||
|
||||
var setPosition = function(newX, newY) {
|
||||
x = newX;
|
||||
y = newY;
|
||||
$element.css({
|
||||
left: x + 'px',
|
||||
top: y + 'px'});
|
||||
};
|
||||
|
||||
return {
|
||||
click: function(e) {
|
||||
mouseClicked: function(e) {
|
||||
delta = {
|
||||
x: $element.position().left - e.clientX,
|
||||
y: $element.position().top - e.clientY,
|
||||
};
|
||||
},
|
||||
|
||||
update: function(e) {
|
||||
var x = e.clientX + delta.x;
|
||||
var y = e.clientY + delta.y;
|
||||
$element.css({
|
||||
left: x + 'px',
|
||||
top: y + 'px'});
|
||||
mouseMoved: function(e) {
|
||||
setPosition(e.clientX + delta.x, e.clientY + delta.y);
|
||||
},
|
||||
|
||||
getPosition: getPosition,
|
||||
setPosition: setPosition,
|
||||
};
|
||||
}
|
||||
|
||||
function makeDraggable($element, dragStrategy) {
|
||||
function makeDraggable($element, dragStrategy, enableHotkeys) {
|
||||
var strategy = dragStrategy($element);
|
||||
$element.data('drag-strategy', strategy);
|
||||
|
||||
$element.addClass('draggable');
|
||||
|
||||
$element.mousedown(function(e) {
|
||||
if (e.target !== $element.get(0)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
$element.focus();
|
||||
$element.addClass('dragging');
|
||||
|
||||
strategy.click(e);
|
||||
strategy.mouseClicked(e);
|
||||
jQuery(window).bind('mousemove.elemmove', function(e) {
|
||||
strategy.update(e);
|
||||
strategy.mouseMoved(e);
|
||||
}).bind('mouseup.elemmove', function(e) {
|
||||
e.preventDefault();
|
||||
strategy.update(e);
|
||||
strategy.mouseMoved(e);
|
||||
$element.removeClass('dragging');
|
||||
jQuery(window).unbind('mousemove.elemmove');
|
||||
jQuery(window).unbind('mouseup.elemmove');
|
||||
});
|
||||
});
|
||||
|
||||
if (enableHotkeys) {
|
||||
$element.keydown(function(e) {
|
||||
var position = strategy.getPosition();
|
||||
var oldPosition = {x: position.x, y: position.y};
|
||||
if (e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = e.ctrlKey ? 10 : 1;
|
||||
if (e.which === KEY_LEFT) {
|
||||
position.x -= delta;
|
||||
} else if (e.which === KEY_RIGHT) {
|
||||
position.x += delta;
|
||||
} else if (e.which === KEY_UP) {
|
||||
position.y -= delta;
|
||||
} else if (e.which === KEY_DOWN) {
|
||||
position.y += delta;
|
||||
}
|
||||
|
||||
if (position.x !== oldPosition.x || position.y !== oldPosition.y) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
strategy.setPosition(position.x, position.y);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -146,6 +146,11 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
return future ? 'in ' + text : text + ' ago';
|
||||
}
|
||||
|
||||
function formatAbsoluteTime(timeString) {
|
||||
var time = new Date(Date.parse(timeString));
|
||||
return time.toString();
|
||||
}
|
||||
|
||||
function formatUnits(number, base, suffixes, callback) {
|
||||
if (!number && number !== 0) {
|
||||
return NaN;
|
||||
@ -188,11 +193,23 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
smartypants: true,
|
||||
};
|
||||
|
||||
var sjis = [];
|
||||
|
||||
var preDecorator = function(text) {
|
||||
text = text.replace(/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig, function(match, capture) {
|
||||
var ret = '%%%SJIS' + sjis.length;
|
||||
sjis.push(capture);
|
||||
return ret;
|
||||
});
|
||||
//prevent ^#... from being treated as headers, due to tag permalinks
|
||||
text = text.replace(/^#/g, '%%%#');
|
||||
//fix \ before ~ being stripped away
|
||||
text = text.replace(/\\~/g, '%%%T');
|
||||
//post, user and tags premalinks
|
||||
text = text.replace(/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, '$1[$2]($2)');
|
||||
text = text.replace(/\]\(@(\d+)\)/g, '](#/post/$1)');
|
||||
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](#/user/$1)');
|
||||
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](#/posts/query=$1)');
|
||||
return text;
|
||||
};
|
||||
|
||||
@ -201,19 +218,17 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
text = text.replace(/%%%T/g, '\\~');
|
||||
text = text.replace(/%%%#/g, '#');
|
||||
|
||||
text = text.replace(/%%%SJIS(\d+)/, function(match, capture) { return '<div class="sjis">' + sjis[capture] + '</div>'; });
|
||||
|
||||
//search permalinks
|
||||
text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>');
|
||||
//spoilers
|
||||
text = text.replace(/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig, '<span class="spoiler">$1</span>');
|
||||
//[small]
|
||||
text = text.replace(/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig, '<small>$1</small>');
|
||||
//strike-through
|
||||
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
|
||||
text = text.replace(/\\~/g, '~');
|
||||
//post premalinks
|
||||
text = text.replace(/(^|[\s<>\(\)\[\]])@(\d+)/g, '$1<a href="#/post/$2"><code>@$2</code></a>');
|
||||
//user permalinks
|
||||
text = text.replace(/(^|[\s<>\(\)\[\]])\+([a-zA-Z0-9_-]+)/g, '$1<a href="#/user/$2"><code>+$2</code></a>');
|
||||
//tag permalinks
|
||||
text = text.replace(/(^|[\s<>\(\)\[\]])\#([^\s<>/\\]+)/g, '$1<a href="#/posts/query=$2"><code>#$2</code></a>');
|
||||
return text;
|
||||
};
|
||||
|
||||
@ -230,9 +245,21 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
return result.slice(0, -1);
|
||||
}
|
||||
|
||||
function simplifySearchQuery(query) {
|
||||
if (typeof(query) === 'undefined') {
|
||||
return {};
|
||||
}
|
||||
if (query.page === 1) {
|
||||
delete query.page;
|
||||
}
|
||||
query = _.pick(query, _.identity); //remove falsy values
|
||||
return query;
|
||||
}
|
||||
|
||||
return {
|
||||
promiseTemplate: promiseTemplate,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
formatAbsoluteTime: formatAbsoluteTime,
|
||||
formatFileSize: formatFileSize,
|
||||
formatMarkdown: formatMarkdown,
|
||||
enableExitConfirmation: enableExitConfirmation,
|
||||
@ -241,6 +268,7 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
transparentPixel: transparentPixel,
|
||||
loadImagesNicely: loadImagesNicely,
|
||||
appendComplexRouteParam: appendComplexRouteParam,
|
||||
simplifySearchQuery: simplifySearchQuery,
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -2,41 +2,103 @@ var App = App || {};
|
||||
App.Util = App.Util || {};
|
||||
|
||||
App.Util.Resizable = function(jQuery) {
|
||||
function makeResizable($element) {
|
||||
var KEY_LEFT = 37;
|
||||
var KEY_UP = 38;
|
||||
var KEY_RIGHT = 39;
|
||||
var KEY_DOWN = 40;
|
||||
|
||||
function relativeResizeStrategy($element) {
|
||||
var $parent = $element.parent();
|
||||
var delta;
|
||||
var width = $element.width();
|
||||
var height = $element.height();
|
||||
|
||||
var getSize = function() {
|
||||
return {width: width, height: height};
|
||||
};
|
||||
|
||||
var setSize = function(newWidth, newHeight) {
|
||||
width = newWidth;
|
||||
height = newHeight;
|
||||
var screenWidth = Math.min(Math.max(width, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
|
||||
var screenHeight = Math.min(Math.max(height, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
|
||||
screenWidth *= 100.0 / $parent.outerWidth();
|
||||
screenHeight *= 100.0 / $parent.outerHeight();
|
||||
$element.css({
|
||||
width: screenWidth + '%',
|
||||
height: screenHeight + '%'});
|
||||
};
|
||||
|
||||
return {
|
||||
mouseClicked: function(e) {
|
||||
delta = {
|
||||
x: $element.width() - e.clientX,
|
||||
y: $element.height() - e.clientY,
|
||||
};
|
||||
},
|
||||
|
||||
mouseMoved: function(e) {
|
||||
setSize(
|
||||
e.clientX + delta.x,
|
||||
e.clientY + delta.y);
|
||||
},
|
||||
|
||||
getSize: getSize,
|
||||
setSize: setSize,
|
||||
};
|
||||
}
|
||||
|
||||
function makeResizable($element, enableHotkeys) {
|
||||
var $resizer = jQuery('<div class="resizer"></div>');
|
||||
var strategy = relativeResizeStrategy($element);
|
||||
$element.append($resizer);
|
||||
|
||||
$resizer.mousedown(function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$element.focus();
|
||||
$element.addClass('resizing');
|
||||
|
||||
var $parent = $element.parent();
|
||||
var deltaX = $element.width() - e.clientX;
|
||||
var deltaY = $element.height() - e.clientY;
|
||||
|
||||
var update = function(e) {
|
||||
var w = e.clientX + deltaX;
|
||||
var h = e.clientY + deltaY;
|
||||
w = Math.min(Math.max(w, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
|
||||
h = Math.min(Math.max(h, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
|
||||
w *= 100.0 / $parent.outerWidth();
|
||||
h *= 100.0 / $parent.outerHeight();
|
||||
$element.css({
|
||||
width: w + '%',
|
||||
height: h + '%'});
|
||||
};
|
||||
strategy.mouseClicked(e);
|
||||
|
||||
jQuery(window).bind('mousemove.elemsize', function(e) {
|
||||
update(e);
|
||||
strategy.mouseMoved(e);
|
||||
}).bind('mouseup.elemsize', function(e) {
|
||||
e.preventDefault();
|
||||
update(e);
|
||||
strategy.mouseMoved(e);
|
||||
$element.removeClass('resizing');
|
||||
jQuery(window).unbind('mousemove.elemsize');
|
||||
jQuery(window).unbind('mouseup.elemsize');
|
||||
});
|
||||
});
|
||||
|
||||
if (enableHotkeys) {
|
||||
$element.keydown(function(e) {
|
||||
var size = strategy.getSize();
|
||||
var oldSize = {width: size.width, height: size.height};
|
||||
if (!e.shiftKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
var delta = e.ctrlKey ? 10 : 1;
|
||||
if (e.which === KEY_LEFT) {
|
||||
size.width -= delta;
|
||||
} else if (e.which === KEY_RIGHT) {
|
||||
size.width += delta;
|
||||
} else if (e.which === KEY_UP) {
|
||||
size.height -= delta;
|
||||
} else if (e.which === KEY_DOWN) {
|
||||
size.height += delta;
|
||||
}
|
||||
|
||||
if (size.width !== oldSize.width || size.height !== oldSize.height) {
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
strategy.setSize(size.width, size.height);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -41,6 +41,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="form-label" for="browsing-settings-keyboard-shortcuts">Keyboard shortcuts:</label>
|
||||
<div class="form-input">
|
||||
<input <% print(settings.keyboardShortcuts ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-keyboard-shortcuts" name="keyboardShortcuts"/>
|
||||
<label for="browsing-settings-keyboard-shortcuts">
|
||||
Enabled
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="form-label">Default fit mode:</label>
|
||||
<div class="form-input">
|
||||
<input <% print(settings.fitMode === 'fit-width' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-width" name="fitMode" value="fit-width"/>
|
||||
<label for="browsing-settings-fit-width">
|
||||
Fit to window width
|
||||
</label>
|
||||
<br/>
|
||||
|
||||
<input <% print(settings.fitMode === 'fit-height' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-height" name="fitMode" value="fit-height"/>
|
||||
<label for="browsing-settings-fit-height">
|
||||
Fit to window height
|
||||
</label>
|
||||
<br/>
|
||||
|
||||
<input <% print(settings.fitMode === 'fit-both' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-both" name="fitMode" value="fit-both"/>
|
||||
<label for="browsing-settings-fit-both">
|
||||
Fit to both width and height
|
||||
</label>
|
||||
<br/>
|
||||
|
||||
<input <% print(settings.fitMode === 'original' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-original" name="fitMode" value="original"/>
|
||||
<label for="browsing-settings-fit-original">
|
||||
Show at original size
|
||||
</label>
|
||||
<br/>
|
||||
|
||||
<input <% print(settings.upscale ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-upscale" name="upscale" value="upscale"/>
|
||||
<label for="browsing-settings-upscale">
|
||||
Upscale small posts
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="form-label"></label>
|
||||
<div class="form-input">
|
||||
@ -48,5 +92,3 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="comment">
|
||||
<div class="avatar">
|
||||
<% if (comment.user.name) { %>
|
||||
<% if (comment.user.name && canViewUsers) { %>
|
||||
<a href="#/user/<%= comment.user.name %>">
|
||||
<% } %>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
src="/data/thumbnails/40x40/avatars/<%= comment.user.name || '!' %>"
|
||||
alt="<%= comment.user.name || 'Anonymous user' %>"/>
|
||||
|
||||
<% if (comment.user.name) { %>
|
||||
<% if (comment.user.name && canViewUsers) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
@ -16,19 +16,19 @@
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<span class="nickname">
|
||||
<% if (comment.user.name) { %>
|
||||
<% if (comment.user.name && canViewUsers) { %>
|
||||
<a href="#/user/<%= comment.user.name %>">
|
||||
<% } %>
|
||||
|
||||
<%= comment.user.name || 'Anonymous user' %>
|
||||
|
||||
<% if (comment.user.name) { %>
|
||||
<% if (comment.user.name && canViewUsers) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</span>
|
||||
|
||||
<span class="date" title="<%= comment.creationTime %>">
|
||||
<%= formatRelativeTime(comment.creationTime) %>
|
||||
<span class="date" title="<%= util.formatAbsoluteTime(comment.creationTime) %>">
|
||||
<%= util.formatRelativeTime(comment.creationTime) %>
|
||||
</span>
|
||||
|
||||
<span class="score">
|
||||
@ -60,7 +60,7 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<%= formatMarkdown(comment.text) %>
|
||||
<%= util.formatMarkdown(comment.text) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
13
public_html/templates/comment-list.tpl
Normal file
13
public_html/templates/comment-list.tpl
Normal file
@ -0,0 +1,13 @@
|
||||
<% if (canListComments && comments.length) { %>
|
||||
<div class="comments">
|
||||
<h1>Comments</h1>
|
||||
<ul class="comments">
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (canAddComments) { %>
|
||||
<div class="comment-add">
|
||||
<%= commentFormTemplate({title: 'Add comment'}) %>
|
||||
</div>
|
||||
<% } %>
|
@ -1,6 +1,6 @@
|
||||
<div class="post-comment">
|
||||
<div class="post">
|
||||
<%= postTemplate({post: post, util: util}) %>
|
||||
<%= postTemplate({post: post, util: util, canViewPosts: canViewPosts}) %>
|
||||
</div>
|
||||
|
||||
<div class="post-comments-target">
|
||||
|
@ -60,10 +60,15 @@
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>[A]</code> and <code>[D]</code></td>
|
||||
<td><code>[A]</code> and <code>[D]</code><br/><code>[Left]</code> and <code>[Right]</code> arrow keys</td>
|
||||
<td>Go to newer/older page or post</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>[F]</code></td>
|
||||
<td>Cycle post fit mode</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td><code>[E]</code></td>
|
||||
<td>Edit post</td>
|
||||
@ -109,6 +114,8 @@
|
||||
{search: 'comment_count:3', description: 'having exactly three comments'},
|
||||
{search: 'score:4', description: 'having score of 4'},
|
||||
{search: 'tag_count:7', description: 'tagged with exactly seven tags'},
|
||||
{search: 'note_count:1..', description: 'having at least one post note'},
|
||||
{search: 'feature_count:1..', description: 'having been featured at least once'},
|
||||
{search: 'date:today', description: 'posted today'},
|
||||
{search: 'date:yesterday', description: 'posted yesterday'},
|
||||
{search: 'date:2000', description: 'posted in year 2000'},
|
||||
@ -116,6 +123,10 @@
|
||||
{search: 'date:2000-01-01', description: 'posted on January 1st, 2000'},
|
||||
{search: 'id:1', description: 'having specific post ID'},
|
||||
{search: 'name:<em>hash</em>', description: 'having specific post name (hash in full URLs)'},
|
||||
{search: 'file_size:100..', description: 'having at least 100 bytes'},
|
||||
{search: 'image_width:100..', description: 'being at least 100 pixels wide'},
|
||||
{search: 'image_height:100..', description: 'being at least 100 pixels tall'},
|
||||
{search: 'image_area:10000..', description: 'having at least 10000 pixels'},
|
||||
{search: 'type:image', description: 'only image posts'},
|
||||
{search: 'type:flash', description: 'only Flash posts'},
|
||||
{search: 'type:youtube', description: 'only Youtube posts'},
|
||||
@ -123,6 +134,7 @@
|
||||
{search: 'special:liked', description: 'posts liked by currently logged in user'},
|
||||
{search: 'special:disliked', description: 'posts disliked by currently logged in user'},
|
||||
{search: 'special:fav', description: 'posts added to favorites by currently logged in user'},
|
||||
{search: 'special:tumbleweed', description: 'posts with score of 0, without comments and without favorites'},
|
||||
];
|
||||
_.each(table, function(row) { %>
|
||||
<tr>
|
||||
@ -159,17 +171,22 @@
|
||||
var table = [
|
||||
{search: 'order:random', description: 'as random as it can get'},
|
||||
{search: 'order:id', description: 'highest to lowest post ID (default browse view)'},
|
||||
{search: 'order:date', description: 'newest to oldest (pretty much same as above)'},
|
||||
{search: '-order:date', description: 'oldest to newest'},
|
||||
{search: 'order:date,asc', description: 'oldest to newest (ascending order, default = descending)'},
|
||||
{search: 'order:creation_date', description: 'newest to oldest (pretty much same as above)'},
|
||||
{search: '-order:creation_date', description: 'oldest to newest'},
|
||||
{search: 'order:creation_date,asc', description: 'oldest to newest (ascending order, default = descending)'},
|
||||
{search: 'order:edit_date', description: 'like <code>creation_date</code>, only looks at last edit time'},
|
||||
{search: 'order:score', description: 'highest scored'},
|
||||
{search: 'order:file_size', description: 'largest files first'},
|
||||
{search: 'order:image_width', description: 'widest images first'},
|
||||
{search: 'order:image_height', description: 'tallest images first'},
|
||||
{search: 'order:image_area', description: 'largest images first'},
|
||||
{search: 'order:tag_count', description: 'with most tags'},
|
||||
{search: 'order:fav_count', description: 'loved by most'},
|
||||
{search: 'order:comment_count', description: 'most commented first'},
|
||||
{search: 'order:fav_date', description: 'recently added to favorites'},
|
||||
{search: 'order:comment_date', description: 'recently commented'},
|
||||
{search: 'order:feature_date', description: 'recently featured'},
|
||||
{search: 'order:feature_count', description: 'most often featured'},
|
||||
];
|
||||
_.each(table, function(row) { %>
|
||||
<tr>
|
||||
@ -181,9 +198,9 @@
|
||||
</table>
|
||||
|
||||
<p>As shown with <a
|
||||
href="#/posts/query=-order:date"><code>-order:date</code></a>, any of them
|
||||
can be reversed in the same way as negating other tags: by placing a dash
|
||||
before the tag.</p>
|
||||
href="#/posts/query=-order:creation_date"><code>-order:creation_date</code></a>,
|
||||
any of them can be reversed in the same way as negating other tags: by
|
||||
placing a dash before the tag.</p>
|
||||
</div>
|
||||
|
||||
<div data-tab="comments">
|
||||
@ -212,6 +229,10 @@
|
||||
<td><code>[spoiler]Lelouch survives[/spoiler]</td>
|
||||
<td>marks text as spoiler and hides it</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[sjis](´・ω・`)[/sjis]</td>
|
||||
<td>adds SJIS art</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -5,14 +5,25 @@ var reprValue = function(value) {
|
||||
}
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
|
||||
var showDifference = function(className, difference) {
|
||||
_.each(difference, function(value, key) {
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
_.each(value, function(v) {
|
||||
%><li class="<%= className %> difference-<%= key %>"><%= key + ':' + reprValue(v) %></li><%
|
||||
});
|
||||
});
|
||||
};
|
||||
%>
|
||||
|
||||
<table class="history">
|
||||
<tbody>
|
||||
<% _.each(history, function( historyEntry) { %>
|
||||
<tr>
|
||||
<td class="time">
|
||||
<%= formatRelativeTime(historyEntry.time) %>
|
||||
<td class="time" title="<%= util.formatAbsoluteTime(historyEntry.time) %>">
|
||||
<%= util.formatRelativeTime(historyEntry.time) %>
|
||||
</td>
|
||||
|
||||
<td class="user">
|
||||
@ -59,17 +70,8 @@ var reprValue = function(value) {
|
||||
|
||||
<% if (historyEntry.dataDifference) { %>
|
||||
<ul><!--
|
||||
--><% _.each(historyEntry.dataDifference['+'], function (difference) { %><!--
|
||||
--><li class="addition difference-<%= difference[0] %>"><!--
|
||||
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
|
||||
--></li><!--
|
||||
--><% }) %><!--
|
||||
|
||||
--><% _.each(historyEntry.dataDifference['-'], function (difference) { %><!--
|
||||
--><li class="removal difference-<%= difference[0] %>"><!--
|
||||
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
|
||||
--></li><!--
|
||||
--><% }) %><!--
|
||||
--><% showDifference('addition', historyEntry.dataDifference['+']) %><!--
|
||||
--><% showDifference('removal', historyEntry.dataDifference['-']) %><!--
|
||||
--></ul>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
@ -1,17 +1,35 @@
|
||||
<% function showUser(name) { %>
|
||||
<% var showLink = typeof(canViewUsers) !== 'undefined' && canViewUsers && name %>
|
||||
|
||||
<% if (showLink) { %>
|
||||
<a href="#/user/<%= name %>">
|
||||
<% } %>
|
||||
|
||||
<img width="25" height="25" class="author-avatar"
|
||||
src="/data/thumbnails/25x25/avatars/<%= name || '!' %>"
|
||||
alt="<%= name || 'Anonymous user' %>"/>
|
||||
|
||||
<%= name || 'Anonymous user' %>
|
||||
|
||||
<% if (showLink) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<div id="home">
|
||||
<h1><%= title %></h1>
|
||||
<p>
|
||||
<small>Serving <%= globals.postCount || 0 %> posts (<%= formatFileSize(globals.postSize || 0) %>)</small>
|
||||
<p class="subheader">
|
||||
Serving <%= globals.postCount || 0 %> posts (<%= util.formatFileSize(globals.postSize || 0) %>)
|
||||
</p>
|
||||
|
||||
<% if (post && post.id) { %>
|
||||
<div class="post">
|
||||
<div class="post" style="width: <%= post.imageWidth || 800 %>px">
|
||||
<div id="post-content-target">
|
||||
</div>
|
||||
|
||||
<div class="post-footer">
|
||||
|
||||
<small class="left">
|
||||
<span class="left">
|
||||
<% var showLink = canViewPosts %>
|
||||
|
||||
<% if (showLink) { %>
|
||||
@ -25,30 +43,17 @@
|
||||
<% } %>
|
||||
|
||||
uploaded
|
||||
<%= formatRelativeTime(post.uploadTime) %>
|
||||
</small>
|
||||
|
||||
<small class="right">
|
||||
featured
|
||||
<%= formatRelativeTime(post.lastFeatureTime) %>
|
||||
<%= util.formatRelativeTime(post.creationTime) %>
|
||||
by
|
||||
<% showUser(post.user.name) %>
|
||||
</span>
|
||||
|
||||
<% var showLink = canViewUsers && user.name %>
|
||||
|
||||
<% if (showLink) { %>
|
||||
<a href="#/user/<%= user.name %>">
|
||||
<% } %>
|
||||
|
||||
<img width="25" height="25" class="author-avatar"
|
||||
src="/data/thumbnails/25x25/avatars/<%= user.name || '!' %>"
|
||||
alt="<%= user.name || 'Anonymous user' %>"/>
|
||||
|
||||
<%= user.name || 'Anonymous user' %>
|
||||
|
||||
<% if (showLink) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</small>
|
||||
<span class="right">
|
||||
featured
|
||||
<%= util.formatRelativeTime(post.lastFeatureTime) %>
|
||||
by
|
||||
<% showUser(user.name) %>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@ -56,7 +61,7 @@
|
||||
|
||||
<p>
|
||||
<small class="version">
|
||||
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= formatRelativeTime(buildTime) %>)
|
||||
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= util.formatRelativeTime(buildTime) %>)
|
||||
|
|
||||
<a href="#/history">Recent tag and post edits</a>
|
||||
</small>
|
||||
|
@ -2,4 +2,6 @@
|
||||
</div>
|
||||
|
||||
<ul class="page-list">
|
||||
<li class="prev"><a href="#">Prev</a></li>
|
||||
<li class="next"><a href="#">Next</a></li>
|
||||
</ul>
|
||||
|
@ -1,13 +0,0 @@
|
||||
<% if (canListComments && comments.length) { %>
|
||||
<div class="comments">
|
||||
<h1>Comments</h1>
|
||||
<ul class="comments">
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (canAddComments) { %>
|
||||
<div class="comment-add">
|
||||
<%= commentFormTemplate({title: 'Add comment'}) %>
|
||||
</div>
|
||||
<% } %>
|
@ -1,15 +1,28 @@
|
||||
<% var postContentUrl = '/data/posts/' + post.name + '?x=' + Math.random() /* reset gif animations */ %>
|
||||
<%
|
||||
var postContentUrl = '/data/posts/' + post.name;
|
||||
var width;
|
||||
var height;
|
||||
if (post.contentType === 'image' || post.contentType === 'animation' || post.contentType === 'flash') {
|
||||
width = post.imageWidth;
|
||||
height = post.imageHeight;
|
||||
}
|
||||
if (!width) { width = 800; }
|
||||
if (!height) { height = 450; }
|
||||
%>
|
||||
|
||||
<div class="post-content post-type-<%= post.contentType %>">
|
||||
<div class="post-notes-target">
|
||||
</div>
|
||||
|
||||
<% if (post.contentType === 'image') { %>
|
||||
<div
|
||||
class="object-wrapper"
|
||||
data-width="<%= width %>"
|
||||
data-height="<%= height %>"
|
||||
style="max-width: <%= width %>px">
|
||||
|
||||
<% if (post.contentType === 'image' || post.contentType === 'animation') { %>
|
||||
|
||||
<div class="image-wrapper" style="width: <%= post.imageWidth %>px">
|
||||
<img alt="<%= post.name %>" src="<%= postContentUrl %>"/>
|
||||
<div style="padding-top: calc(100% * <%= post.imageHeight %> / <%= post.imageWidth %>)"></div>
|
||||
</div>
|
||||
|
||||
<% } else if (post.contentType === 'youtube') { %>
|
||||
|
||||
@ -19,14 +32,15 @@
|
||||
|
||||
<object
|
||||
type="<%= post.contentMimeType %>"
|
||||
width="<%= post.imageWidth %>"
|
||||
height="<%= post.imageHeight %>"
|
||||
width="<%= width %>"
|
||||
height="<%= height %>"
|
||||
data="<%= postContentUrl %>">
|
||||
<param name="wmode" value="opaque"/>
|
||||
<param name="movie" value="<%= postContentUrl %>"/>
|
||||
</object>
|
||||
|
||||
<% } else if (post.contentType === 'video') { %>
|
||||
|
||||
<% if (post.flags.loop) { %>
|
||||
<video id="video" controls loop="loop">
|
||||
<% } else { %>
|
||||
@ -40,4 +54,7 @@
|
||||
|
||||
<% } else { console.log(new Error('Unknown post type')) } %>
|
||||
|
||||
<div class="padding-fix" style="padding-bottom: calc(100% * <%= height %> / <%= width %>)"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -30,8 +30,15 @@
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="form-row advanced-trigger">
|
||||
<label></label>
|
||||
<div class="form-input">
|
||||
<a href="#">Advanced…</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if (privileges.canChangeSource) { %>
|
||||
<div class="form-row">
|
||||
<div class="form-row advanced">
|
||||
<label class="form-label" for="post-source">Source:</label>
|
||||
<div class="form-input">
|
||||
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value="<%= post.source %>"/>
|
||||
@ -40,7 +47,7 @@
|
||||
<% } %>
|
||||
|
||||
<% if (privileges.canChangeRelations) { %>
|
||||
<div class="form-row">
|
||||
<div class="form-row advanced">
|
||||
<label class="form-label" for="post-relations">Relations:</label>
|
||||
<div class="form-input">
|
||||
<input maxlength="200" type="text" name="relations" id="post-relations" placeholder="Post ids, separated with space" value="<%= _.pluck(post.relations, 'id').join(' ') %>"/>
|
||||
@ -49,7 +56,7 @@
|
||||
<% } %>
|
||||
|
||||
<% if (privileges.canChangeFlags && post.contentType === 'video') { %>
|
||||
<div class="form-row">
|
||||
<div class="form-row advanced">
|
||||
<label class="form-label">Loop:</label>
|
||||
<div class="form-input">
|
||||
<input type="checkbox" id="post-loop" name="loop" value="loop" <%= post.flags.loop ? 'checked="checked"' : '' %>/>
|
||||
@ -61,7 +68,7 @@
|
||||
<% } %>
|
||||
|
||||
<% if (privileges.canChangeContent) { %>
|
||||
<div class="form-row">
|
||||
<div class="form-row advanced">
|
||||
<label class="form-label" for="post-content">Content:</label>
|
||||
<div class="form-input">
|
||||
<input type="file" id="post-content" name="content"/>
|
||||
@ -70,7 +77,7 @@
|
||||
<% } %>
|
||||
|
||||
<% if (privileges.canChangeThumbnail) { %>
|
||||
<div class="form-row">
|
||||
<div class="form-row advanced">
|
||||
<label class="form-label" for="post-thumbnail">Thumbnail:</label>
|
||||
<div class="form-input">
|
||||
<input type="file" id="post-thumbnail" name="thumbnail"/>
|
||||
|
@ -1,7 +1,12 @@
|
||||
<div class="post-small post-type-<%= post.contentType %> ">
|
||||
|
||||
<% if (canViewPosts) { %>
|
||||
<a class="link"
|
||||
href="<%= util.appendComplexRouteParam('#/post/' + post.id, typeof(query) !== 'undefined' ? query : {}) %>"
|
||||
href="<%= util.appendComplexRouteParam('#/post/' + post.id, util.simplifySearchQuery(typeof(query) !== 'undefined' ? query : {})) %>"
|
||||
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
|
||||
<% } else { %>
|
||||
<span class="link">
|
||||
<% } %>
|
||||
|
||||
<img width="160" height="160" class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>
|
||||
|
||||
@ -31,7 +36,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (canViewPosts) { %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
</span>
|
||||
<% } %>
|
||||
|
||||
<div class="action">
|
||||
<button>Action</button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="post-notes">
|
||||
<% _.each(notes, function(note) { %>
|
||||
<div class="post-note"
|
||||
<div tabindex="0" class="post-note"
|
||||
style="left: <%= note.left %>%;
|
||||
top: <%= note.top %>%;
|
||||
width: <%= note.width %>%;
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
<div class="text-wrapper">
|
||||
<div class="text">
|
||||
<%= formatMarkdown(note.text) %>
|
||||
<%= util.formatMarkdown(note.text) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -39,7 +39,9 @@
|
||||
<label></label>
|
||||
</td>
|
||||
<td class="thumbnail">
|
||||
<a href="#"/>
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
|
||||
</a>
|
||||
</td>
|
||||
<td class="tags"></td>
|
||||
<td class="safety"><div class="safety-template"></div></td>
|
||||
@ -52,10 +54,16 @@
|
||||
<button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
|
||||
</li><!--
|
||||
--><li>
|
||||
<button class="post-table-op move-up"><i class="fa fa-chevron-up"></i> Move up</button>
|
||||
<button class="post-table-op previous"><i class="fa fa-chevron-up"></i> Previous</button>
|
||||
</li><!--
|
||||
--><li>
|
||||
<button class="post-table-op move-down"><i class="fa fa-chevron-down"></i> Move down</button>
|
||||
<button class="post-table-op next"><i class="fa fa-chevron-down"></i> Next</button>
|
||||
</li><!--
|
||||
--><li>
|
||||
<button class="post-table-op move-up"><i class="fa fa-arrow-up"></i> Move up</button>
|
||||
</li><!--
|
||||
--><li>
|
||||
<button class="post-table-op move-down"><i class="fa fa-arrow-down"></i> Move down</button>
|
||||
</li><!--
|
||||
--><li>
|
||||
<button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
|
||||
@ -73,6 +81,7 @@
|
||||
<div class="form-slider">
|
||||
<div class="thumbnail">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
|
||||
<a href="#" target="_blank">Open preview in a new tab</a>
|
||||
</div>
|
||||
|
||||
<form class="form-wrapper">
|
||||
@ -134,8 +143,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="lightbox">
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Preview">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -1,4 +1,13 @@
|
||||
<% var permaLink = (window.location.origin + '/' + window.location.pathname + '/data/posts/' + post.name).replace(/([^:])\/+/g, '$1/') %>
|
||||
<%
|
||||
var permaLink = '';
|
||||
permaLink += window.location.origin + '/';
|
||||
permaLink += window.location.pathname + '/';
|
||||
permaLink += 'data/posts/' + post.name;
|
||||
permaLink = permaLink.replace(/([^:])\/+/g, '$1/');
|
||||
if (forceHttpInPermalinks > 0) {
|
||||
permaLink = permaLink.replace('https', 'http');
|
||||
}
|
||||
%>
|
||||
|
||||
<div id="post-current-search-wrapper">
|
||||
<div id="post-current-search">
|
||||
@ -10,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
<div class="search">
|
||||
<a href="#/posts/query=<%= query.query %>;order=<%= query.order %>">
|
||||
<a class="enabled" href="<%= util.appendComplexRouteParam('#/posts', util.simplifySearchQuery({query: query.query, order: query.order})) %>">
|
||||
Current search: <%= query.query || '-' %>
|
||||
</a>
|
||||
</div>
|
||||
@ -27,13 +36,15 @@
|
||||
<div id="post-view-wrapper">
|
||||
<div id="sidebar">
|
||||
<ul class="essential">
|
||||
<% if (post.contentType !== 'youtube') { %>
|
||||
<li>
|
||||
<a class="download" href="<%= permaLink %>">
|
||||
<i class="fa fa-download"></i>
|
||||
<br/>
|
||||
<%= post.contentExtension + ', ' + formatFileSize(post.originalFileSize) %>
|
||||
<%= post.contentExtension + ', ' + util.formatFileSize(post.originalFileSize) %>
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (isLoggedIn) { %>
|
||||
<li>
|
||||
@ -70,6 +81,7 @@
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<div class="box">
|
||||
<h1>Tags (<%= _.size(post.tags) %>)</h1>
|
||||
<ul class="tags">
|
||||
<% _.each(post.tags, function(tag) { %>
|
||||
@ -85,9 +97,10 @@
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h1>Details</h1>
|
||||
|
||||
<div class="author-box">
|
||||
<% if (post.user.name) { %>
|
||||
<a href="#/user/<%= post.user.name %>">
|
||||
@ -107,11 +120,12 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<span class="date"><%= formatRelativeTime(post.uploadTime) %></span>
|
||||
<span class="date" title="<%= util.formatAbsoluteTime(post.creationTime) %>">
|
||||
<%= util.formatRelativeTime(post.creationTime) %>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<ul class="other-info">
|
||||
|
||||
<li>
|
||||
Rating:
|
||||
<span class="safety-<%= post.safety %>">
|
||||
@ -122,7 +136,7 @@
|
||||
<% if (post.originalFileSize) { %>
|
||||
<li>
|
||||
File size:
|
||||
<%= formatFileSize(post.originalFileSize) %>
|
||||
<%= util.formatFileSize(post.originalFileSize) %>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
@ -133,17 +147,19 @@
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (post.lastEditTime !== post.uploadTime) { %>
|
||||
<% if (post.lastEditTime !== post.creationTime) { %>
|
||||
<li>
|
||||
Edited:
|
||||
<%= formatRelativeTime(post.lastEditTime) %>
|
||||
<span title="<%= util.formatAbsoluteTime(post.lastEditTime) %>">
|
||||
<%= util.formatRelativeTime(post.lastEditTime) %>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (post.featureCount > 0) { %>
|
||||
<li>
|
||||
Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %>
|
||||
<small>(<%= formatRelativeTime(post.lastFeatureTime) %>)</small>
|
||||
<small>(<%= util.formatRelativeTime(post.lastFeatureTime) %>)</small>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
@ -181,8 +197,10 @@
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (_.any(post.relations)) { %>
|
||||
<div class="box">
|
||||
<h1>Related posts</h1>
|
||||
<ul class="related">
|
||||
<% _.each(post.relations, function(relatedPost) { %>
|
||||
@ -193,11 +211,11 @@
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (_.any(privileges) || _.any(editPrivileges) || post.contentType === 'image') { %>
|
||||
<div class="box">
|
||||
<h1>Options</h1>
|
||||
|
||||
<ul class="operations">
|
||||
<% if (_.any(editPrivileges)) { %>
|
||||
<li>
|
||||
@ -207,7 +225,7 @@
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (privileges.canAddPostNotes) { %>
|
||||
<% if (privileges.canAddPostNotes && (post.contentType === 'image' || post.contentType === 'animation')) { %>
|
||||
<li>
|
||||
<a class="add-note" href="#">
|
||||
Add new note
|
||||
@ -239,7 +257,7 @@
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (post.contentType === 'image') { %>
|
||||
<% if (post.contentType === 'image' || post.contentType === 'animation') { %>
|
||||
<li>
|
||||
<a href="http://iqdb.org/?url=<%= permaLink %>">
|
||||
Search on IQDB
|
||||
@ -252,9 +270,16 @@
|
||||
</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
<% } %>
|
||||
|
||||
<li class="fit-mode">
|
||||
Fit:
|
||||
<a data-fit-mode="fit-width" href="#">width</a>,
|
||||
<a data-fit-mode="fit-height" href="#">height</a>,
|
||||
<a data-fit-mode="fit-both" href="#">both</a>,
|
||||
<a data-fit-mode="original" href="#">original</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="post-view">
|
||||
@ -271,7 +296,7 @@
|
||||
<h1>History</h1>
|
||||
<%= historyTemplate({
|
||||
history: postHistory,
|
||||
formatRelativeTime: formatRelativeTime
|
||||
util: util,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
@ -41,7 +41,7 @@
|
||||
<div class="form-row">
|
||||
<label class="form-label" for="tag-category">Category:</label>
|
||||
<div class="form-input">
|
||||
<% _.each(_.extend({'default': 'default'}, _.object(tagCategories, tagCategories)), function(v, k) { %>
|
||||
<% _.each(_.extend({'default': 'default'}, tagCategories), function(v, k) { %>
|
||||
<input name="category" type="radio" value="<%= k %>" id="category-<%= k %>" <% print(tag.category === k ? 'checked="checked"' : '') %>>
|
||||
<label for="category-<%= k %>">
|
||||
<% print(tag.category === k ? v + ' (current)' : v) %>
|
||||
@ -103,7 +103,7 @@
|
||||
<h3>History</h3>
|
||||
<%= historyTemplate({
|
||||
history: tag.history,
|
||||
formatRelativeTime: formatRelativeTime
|
||||
util: util,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
@ -1,18 +1,29 @@
|
||||
<div class="user">
|
||||
<div class="avatar">
|
||||
<% if (canViewUsers) { %>
|
||||
<a href="#/user/<%= user.name %>">
|
||||
<% } %>
|
||||
<img width="80" height="80" src="/data/thumbnails/80x80/avatars/<%= user.name %>" alt="<%= user.name %>"/>
|
||||
<% if (canViewUsers) { %>
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<h1>
|
||||
<% if (canViewUsers) { %>
|
||||
<a href="#/user/<%= user.name %>">
|
||||
<%= user.name %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<%= user.name %>
|
||||
<% } %>
|
||||
</h1>
|
||||
<div class="date-joined" title="<%= user.registrationTime %>">
|
||||
Joined: <%= formatRelativeTime(user.registrationTime) %>
|
||||
<div class="date-joined" title="<%= util.formatAbsoluteTime(user.creationTime) %>">
|
||||
Joined: <%= util.formatRelativeTime(user.creationTime) %>
|
||||
</div>
|
||||
<div class="date-seen">
|
||||
Last seen: <%= formatRelativeTime(user.lastLoginTime) %>
|
||||
<div class="date-seen" title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
|
||||
Last seen: <%= util.formatRelativeTime(user.lastLoginTime) %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -7,10 +7,10 @@
|
||||
<a class="big-button" href="#/users/order=name,desc">Sort Z→A</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="big-button" href="#/users/order=registration_time,asc">Sort old→new</a>
|
||||
<a class="big-button" href="#/users/order=creation_time,asc">Sort old→new</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="big-button" href="#/users/order=registration_time,desc">Sort new→old</a>
|
||||
<a class="big-button" href="#/users/order=creation_time,desc">Sort new→old</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -51,12 +51,16 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>Registered:</td>
|
||||
<td><%= formatRelativeTime(user.registrationTime) %></td>
|
||||
<td title="<%= util.formatAbsoluteTime(user.creationTime) %>">
|
||||
<%= util.formatRelativeTime(user.creationTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Seen:</td>
|
||||
<td><%= formatRelativeTime(user.lastLoginTime) %></td>
|
||||
<td title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
|
||||
<%= util.formatRelativeTime(user.lastLoginTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<% if (user.accessRank) { %>
|
||||
|
1
scripts/cron-globals.php → scripts/cron-globals
Normal file → Executable file
1
scripts/cron-globals.php → scripts/cron-globals
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
43
scripts/cron-stats
Executable file
43
scripts/cron-stats
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'src'
|
||||
. DIRECTORY_SEPARATOR . 'Bootstrap.php');
|
||||
|
||||
// Why this exists:
|
||||
// 1. Some entities store a few basic stats in special columns for performance reasons. The benefit of such
|
||||
// denormalization is vast.
|
||||
// 2. The maintenance of the stats is implemented using triggers - when users tags a post, tag usage increases.
|
||||
// 3. This mostly works.
|
||||
// 4. Meanwhile, in order not to leave any orphans upon row deletions (e.g. have dangling postTags row after specific
|
||||
// post removal), the database schema uses foreign keys with CASCADE option. This option recursively removes
|
||||
// everything that would have missing references. This is good.
|
||||
// 5. Here's the thing: row removals caused by CASCADE foreign key checks don't execute triggers. So if user removes a
|
||||
// post, then although corresponding postTags entries will get deleted, ON postTags AFTER DELETE trigger will not
|
||||
// execute, leaving the tags with invalid usage count.
|
||||
//
|
||||
// There are three possible solutions to this problem:
|
||||
// 1. Implement all that logic in the appplication layer. I don't feel like doing this at all, it causes more havoc in
|
||||
// the code and possibly adds even more holes to the whole denormalization maintenance process.
|
||||
// 2. Convert CASCADE foreign checks to another set of triggers. This won't work for MySQL because of its limitations:
|
||||
// >Can't update table 'comments' in stored function/trigger because it is already used by statement which invoked
|
||||
// >this stored function/trigger
|
||||
// Creating complex triggers will result very quickly in this error message (I tested it on postTags and posts, it
|
||||
// did). I strongly believe the reason behind the error above is linked directly into the discussed MySQL's
|
||||
// limitation.
|
||||
// 3. Make a scripts like this. This is the easiest option out. The downside is that changes will be seen not
|
||||
// immediately, but except for heavy tag maintenance, I don't see where such a delay in stat synchronization might
|
||||
// really hurt.
|
||||
|
||||
use Szurubooru\DatabaseConnection;
|
||||
$databaseConnection = Szurubooru\Injector::get(DatabaseConnection::class);
|
||||
$pdo = $databaseConnection->getPDO();
|
||||
$pdo->exec('UPDATE tags SET usages = (SELECT COUNT(1) FROM postTags WHERE tagId = tags.id)');
|
||||
$pdo->exec('UPDATE posts SET tagCount = (SELECT COUNT(1) FROM postTags WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET score = (SELECT SUM(score) FROM scores WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET favCount = (SELECT COUNT(1) FROM favorites WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET lastFavTime = (SELECT MAX(time) FROM favorites WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET commentCount = (SELECT COUNT(1) FROM comments WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET lastCommentCreationTime = (SELECT MAX(creationTime) FROM comments WHERE postId = posts.id)');
|
||||
$pdo->exec('UPDATE posts SET lastCommentEditTime = (SELECT MAX(lastEditTime) FROM comments WHERE postId = posts.id)');
|
32
scripts/find-dead-posts
Executable file
32
scripts/find-dead-posts
Executable file
@ -0,0 +1,32 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'src'
|
||||
. DIRECTORY_SEPARATOR . 'Bootstrap.php');
|
||||
|
||||
use Szurubooru\Injector;
|
||||
use Szurubooru\Dao\PublicFileDao;
|
||||
use Szurubooru\Dao\PostDao;
|
||||
|
||||
$publicFileDao = Injector::get(PublicFileDao::class);
|
||||
$postDao = Injector::get(PostDao::class);
|
||||
|
||||
$paths = [];
|
||||
foreach ($postDao->findAll() as $post)
|
||||
{
|
||||
$paths[] = $post->getContentPath();
|
||||
$paths[] = $post->getThumbnailSourceContentPath();
|
||||
}
|
||||
|
||||
$paths = array_flip($paths);
|
||||
foreach ($publicFileDao->listAll() as $path)
|
||||
{
|
||||
if (dirname($path) !== 'posts')
|
||||
continue;
|
||||
if (!isset($paths[$path]))
|
||||
{
|
||||
echo $path . PHP_EOL;
|
||||
flush();
|
||||
}
|
||||
}
|
19
scripts/test-email
Executable file
19
scripts/test-email
Executable file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'src'
|
||||
. DIRECTORY_SEPARATOR . 'Bootstrap.php');
|
||||
|
||||
use Szurubooru\Injector;
|
||||
use Szurubooru\Services\EmailService;
|
||||
|
||||
if (!isset($argv[1]))
|
||||
{
|
||||
echo "No recipient email specified.";
|
||||
return;
|
||||
}
|
||||
$address = $argv[1];
|
||||
|
||||
$emailService = Injector::get(EmailService::class);
|
||||
$emailService->sendEmail($address, 'test', "test\nąćęłóńśźż\n←↑→↓");
|
1
scripts/thumbnails.php → scripts/thumbnails
Normal file → Executable file
1
scripts/thumbnails.php → scripts/thumbnails
Normal file → Executable file
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
35
scripts/upgrade
Executable file
35
scripts/upgrade
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'src'
|
||||
. DIRECTORY_SEPARATOR . 'Bootstrap.php');
|
||||
|
||||
$testMode = false;
|
||||
|
||||
if (isset($argv))
|
||||
{
|
||||
foreach ($argv as $arg)
|
||||
{
|
||||
if ($arg === '--test')
|
||||
$testMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($testMode)
|
||||
{
|
||||
$config = \Szurubooru\Injector::get(\Szurubooru\Config::class);
|
||||
$config->database->dsn = $config->database->tests->dsn;
|
||||
$config->database->user = $config->database->tests->user;
|
||||
$config->database->password = $config->database->tests->password;
|
||||
\Szurubooru\Injector::set(\Szurubooru\Config::class, $config);
|
||||
|
||||
$databaseConnection = \Szurubooru\Injector::get(\Szurubooru\DatabaseConnection::class);
|
||||
$pdo = $databaseConnection->getPDO();
|
||||
$pdo->exec('DROP DATABASE IF EXISTS szuru_test');
|
||||
$pdo->exec('CREATE DATABASE szuru_test');
|
||||
$pdo->exec('USE szuru_test');
|
||||
}
|
||||
|
||||
$upgradeService = \Szurubooru\Injector::get(\Szurubooru\Services\UpgradeService::class);
|
||||
$upgradeService->runUpgradesVerbose();
|
@ -1,34 +0,0 @@
|
||||
<?php
|
||||
require_once(__DIR__
|
||||
. DIRECTORY_SEPARATOR . '..'
|
||||
. DIRECTORY_SEPARATOR . 'src'
|
||||
. DIRECTORY_SEPARATOR . 'Bootstrap.php');
|
||||
|
||||
$testMode = false;
|
||||
|
||||
if (isset($argv))
|
||||
{
|
||||
foreach ($argv as $arg)
|
||||
{
|
||||
if ($arg === '--test')
|
||||
$testMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($testMode)
|
||||
{
|
||||
$config = \Szurubooru\Injector::get(\Szurubooru\Config::class);
|
||||
$config->database->dsn = $config->database->tests->dsn;
|
||||
$config->database->user = $config->database->tests->user;
|
||||
$config->database->password = $config->database->tests->password;
|
||||
\Szurubooru\Injector::set(\Szurubooru\Config::class, $config);
|
||||
|
||||
$databaseConnection = \Szurubooru\Injector::get(\Szurubooru\DatabaseConnection::class);
|
||||
$pdo = $databaseConnection->getPDO();
|
||||
$pdo->exec('DROP DATABASE szuru_test');
|
||||
$pdo->exec('CREATE DATABASE szuru_test');
|
||||
$pdo->exec('USE szuru_test');
|
||||
}
|
||||
|
||||
$upgradeService = \Szurubooru\Injector::get(\Szurubooru\Services\UpgradeService::class);
|
||||
$upgradeService->runUpgradesVerbose();
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru;
|
||||
|
||||
class ControllerRepository
|
||||
{
|
||||
private $controllers = [];
|
||||
|
||||
public function __construct(array $controllers)
|
||||
{
|
||||
$this->controllers = $controllers;
|
||||
}
|
||||
|
||||
public function getControllers()
|
||||
{
|
||||
return $this->controllers;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Router;
|
||||
|
||||
abstract class AbstractController
|
||||
{
|
||||
abstract function registerRoutes(Router $router);
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\TokenViewProxy;
|
||||
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
|
||||
use Szurubooru\FormData\LoginFormData;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\TokenService;
|
||||
use Szurubooru\Services\UserService;
|
||||
|
||||
final class AuthController extends AbstractController
|
||||
{
|
||||
private $authService;
|
||||
private $userService;
|
||||
private $tokenService;
|
||||
private $privilegeService;
|
||||
private $inputReader;
|
||||
private $userViewProxy;
|
||||
private $tokenViewProxy;
|
||||
|
||||
public function __construct(
|
||||
AuthService $authService,
|
||||
UserService $userService,
|
||||
TokenService $tokenService,
|
||||
PrivilegeService $privilegeService,
|
||||
InputReader $inputReader,
|
||||
UserViewProxy $userViewProxy,
|
||||
TokenViewProxy $tokenViewProxy)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
$this->userService = $userService;
|
||||
$this->tokenService = $tokenService;
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->inputReader = $inputReader;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
$this->tokenViewProxy = $tokenViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->post('/api/login', [$this, 'login']);
|
||||
$router->put('/api/login', [$this, 'login']);
|
||||
}
|
||||
|
||||
public function login()
|
||||
{
|
||||
if (isset($this->inputReader->userNameOrEmail) && isset($this->inputReader->password))
|
||||
{
|
||||
$formData = new LoginFormData($this->inputReader);
|
||||
$this->authService->loginFromCredentials($formData);
|
||||
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
$this->userService->updateUserLastLoginTime($user);
|
||||
}
|
||||
elseif (isset($this->inputReader->token))
|
||||
{
|
||||
$token = $this->tokenService->getByName($this->inputReader->token);
|
||||
$this->authService->loginFromToken($token);
|
||||
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
$isFromCookie = boolval($this->inputReader->isFromCookie);
|
||||
if ($isFromCookie)
|
||||
$this->userService->updateUserLastLoginTime($user);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->authService->loginAnonymous();
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
'token' => $this->tokenViewProxy->fromEntity($this->authService->getLoginToken()),
|
||||
'user' => $this->userViewProxy->fromEntity($user),
|
||||
'privileges' => $this->privilegeService->getCurrentPrivileges(),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,155 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\CommentViewProxy;
|
||||
use Szurubooru\Controllers\ViewProxies\PostViewProxy;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\SearchServices\Filters\CommentFilter;
|
||||
use Szurubooru\SearchServices\Filters\PostFilter;
|
||||
use Szurubooru\SearchServices\Requirements\Requirement;
|
||||
use Szurubooru\SearchServices\Requirements\RequirementRangedValue;
|
||||
use Szurubooru\SearchServices\Requirements\RequirementSingleValue;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\CommentService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
|
||||
final class CommentController extends AbstractController
|
||||
{
|
||||
private $privilegeService;
|
||||
private $authService;
|
||||
private $postService;
|
||||
private $commentService;
|
||||
private $commentViewProxy;
|
||||
private $postViewProxy;
|
||||
private $inputReader;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
AuthService $authService,
|
||||
PostService $postService,
|
||||
CommentService $commentService,
|
||||
CommentViewProxy $commentViewProxy,
|
||||
PostViewProxy $postViewProxy,
|
||||
InputReader $inputReader)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->authService = $authService;
|
||||
$this->postService = $postService;
|
||||
$this->commentService = $commentService;
|
||||
$this->commentViewProxy = $commentViewProxy;
|
||||
$this->postViewProxy = $postViewProxy;
|
||||
$this->inputReader = $inputReader;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/comments', [$this, 'getComments']);
|
||||
$router->get('/api/comments/:postNameOrId', [$this, 'getPostComments']);
|
||||
$router->post('/api/comments/:postNameOrId', [$this, 'addComment']);
|
||||
$router->put('/api/comments/:commentId', [$this, 'editComment']);
|
||||
$router->delete('/api/comments/:commentId', [$this, 'deleteComment']);
|
||||
}
|
||||
|
||||
public function getComments()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_COMMENTS);
|
||||
|
||||
$filter = new PostFilter();
|
||||
$filter->setPageSize(10);
|
||||
$filter->setPageNumber($this->inputReader->page);
|
||||
$filter->setOrder([
|
||||
PostFilter::ORDER_LAST_COMMENT_TIME =>
|
||||
PostFilter::ORDER_DESC]);
|
||||
|
||||
$this->postService->decorateFilterFromBrowsingSettings($filter);
|
||||
|
||||
$requirement = new Requirement();
|
||||
$requirement->setValue(new RequirementRangedValue());
|
||||
$requirement->getValue()->setMinValue(1);
|
||||
$requirement->setType(PostFilter::REQUIREMENT_COMMENT_COUNT);
|
||||
$filter->addRequirement($requirement);
|
||||
|
||||
$result = $this->postService->getFiltered($filter);
|
||||
$posts = $result->getEntities();
|
||||
|
||||
$data = [];
|
||||
foreach ($posts as $post)
|
||||
{
|
||||
$data[] = [
|
||||
'post' => $this->postViewProxy->fromEntity($post),
|
||||
'comments' => $this->commentViewProxy->fromArray(
|
||||
array_reverse($this->commentService->getByPost($post)),
|
||||
$this->getCommentsFetchConfig()),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
|
||||
public function getPostComments($postNameOrId)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_COMMENTS);
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
|
||||
$filter = new CommentFilter();
|
||||
$filter->setOrder([
|
||||
CommentFilter::ORDER_ID =>
|
||||
CommentFilter::ORDER_ASC]);
|
||||
|
||||
$requirement = new Requirement();
|
||||
$requirement->setValue(new RequirementSingleValue($post->getId()));
|
||||
$requirement->setType(CommentFilter::REQUIREMENT_POST_ID);
|
||||
$filter->addRequirement($requirement);
|
||||
|
||||
$result = $this->commentService->getFiltered($filter);
|
||||
$entities = $this->commentViewProxy->fromArray($result->getEntities(), $this->getCommentsFetchConfig());
|
||||
return ['data' => $entities];
|
||||
}
|
||||
|
||||
public function addComment($postNameOrId)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::ADD_COMMENTS);
|
||||
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$comment = $this->commentService->createComment($post, $this->inputReader->text);
|
||||
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
|
||||
}
|
||||
|
||||
public function editComment($commentId)
|
||||
{
|
||||
$comment = $this->commentService->getById($commentId);
|
||||
|
||||
$this->privilegeService->assertPrivilege(
|
||||
($comment->getUser() && $this->privilegeService->isLoggedIn($comment->getUser()))
|
||||
? Privilege::EDIT_OWN_COMMENTS
|
||||
: Privilege::EDIT_ALL_COMMENTS);
|
||||
|
||||
$comment = $this->commentService->updateComment($comment, $this->inputReader->text);
|
||||
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
|
||||
}
|
||||
|
||||
public function deleteComment($commentId)
|
||||
{
|
||||
$comment = $this->commentService->getById($commentId);
|
||||
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($comment->getUser())
|
||||
? Privilege::DELETE_OWN_COMMENTS
|
||||
: Privilege::DELETE_ALL_COMMENTS);
|
||||
|
||||
return $this->commentService->deleteComment($comment);
|
||||
}
|
||||
|
||||
private function getCommentsFetchConfig()
|
||||
{
|
||||
return
|
||||
[
|
||||
CommentViewProxy::FETCH_OWN_SCORE => true,
|
||||
];
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\FavoritesService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
|
||||
final class FavoritesController extends AbstractController
|
||||
{
|
||||
private $privilegeService;
|
||||
private $authService;
|
||||
private $postService;
|
||||
private $favoritesService;
|
||||
private $userViewProxy;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
AuthService $authService,
|
||||
PostService $postService,
|
||||
FavoritesService $favoritesService,
|
||||
UserViewProxy $userViewProxy)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->authService = $authService;
|
||||
$this->postService = $postService;
|
||||
$this->favoritesService = $favoritesService;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/posts/:postNameOrId/favorites', [$this, 'getFavoriteUsers']);
|
||||
$router->post('/api/posts/:postNameOrId/favorites', [$this, 'addFavorite']);
|
||||
$router->delete('/api/posts/:postNameOrId/favorites', [$this, 'deleteFavorite']);
|
||||
}
|
||||
|
||||
public function getFavoriteUsers($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$users = $this->favoritesService->getFavoriteUsers($post);
|
||||
return ['data' => $this->userViewProxy->fromArray($users)];
|
||||
}
|
||||
|
||||
public function addFavorite($postNameOrId)
|
||||
{
|
||||
$this->privilegeService->assertLoggedIn();
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$this->favoritesService->addFavorite($user, $post);
|
||||
return $this->getFavoriteUsers($postNameOrId);
|
||||
}
|
||||
|
||||
public function deleteFavorite($postNameOrId)
|
||||
{
|
||||
$this->privilegeService->assertLoggedIn();
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$this->favoritesService->deleteFavorite($user, $post);
|
||||
return $this->getFavoriteUsers($postNameOrId);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Dao\GlobalParamDao;
|
||||
use Szurubooru\Router;
|
||||
|
||||
final class GlobalParamController extends AbstractController
|
||||
{
|
||||
private $globalParamDao;
|
||||
|
||||
public function __construct(GlobalParamDao $globalParamDao)
|
||||
{
|
||||
$this->globalParamDao = $globalParamDao;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/globals', [$this, 'getGlobals']);
|
||||
}
|
||||
|
||||
public function getGlobals()
|
||||
{
|
||||
$globals = $this->globalParamDao->findAll();
|
||||
$return = [];
|
||||
foreach ($globals as $global)
|
||||
{
|
||||
$return[$global->getKey()] = $global->getValue();
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\SnapshotViewProxy;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\SearchServices\Parsers\SnapshotSearchParser;
|
||||
use Szurubooru\Services\HistoryService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
|
||||
final class HistoryController extends AbstractController
|
||||
{
|
||||
private $historyService;
|
||||
private $privilegeService;
|
||||
private $snapshotSearchParser;
|
||||
private $inputReader;
|
||||
private $snapshotViewProxy;
|
||||
|
||||
public function __construct(
|
||||
HistoryService $historyService,
|
||||
PrivilegeService $privilegeService,
|
||||
SnapshotSearchParser $snapshotSearchParser,
|
||||
InputReader $inputReader,
|
||||
SnapshotViewProxy $snapshotViewProxy)
|
||||
{
|
||||
$this->historyService = $historyService;
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->snapshotSearchParser = $snapshotSearchParser;
|
||||
$this->inputReader = $inputReader;
|
||||
$this->snapshotViewProxy = $snapshotViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/history', [$this, 'getFiltered']);
|
||||
}
|
||||
|
||||
public function getFiltered()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY);
|
||||
|
||||
$filter = $this->snapshotSearchParser->createFilterFromInputReader($this->inputReader);
|
||||
$filter->setPageSize(50);
|
||||
$result = $this->historyService->getFiltered($filter);
|
||||
$entities = $this->snapshotViewProxy->fromArray($result->getEntities());
|
||||
return [
|
||||
'data' => $entities,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Config;
|
||||
use Szurubooru\Dao\PublicFileDao;
|
||||
use Szurubooru\Helpers\MimeHelper;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\NetworkingService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PostThumbnailService;
|
||||
|
||||
final class PostContentController extends AbstractController
|
||||
{
|
||||
private $config;
|
||||
private $fileDao;
|
||||
private $postService;
|
||||
private $networkingService;
|
||||
private $postThumbnailService;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
PublicFileDao $fileDao,
|
||||
PostService $postService,
|
||||
NetworkingService $networkingService,
|
||||
PostThumbnailService $postThumbnailService)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->fileDao = $fileDao;
|
||||
$this->postService = $postService;
|
||||
$this->networkingService = $networkingService;
|
||||
$this->postThumbnailService = $postThumbnailService;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/posts/:postName/content', [$this, 'getPostContent']);
|
||||
$router->get('/api/posts/:postName/thumbnail/:size', [$this, 'getPostThumbnail']);
|
||||
}
|
||||
|
||||
public function getPostContent($postName)
|
||||
{
|
||||
$post = $this->postService->getByName($postName);
|
||||
|
||||
$customFileName = sprintf('%s_%s.%s',
|
||||
$this->config->basic->serviceName,
|
||||
$post->getName(),
|
||||
strtolower(MimeHelper::getExtension($post->getContentMimeType())));
|
||||
|
||||
$this->networkingService->serveFile($this->fileDao->getFullPath($post->getContentPath()), $customFileName);
|
||||
}
|
||||
|
||||
public function getPostThumbnail($postName, $size)
|
||||
{
|
||||
$post = $this->postService->getByName($postName);
|
||||
$thumbnailName = $this->postThumbnailService->generateIfNeeded($post, $size, $size);
|
||||
$this->networkingService->serveFile($this->fileDao->getFullPath($thumbnailName));
|
||||
}
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Config;
|
||||
use Szurubooru\Controllers\ViewProxies\PostViewProxy;
|
||||
use Szurubooru\Controllers\ViewProxies\SnapshotViewProxy;
|
||||
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
|
||||
use Szurubooru\Entities\Post;
|
||||
use Szurubooru\FormData\PostEditFormData;
|
||||
use Szurubooru\FormData\UploadFormData;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\SearchServices\Parsers\PostSearchParser;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\PostFeatureService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
|
||||
final class PostController extends AbstractController
|
||||
{
|
||||
private $config;
|
||||
private $authService;
|
||||
private $privilegeService;
|
||||
private $postService;
|
||||
private $postFeatureService;
|
||||
private $postSearchParser;
|
||||
private $inputReader;
|
||||
private $postViewProxy;
|
||||
private $snapshotViewProxy;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
AuthService $authService,
|
||||
PrivilegeService $privilegeService,
|
||||
PostService $postService,
|
||||
PostFeatureService $postFeatureService,
|
||||
PostSearchParser $postSearchParser,
|
||||
InputReader $inputReader,
|
||||
UserViewProxy $userViewProxy,
|
||||
PostViewProxy $postViewProxy,
|
||||
SnapshotViewProxy $snapshotViewProxy)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->authService = $authService;
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->postService = $postService;
|
||||
$this->postFeatureService = $postFeatureService;
|
||||
$this->postSearchParser = $postSearchParser;
|
||||
$this->inputReader = $inputReader;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
$this->postViewProxy = $postViewProxy;
|
||||
$this->snapshotViewProxy = $snapshotViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->post('/api/posts', [$this, 'createPost']);
|
||||
$router->get('/api/posts', [$this, 'getFiltered']);
|
||||
$router->get('/api/posts/featured', [$this, 'getFeatured']);
|
||||
$router->get('/api/posts/:postNameOrId', [$this, 'getByNameOrId']);
|
||||
$router->get('/api/posts/:postNameOrId/history', [$this, 'getHistory']);
|
||||
$router->put('/api/posts/:postNameOrId', [$this, 'updatePost']);
|
||||
$router->delete('/api/posts/:postNameOrId', [$this, 'deletePost']);
|
||||
$router->post('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
|
||||
$router->put('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
|
||||
}
|
||||
|
||||
public function getFeatured()
|
||||
{
|
||||
$post = $this->postFeatureService->getFeaturedPost();
|
||||
$user = $this->postFeatureService->getFeaturedPostUser();
|
||||
return [
|
||||
'user' => $this->userViewProxy->fromEntity($user),
|
||||
'post' => $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()),
|
||||
];
|
||||
}
|
||||
|
||||
public function getByNameOrId($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
|
||||
}
|
||||
|
||||
public function getHistory($postNameOrId)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY);
|
||||
$post = $this->getByNameOrId($postNameOrId);
|
||||
return ['data' => $this->snapshotViewProxy->fromArray($this->postService->getHistory($post))];
|
||||
}
|
||||
|
||||
public function getFiltered()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_POSTS);
|
||||
|
||||
$filter = $this->postSearchParser->createFilterFromInputReader($this->inputReader);
|
||||
$filter->setPageSize($this->config->posts->postsPerPage);
|
||||
$this->postService->decorateFilterFromBrowsingSettings($filter);
|
||||
|
||||
$result = $this->postService->getFiltered($filter);
|
||||
$entities = $this->postViewProxy->fromArray($result->getEntities(), $this->getLightFetchConfig());
|
||||
return [
|
||||
'data' => $entities,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
|
||||
public function createPost()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS);
|
||||
$formData = new UploadFormData($this->inputReader);
|
||||
|
||||
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS);
|
||||
|
||||
if ($formData->anonymous)
|
||||
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS_ANONYMOUSLY);
|
||||
|
||||
$post = $this->postService->createPost($formData);
|
||||
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
|
||||
}
|
||||
|
||||
public function updatePost($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$formData = new PostEditFormData($this->inputReader);
|
||||
|
||||
if ($formData->content !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_CONTENT);
|
||||
|
||||
if ($formData->thumbnail !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_THUMBNAIL);
|
||||
|
||||
if ($formData->safety !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_SAFETY);
|
||||
|
||||
if ($formData->source !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_SOURCE);
|
||||
|
||||
if ($formData->tags !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_TAGS);
|
||||
|
||||
$this->postService->updatePost($post, $formData);
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
|
||||
}
|
||||
|
||||
public function deletePost($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$this->postService->deletePost($post);
|
||||
}
|
||||
|
||||
public function featurePost($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$this->postFeatureService->featurePost($post);
|
||||
}
|
||||
|
||||
private function getFullFetchConfig()
|
||||
{
|
||||
return
|
||||
[
|
||||
PostViewProxy::FETCH_RELATIONS => true,
|
||||
PostViewProxy::FETCH_TAGS => true,
|
||||
PostViewProxy::FETCH_USER => true,
|
||||
PostViewProxy::FETCH_HISTORY => true,
|
||||
PostViewProxy::FETCH_OWN_SCORE => true,
|
||||
PostViewProxy::FETCH_FAVORITES => true,
|
||||
PostViewProxy::FETCH_NOTES => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function getLightFetchConfig()
|
||||
{
|
||||
return
|
||||
[
|
||||
PostViewProxy::FETCH_TAGS => true,
|
||||
];
|
||||
}
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\PostNoteViewProxy;
|
||||
use Szurubooru\FormData\PostNoteFormData;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\PostNotesService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
|
||||
final class PostNotesController extends AbstractController
|
||||
{
|
||||
private $inputReader;
|
||||
private $postService;
|
||||
private $postNotesService;
|
||||
private $privilegeService;
|
||||
private $postNoteViewProxy;
|
||||
|
||||
public function __construct(
|
||||
InputReader $inputReader,
|
||||
PostService $postService,
|
||||
PostNotesService $postNotesService,
|
||||
PrivilegeService $privilegeService,
|
||||
PostNoteViewProxy $postNoteViewProxy)
|
||||
{
|
||||
$this->inputReader = $inputReader;
|
||||
$this->postService = $postService;
|
||||
$this->postNotesService = $postNotesService;
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->postNoteViewProxy = $postNoteViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/notes/:postNameOrId', [$this, 'getPostNotes']);
|
||||
$router->post('/api/notes/:postNameOrId', [$this, 'addPostNote']);
|
||||
$router->put('/api/notes/:postNoteId', [$this, 'editPostNote']);
|
||||
$router->delete('/api/notes/:postNoteId', [$this, 'deletePostNote']);
|
||||
}
|
||||
|
||||
public function getPostNotes($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
$postNotes = $this->postNotesService->getByPost($post);
|
||||
return $this->postNoteViewProxy->fromArray($postNotes);
|
||||
}
|
||||
|
||||
public function addPostNote($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
|
||||
$this->privilegeService->assertPrivilege(Privilege::ADD_POST_NOTES);
|
||||
|
||||
$formData = new PostNoteFormData($this->inputReader);
|
||||
$postNote = $this->postNotesService->createPostNote($post, $formData);
|
||||
return $this->postNoteViewProxy->fromEntity($postNote);
|
||||
}
|
||||
|
||||
public function editPostNote($postNoteId)
|
||||
{
|
||||
$postNote = $this->postNotesService->getById($postNoteId);
|
||||
|
||||
$this->privilegeService->assertPrivilege(Privilege::EDIT_POST_NOTES);
|
||||
|
||||
$formData = new PostNoteFormData($this->inputReader);
|
||||
$postNote = $this->postNotesService->updatePostNote($postNote, $formData);
|
||||
return $this->postNoteViewProxy->fromEntity($postNote);
|
||||
}
|
||||
|
||||
public function deletePostNote($postNoteId)
|
||||
{
|
||||
$postNote = $this->postNotesService->getById($postNoteId);
|
||||
$this->privilegeService->assertPrivilege(Privilege::DELETE_POST_NOTES);
|
||||
return $this->postNotesService->deletePostNote($postNote);
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Entities\Entity;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\CommentService;
|
||||
use Szurubooru\Services\PostService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\ScoreService;
|
||||
|
||||
final class ScoreController extends AbstractController
|
||||
{
|
||||
private $privilegeService;
|
||||
private $authService;
|
||||
private $postService;
|
||||
private $commentService;
|
||||
private $scoreService;
|
||||
private $inputReader;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
AuthService $authService,
|
||||
PostService $postService,
|
||||
CommentService $commentService,
|
||||
ScoreService $scoreService,
|
||||
InputReader $inputReader)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->authService = $authService;
|
||||
$this->postService = $postService;
|
||||
$this->commentService = $commentService;
|
||||
$this->scoreService = $scoreService;
|
||||
$this->inputReader = $inputReader;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/posts/:postNameOrId/score', [$this, 'getPostScore']);
|
||||
$router->post('/api/posts/:postNameOrId/score', [$this, 'setPostScore']);
|
||||
$router->get('/api/comments/:commentId/score', [$this, 'getCommentScore']);
|
||||
$router->post('/api/comments/:commentId/score', [$this, 'setCommentScore']);
|
||||
}
|
||||
|
||||
public function getPostScore($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
return $this->getScore($post);
|
||||
}
|
||||
|
||||
public function setPostScore($postNameOrId)
|
||||
{
|
||||
$post = $this->postService->getByNameOrId($postNameOrId);
|
||||
return $this->setScore($post);
|
||||
}
|
||||
|
||||
public function getCommentScore($commentId)
|
||||
{
|
||||
$comment = $this->commentService->getById($commentId);
|
||||
return $this->getScore($comment);
|
||||
}
|
||||
|
||||
public function setCommentScore($commentId)
|
||||
{
|
||||
$comment = $this->commentService->getById($commentId);
|
||||
return $this->setScore($comment);
|
||||
}
|
||||
|
||||
private function setScore(Entity $entity)
|
||||
{
|
||||
$this->privilegeService->assertLoggedIn();
|
||||
$score = intval($this->inputReader->score);
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
$result = $this->scoreService->setUserScore($user, $entity, $score);
|
||||
return [
|
||||
'score' => $this->scoreService->getScoreValue($entity),
|
||||
'ownScore' => $result->getScore(),
|
||||
];
|
||||
}
|
||||
|
||||
private function getScore(Entity $entity)
|
||||
{
|
||||
$user = $this->authService->getLoggedInUser();
|
||||
return [
|
||||
'score' => $this->scoreService->getScoreValue($entity),
|
||||
'ownScore' => $this->scoreService->getUserScoreValue($user, $entity),
|
||||
];
|
||||
}
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Controllers\ViewProxies\TagViewProxy;
|
||||
use Szurubooru\FormData\TagEditFormData;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\SearchServices\Parsers\TagSearchParser;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\TagService;
|
||||
|
||||
final class TagController extends AbstractController
|
||||
{
|
||||
private $privilegeService;
|
||||
private $tagService;
|
||||
private $tagViewProxy;
|
||||
private $tagSearchParser;
|
||||
private $inputReader;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
TagService $tagService,
|
||||
TagViewProxy $tagViewProxy,
|
||||
TagSearchParser $tagSearchParser,
|
||||
InputReader $inputReader)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->tagService = $tagService;
|
||||
$this->tagViewProxy = $tagViewProxy;
|
||||
$this->tagSearchParser = $tagSearchParser;
|
||||
$this->inputReader = $inputReader;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/tags', [$this, 'getTags']);
|
||||
$router->get('/api/tags/:tagName', [$this, 'getTag']);
|
||||
$router->get('/api/tags/:tagName/siblings', [$this, 'getTagSiblings']);
|
||||
$router->put('/api/tags/:tagName', [$this, 'updateTag']);
|
||||
$router->put('/api/tags/:tagName/merge', [$this, 'mergeTag']);
|
||||
$router->delete('/api/tags/:tagName', [$this, 'deleteTag']);
|
||||
}
|
||||
|
||||
public function getTag($tagName)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
|
||||
|
||||
$tag = $this->tagService->getByName($tagName);
|
||||
return $this->tagViewProxy->fromEntity($tag, $this->getFullFetchConfig());
|
||||
}
|
||||
|
||||
public function getTags()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
|
||||
|
||||
$filter = $this->tagSearchParser->createFilterFromInputReader($this->inputReader);
|
||||
$filter->setPageSize(50);
|
||||
|
||||
$result = $this->tagService->getFiltered($filter);
|
||||
$entities = $this->tagViewProxy->fromArray($result->getEntities(), $this->getFullFetchConfig());
|
||||
return [
|
||||
'data' => $entities,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
|
||||
public function getTagSiblings($tagName)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
|
||||
$tag = $this->tagService->getByName($tagName);
|
||||
$result = $this->tagService->getSiblings($tagName);
|
||||
$entities = $this->tagViewProxy->fromArray($result);
|
||||
return [
|
||||
'data' => $entities,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateTag($tagName)
|
||||
{
|
||||
$tag = $this->tagService->getByName($tagName);
|
||||
$formData = new TagEditFormData($this->inputReader);
|
||||
|
||||
if ($formData->name !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_NAME);
|
||||
|
||||
if ($formData->category !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_CATEGORY);
|
||||
|
||||
if ($formData->banned !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::BAN_TAGS);
|
||||
|
||||
if ($formData->implications !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_IMPLICATIONS);
|
||||
|
||||
if ($formData->suggestions !== null)
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_SUGGESTIONS);
|
||||
|
||||
$tag = $this->tagService->updateTag($tag, $formData);
|
||||
return $this->tagViewProxy->fromEntity($tag, $this->getFullFetchConfig());
|
||||
}
|
||||
|
||||
public function deleteTag($tagName)
|
||||
{
|
||||
$tag = $this->tagService->getByName($tagName);
|
||||
$this->privilegeService->assertPrivilege(Privilege::DELETE_TAGS);
|
||||
return $this->tagService->deleteTag($tag);
|
||||
}
|
||||
|
||||
public function mergeTag($tagName)
|
||||
{
|
||||
$targetTagName = $this->inputReader->targetTag;
|
||||
$sourceTag = $this->tagService->getByName($tagName);
|
||||
$targetTag = $this->tagService->getByName($targetTagName);
|
||||
$this->privilegeService->assertPrivilege(Privilege::MERGE_TAGS);
|
||||
return $this->tagService->mergeTag($sourceTag, $targetTag);
|
||||
}
|
||||
|
||||
private function getFullFetchConfig()
|
||||
{
|
||||
return
|
||||
[
|
||||
TagViewProxy::FETCH_IMPLICATIONS => true,
|
||||
TagViewProxy::FETCH_SUGGESTIONS => true,
|
||||
TagViewProxy::FETCH_HISTORY => true,
|
||||
];
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Dao\PublicFileDao;
|
||||
use Szurubooru\Entities\User;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\Services\NetworkingService;
|
||||
use Szurubooru\Services\ThumbnailService;
|
||||
use Szurubooru\Services\UserService;
|
||||
|
||||
final class UserAvatarController extends AbstractController
|
||||
{
|
||||
private $fileDao;
|
||||
private $userService;
|
||||
private $networkingService;
|
||||
private $thumbnailService;
|
||||
|
||||
public function __construct(
|
||||
PublicFileDao $fileDao,
|
||||
UserService $userService,
|
||||
NetworkingService $networkingService,
|
||||
ThumbnailService $thumbnailService)
|
||||
{
|
||||
$this->fileDao = $fileDao;
|
||||
$this->userService = $userService;
|
||||
$this->networkingService = $networkingService;
|
||||
$this->thumbnailService = $thumbnailService;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->get('/api/users/:userName/avatar/:size', [$this, 'getAvatarByName']);
|
||||
}
|
||||
|
||||
public function getAvatarByName($userName, $size)
|
||||
{
|
||||
try
|
||||
{
|
||||
$user = $this->userService->getByName($userName);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
$this->serveBlankFile($size);
|
||||
}
|
||||
|
||||
switch ($user->getAvatarStyle())
|
||||
{
|
||||
case User::AVATAR_STYLE_GRAVATAR:
|
||||
$hash = md5(strtolower(trim($user->getEmail() ? $user->getEmail() : $user->getId() . $user->getName())));
|
||||
$url = 'https://www.gravatar.com/avatar/' . $hash . '?d=retro&s=' . $size;
|
||||
$this->serveFromUrl($url);
|
||||
break;
|
||||
|
||||
case User::AVATAR_STYLE_BLANK:
|
||||
$this->serveBlankFile($size);
|
||||
break;
|
||||
|
||||
case User::AVATAR_STYLE_MANUAL:
|
||||
$this->serveFromFile($user->getCustomAvatarSourceContentPath(), $size);
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->serveBlankFile($size);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private function serveFromUrl($url)
|
||||
{
|
||||
$this->networkingService->redirect($url);
|
||||
}
|
||||
|
||||
private function serveFromFile($sourceName, $size)
|
||||
{
|
||||
$thumbnailName = $this->thumbnailService->generateIfNeeded($sourceName, $size, $size);
|
||||
$this->networkingService->serveFile($this->fileDao->getFullPath($thumbnailName));
|
||||
}
|
||||
|
||||
private function serveBlankFile($size)
|
||||
{
|
||||
$this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size);
|
||||
}
|
||||
|
||||
private function getBlankAvatarSourceContentPath()
|
||||
{
|
||||
return 'avatars/blank.png';
|
||||
}
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers;
|
||||
use Szurubooru\Config;
|
||||
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
|
||||
use Szurubooru\FormData\RegistrationFormData;
|
||||
use Szurubooru\FormData\UserEditFormData;
|
||||
use Szurubooru\Helpers\InputReader;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Router;
|
||||
use Szurubooru\SearchServices\Parsers\UserSearchParser;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\TokenService;
|
||||
use Szurubooru\Services\UserService;
|
||||
|
||||
final class UserController extends AbstractController
|
||||
{
|
||||
private $config;
|
||||
private $privilegeService;
|
||||
private $userService;
|
||||
private $tokenService;
|
||||
private $userSearchParser;
|
||||
private $inputReader;
|
||||
private $userViewProxy;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
PrivilegeService $privilegeService,
|
||||
UserService $userService,
|
||||
TokenService $tokenService,
|
||||
UserSearchParser $userSearchParser,
|
||||
InputReader $inputReader,
|
||||
UserViewProxy $userViewProxy)
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->userService = $userService;
|
||||
$this->tokenService = $tokenService;
|
||||
$this->userSearchParser = $userSearchParser;
|
||||
$this->inputReader = $inputReader;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
}
|
||||
|
||||
public function registerRoutes(Router $router)
|
||||
{
|
||||
$router->post('/api/users', [$this, 'createUser']);
|
||||
$router->get('/api/users', [$this, 'getFiltered']);
|
||||
$router->get('/api/users/:userNameOrEmail', [$this, 'getByNameOrEmail']);
|
||||
$router->put('/api/users/:userNameOrEmail', [$this, 'updateUser']);
|
||||
$router->delete('/api/users/:userNameOrEmail', [$this, 'deleteUser']);
|
||||
$router->post('/api/password-reset/:userNameOrEmail', [$this, 'passwordReset']);
|
||||
$router->post('/api/finish-password-reset/:tokenName', [$this, 'finishPasswordReset']);
|
||||
$router->post('/api/activation/:userNameOrEmail', [$this, 'activation']);
|
||||
$router->post('/api/finish-activation/:tokenName', [$this, 'finishActivation']);
|
||||
}
|
||||
|
||||
public function getByNameOrEmail($userNameOrEmail)
|
||||
{
|
||||
if (!$this->privilegeService->isLoggedIn($userNameOrEmail))
|
||||
$this->privilegeService->assertPrivilege(Privilege::VIEW_USERS);
|
||||
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
|
||||
return $this->userViewProxy->fromEntity($user);
|
||||
}
|
||||
|
||||
public function getFiltered()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::LIST_USERS);
|
||||
|
||||
$filter = $this->userSearchParser->createFilterFromInputReader($this->inputReader);
|
||||
$filter->setPageSize($this->config->users->usersPerPage);
|
||||
$result = $this->userService->getFiltered($filter);
|
||||
$entities = $this->userViewProxy->fromArray($result->getEntities());
|
||||
return [
|
||||
'data' => $entities,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
|
||||
public function createUser()
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::REGISTER);
|
||||
$formData = new RegistrationFormData($this->inputReader);
|
||||
$user = $this->userService->createUser($formData);
|
||||
return $this->userViewProxy->fromEntity($user);
|
||||
}
|
||||
|
||||
public function updateUser($userNameOrEmail)
|
||||
{
|
||||
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
|
||||
$formData = new UserEditFormData($this->inputReader);
|
||||
|
||||
if ($formData->avatarStyle !== null || $formData->avatarContent !== null)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($userNameOrEmail)
|
||||
? Privilege::CHANGE_OWN_AVATAR_STYLE
|
||||
: Privilege::CHANGE_ALL_AVATAR_STYLES);
|
||||
}
|
||||
|
||||
if ($formData->userName !== null)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($userNameOrEmail)
|
||||
? Privilege::CHANGE_OWN_NAME
|
||||
: Privilege::CHANGE_ALL_NAMES);
|
||||
}
|
||||
|
||||
if ($formData->password !== null)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($userNameOrEmail)
|
||||
? Privilege::CHANGE_OWN_PASSWORD
|
||||
: Privilege::CHANGE_ALL_PASSWORDS);
|
||||
}
|
||||
|
||||
if ($formData->email !== null)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($userNameOrEmail)
|
||||
? Privilege::CHANGE_OWN_EMAIL_ADDRESS
|
||||
: Privilege::CHANGE_ALL_EMAIL_ADDRESSES);
|
||||
}
|
||||
|
||||
if ($formData->accessRank)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::CHANGE_ACCESS_RANK);
|
||||
}
|
||||
|
||||
if ($formData->browsingSettings)
|
||||
{
|
||||
$this->privilegeService->assertLoggedIn($userNameOrEmail);
|
||||
}
|
||||
|
||||
if ($formData->banned !== null)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(Privilege::BAN_USERS);
|
||||
}
|
||||
|
||||
$user = $this->userService->updateUser($user, $formData);
|
||||
return $this->userViewProxy->fromEntity($user);
|
||||
}
|
||||
|
||||
public function deleteUser($userNameOrEmail)
|
||||
{
|
||||
$this->privilegeService->assertPrivilege(
|
||||
$this->privilegeService->isLoggedIn($userNameOrEmail)
|
||||
? Privilege::DELETE_OWN_ACCOUNT
|
||||
: Privilege::DELETE_ACCOUNTS);
|
||||
|
||||
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
|
||||
return $this->userService->deleteUser($user);
|
||||
}
|
||||
|
||||
public function passwordReset($userNameOrEmail)
|
||||
{
|
||||
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
|
||||
return $this->userService->sendPasswordResetEmail($user);
|
||||
}
|
||||
|
||||
public function activation($userNameOrEmail)
|
||||
{
|
||||
$user = $this->userService->getByNameOrEmail($userNameOrEmail, true);
|
||||
return $this->userService->sendActivationEmail($user);
|
||||
}
|
||||
|
||||
public function finishPasswordReset($tokenName)
|
||||
{
|
||||
$token = $this->tokenService->getByName($tokenName);
|
||||
return ['newPassword' => $this->userService->finishPasswordReset($token)];
|
||||
}
|
||||
|
||||
public function finishActivation($tokenName)
|
||||
{
|
||||
$token = $this->tokenService->getByName($tokenName);
|
||||
$this->userService->finishActivation($token);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
|
||||
abstract class AbstractViewProxy
|
||||
{
|
||||
public abstract function fromEntity($entity, $config = []);
|
||||
|
||||
public function fromArray($entities, $config = [])
|
||||
{
|
||||
return array_values(array_map(
|
||||
function($entity) use ($config)
|
||||
{
|
||||
return static::fromEntity($entity, $config);
|
||||
},
|
||||
$entities));
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\ScoreService;
|
||||
|
||||
class CommentViewProxy extends AbstractViewProxy
|
||||
{
|
||||
private $authService;
|
||||
private $scoreService;
|
||||
private $userViewProxy;
|
||||
|
||||
const FETCH_OWN_SCORE = 'fetchOwnScore';
|
||||
|
||||
public function __construct(
|
||||
AuthService $authService,
|
||||
ScoreService $scoreService,
|
||||
UserViewProxy $userViewProxy)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
$this->scoreService = $scoreService;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
}
|
||||
|
||||
public function fromEntity($comment, $config = [])
|
||||
{
|
||||
$result = new \StdClass;
|
||||
if ($comment)
|
||||
{
|
||||
$result->id = $comment->getId();
|
||||
$result->creationTime = $comment->getCreationTime();
|
||||
$result->lastEditTime = $comment->getLastEditTime();
|
||||
$result->text = $comment->getText();
|
||||
$result->postId = $comment->getPostId();
|
||||
$result->user = $this->userViewProxy->fromEntity($comment->getUser());
|
||||
$result->score = $comment->getScore();
|
||||
|
||||
if (!empty($config[self::FETCH_OWN_SCORE]) && $this->authService->isLoggedIn())
|
||||
$result->ownScore = $this->scoreService->getUserScoreValue($this->authService->getLoggedInUser(), $comment);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
|
||||
class PostNoteViewProxy extends AbstractViewProxy
|
||||
{
|
||||
public function fromEntity($postNote, $config = [])
|
||||
{
|
||||
$result = new \StdClass;
|
||||
if ($postNote)
|
||||
{
|
||||
$result->id = $postNote->getId();
|
||||
$result->postId = $postNote->getPostId();
|
||||
$result->text = $postNote->getText();
|
||||
$result->left = $postNote->getLeft();
|
||||
$result->top = $postNote->getTop();
|
||||
$result->width = $postNote->getWidth();
|
||||
$result->height = $postNote->getHeight();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
use Szurubooru\Entities\Post;
|
||||
use Szurubooru\Helpers\EnumHelper;
|
||||
use Szurubooru\Helpers\MimeHelper;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Services\AuthService;
|
||||
use Szurubooru\Services\FavoritesService;
|
||||
use Szurubooru\Services\PostHistoryService;
|
||||
use Szurubooru\Services\PostNotesService;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\ScoreService;
|
||||
|
||||
class PostViewProxy extends AbstractViewProxy
|
||||
{
|
||||
const FETCH_USER = 'fetchUser';
|
||||
const FETCH_TAGS = 'fetchTags';
|
||||
const FETCH_RELATIONS = 'fetchRelations';
|
||||
const FETCH_HISTORY = 'fetchHistory';
|
||||
const FETCH_OWN_SCORE = 'fetchOwnScore';
|
||||
const FETCH_FAVORITES = 'fetchFavorites';
|
||||
const FETCH_NOTES = 'fetchNotes';
|
||||
|
||||
private $privilegeService;
|
||||
private $authService;
|
||||
private $postHistoryService;
|
||||
private $favoritesService;
|
||||
private $scoreService;
|
||||
private $postNotesService;
|
||||
private $tagViewProxy;
|
||||
private $userViewProxy;
|
||||
private $snapshotViewProxy;
|
||||
private $postNoteViewProxy;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
AuthService $authService,
|
||||
PostHistoryService $postHistoryService,
|
||||
FavoritesService $favoritesService,
|
||||
ScoreService $scoreService,
|
||||
PostNotesService $postNotesService,
|
||||
TagViewProxy $tagViewProxy,
|
||||
UserViewProxy $userViewProxy,
|
||||
SnapshotViewProxy $snapshotViewProxy,
|
||||
PostNoteViewProxy $postNoteViewProxy)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->authService = $authService;
|
||||
$this->postHistoryService = $postHistoryService;
|
||||
$this->favoritesService = $favoritesService;
|
||||
$this->scoreService = $scoreService;
|
||||
$this->postNotesService = $postNotesService;
|
||||
$this->tagViewProxy = $tagViewProxy;
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
$this->snapshotViewProxy = $snapshotViewProxy;
|
||||
$this->postNoteViewProxy = $postNoteViewProxy;
|
||||
}
|
||||
|
||||
public function fromEntity($post, $config = [])
|
||||
{
|
||||
$result = new \StdClass;
|
||||
if (!$post)
|
||||
return $result;
|
||||
|
||||
$result->id = $post->getId();
|
||||
$result->idMarkdown = $post->getIdMarkdown();
|
||||
$result->name = $post->getName();
|
||||
$result->uploadTime = $post->getUploadTime();
|
||||
$result->lastEditTime = $post->getLastEditTime();
|
||||
$result->safety = EnumHelper::postSafetyToString($post->getSafety());
|
||||
$result->contentType = EnumHelper::postTypeToString($post->getContentType());
|
||||
$result->contentChecksum = $post->getContentChecksum();
|
||||
$result->contentMimeType = $post->getContentMimeType();
|
||||
$result->contentExtension = MimeHelper::getExtension($post->getContentMimeType());
|
||||
$result->source = $post->getSource();
|
||||
$result->imageWidth = $post->getImageWidth();
|
||||
$result->imageHeight = $post->getImageHeight();
|
||||
$result->featureCount = $post->getFeatureCount();
|
||||
$result->lastFeatureTime = $post->getLastFeatureTime();
|
||||
$result->originalFileSize = $post->getOriginalFileSize();
|
||||
$result->favoriteCount = $post->getFavoriteCount();
|
||||
$result->score = $post->getScore();
|
||||
$result->commentCount = $post->getCommentCount();
|
||||
$result->flags = new \StdClass;
|
||||
$result->flags->loop = ($post->getFlags() & Post::FLAG_LOOP);
|
||||
|
||||
if (!empty($config[self::FETCH_TAGS]))
|
||||
{
|
||||
$result->tags = $this->tagViewProxy->fromArray($post->getTags());
|
||||
usort($result->tags, function($tag1, $tag2)
|
||||
{
|
||||
return strcasecmp($tag1->name, $tag2->name);
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($config[self::FETCH_USER]))
|
||||
$result->user = $this->userViewProxy->fromEntity($post->getUser());
|
||||
|
||||
if (!empty($config[self::FETCH_RELATIONS]))
|
||||
$result->relations = $this->fromArray($post->getRelatedPosts());
|
||||
|
||||
if (!empty($config[self::FETCH_HISTORY]))
|
||||
{
|
||||
if ($this->privilegeService->hasPrivilege(Privilege::VIEW_HISTORY))
|
||||
$result->history = $this->snapshotViewProxy->fromArray($this->postHistoryService->getPostHistory($post));
|
||||
else
|
||||
$result->history = [];
|
||||
}
|
||||
|
||||
if (!empty($config[self::FETCH_OWN_SCORE]) && $this->authService->isLoggedIn())
|
||||
$result->ownScore = $this->scoreService->getUserScoreValue($this->authService->getLoggedInUser(), $post);
|
||||
|
||||
if (!empty($config[self::FETCH_FAVORITES]))
|
||||
$result->favorites = $this->userViewProxy->fromArray($this->favoritesService->getFavoriteUsers($post));
|
||||
|
||||
if (!empty($config[self::FETCH_NOTES]))
|
||||
$result->notes = $this->postNoteViewProxy->fromArray($this->postNotesService->getByPost($post));
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
|
||||
class SnapshotViewProxy extends AbstractViewProxy
|
||||
{
|
||||
private $userViewProxy;
|
||||
|
||||
public function __construct(UserViewProxy $userViewProxy)
|
||||
{
|
||||
$this->userViewProxy = $userViewProxy;
|
||||
}
|
||||
|
||||
public function fromEntity($snapshot, $config = [])
|
||||
{
|
||||
$result = new \StdClass;
|
||||
if ($snapshot)
|
||||
{
|
||||
$result->time = $snapshot->getTime();
|
||||
$result->type = $snapshot->getType();
|
||||
$result->primaryKey = $snapshot->getPrimaryKey();
|
||||
$result->operation = $snapshot->getOperation();
|
||||
$result->user = $this->userViewProxy->fromEntity($snapshot->getUser());
|
||||
$result->data = $snapshot->getData();
|
||||
$result->dataDifference = $snapshot->getDataDifference();
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<?php
|
||||
namespace Szurubooru\Controllers\ViewProxies;
|
||||
use Szurubooru\Privilege;
|
||||
use Szurubooru\Services\PrivilegeService;
|
||||
use Szurubooru\Services\TagHistoryService;
|
||||
|
||||
class TagViewProxy extends AbstractViewProxy
|
||||
{
|
||||
const FETCH_IMPLICATIONS = 'fetchImplications';
|
||||
const FETCH_SUGGESTIONS = 'fetchSuggestions';
|
||||
const FETCH_HISTORY = 'fetchHistory';
|
||||
|
||||
private $privilegeService;
|
||||
private $tagHistoryService;
|
||||
private $snapshotViewProxy;
|
||||
|
||||
public function __construct(
|
||||
PrivilegeService $privilegeService,
|
||||
TagHistoryService $tagHistoryService,
|
||||
SnapshotViewProxy $snapshotViewProxy)
|
||||
{
|
||||
$this->privilegeService = $privilegeService;
|
||||
$this->tagHistoryService = $tagHistoryService;
|
||||
$this->snapshotViewProxy = $snapshotViewProxy;
|
||||
}
|
||||
|
||||
public function fromEntity($tag, $config = [])
|
||||
{
|
||||
$result = new \StdClass;
|
||||
if ($tag)
|
||||
{
|
||||
$result->name = $tag->getName();
|
||||
$result->usages = $tag->getUsages();
|
||||
$result->banned = $tag->isBanned();
|
||||
$result->category = $tag->getCategory();
|
||||
|
||||
if (!empty($config[self::FETCH_IMPLICATIONS]))
|
||||
$result->implications = $this->fromArray($tag->getImpliedTags());
|
||||
|
||||
if (!empty($config[self::FETCH_SUGGESTIONS]))
|
||||
$result->suggestions = $this->fromArray($tag->getSuggestedTags());
|
||||
|
||||
if (!empty($config[self::FETCH_HISTORY]))
|
||||
{
|
||||
$result->history = $this->privilegeService->hasPrivilege(Privilege::VIEW_HISTORY)
|
||||
? $this->snapshotViewProxy->fromArray($this->tagHistoryService->getTagHistory($tag))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user