mirror of
https://github.com/rr-/szurubooru.git
synced 2025-07-17 08:26:24 +00:00
Compare commits
158 Commits
Author | SHA1 | Date | |
---|---|---|---|
31f336d690 | |||
0e4365795b | |||
96769f52cf | |||
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 |
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,11 +27,11 @@ h2 {
|
||||
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 13px;
|
||||
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,10 +10,11 @@
|
||||
}
|
||||
|
||||
#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;
|
||||
|
@ -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;
|
||||
@ -69,8 +75,8 @@
|
||||
position: relative;
|
||||
}
|
||||
.post-small .link {
|
||||
display: inline-block;
|
||||
margin: 0.2em;
|
||||
display: block;
|
||||
margin: 0.3em;
|
||||
border: 1px solid #999;
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
@ -134,6 +140,7 @@
|
||||
}
|
||||
|
||||
.post-small:not(.post-type-image) .link::after {
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
content: '...';
|
||||
z-index: 3;
|
||||
@ -158,6 +165,9 @@
|
||||
.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,12 +141,12 @@
|
||||
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 {
|
||||
@ -172,6 +166,9 @@
|
||||
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;
|
||||
}
|
||||
@ -186,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;
|
||||
@ -225,12 +241,17 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
display: none;
|
||||
@ -242,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;
|
||||
}
|
||||
|
||||
|
@ -56,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">
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,6 +19,7 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
maxResults: 15,
|
||||
minLengthToArbitrarySearch: 3,
|
||||
onApply: null,
|
||||
onDelete: null,
|
||||
onRender: null,
|
||||
additionalFilter: null,
|
||||
};
|
||||
@ -63,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);
|
||||
@ -182,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);
|
||||
@ -229,9 +241,15 @@ App.Controls.AutoCompleteInput = function($input) {
|
||||
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();
|
||||
|
@ -31,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();
|
||||
@ -54,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) {
|
||||
@ -74,6 +77,10 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
|
||||
function initAutoComplete() {
|
||||
var autoComplete = new App.Controls.AutoCompleteInput($input);
|
||||
autoComplete.onDelete = function(text) {
|
||||
removeTag(text);
|
||||
$input.val('');
|
||||
};
|
||||
autoComplete.onApply = function(text) {
|
||||
processText(text, SOURCE_AUTOCOMPLETION);
|
||||
$input.val('');
|
||||
@ -112,7 +119,7 @@ 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;
|
||||
}
|
||||
@ -288,7 +295,7 @@ 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();
|
||||
showOrHideSiblings(tagName);
|
||||
@ -380,7 +387,7 @@ App.Controls.TagInput = function($underlyingInput) {
|
||||
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();
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -53,7 +53,7 @@ App.Presenters.CommentListPresenter = function(
|
||||
if (comments.length === 0) {
|
||||
promise.wait(api.get('/comments/' + params.post.id))
|
||||
.then(function(response) {
|
||||
comments = response.json.data;
|
||||
comments = response.json.comments;
|
||||
render();
|
||||
}).fail(function() {
|
||||
console.log(arguments);
|
||||
@ -72,8 +72,7 @@ App.Presenters.CommentListPresenter = function(
|
||||
{
|
||||
commentListItemTemplate: templates.commentListItem,
|
||||
commentFormTemplate: templates.commentForm,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatMarkdown: util.formatMarkdown,
|
||||
util: util,
|
||||
comments: comments,
|
||||
post: post,
|
||||
},
|
||||
@ -102,9 +101,7 @@ App.Presenters.CommentListPresenter = function(
|
||||
function renderComment($targetList, comment) {
|
||||
var $item = jQuery('<li>' + templates.commentListItem({
|
||||
comment: comment,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
formatMarkdown: util.formatMarkdown,
|
||||
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,
|
||||
@ -179,7 +176,7 @@ App.Presenters.CommentListPresenter = function(
|
||||
|
||||
p.then(function(response) {
|
||||
$textarea.val('');
|
||||
var comment = response.json;
|
||||
var comment = response.json.comment;
|
||||
|
||||
if (commentToEdit) {
|
||||
$form.slideUp(function() {
|
||||
|
@ -38,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() {
|
||||
@ -53,7 +53,7 @@ App.Presenters.GlobalCommentListPresenter = function(
|
||||
|
||||
|
||||
function reinit(params, loaded) {
|
||||
pagerPresenter.reinit({query: params.query});
|
||||
pagerPresenter.reinit({query: params.query || {}});
|
||||
loaded();
|
||||
}
|
||||
|
||||
@ -65,12 +65,11 @@ 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,
|
||||
|
@ -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,8 +62,7 @@ App.Presenters.HistoryPresenter = function(
|
||||
|
||||
function renderHistory($page, historyItems) {
|
||||
$page.append(templates.history({
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
util: util,
|
||||
history: historyItems}));
|
||||
}
|
||||
|
||||
@ -73,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();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
@ -89,7 +109,7 @@ App.Presenters.PostEditPresenter = function(
|
||||
function editPost() {
|
||||
var $form = $target.find('form');
|
||||
var formData = new FormData();
|
||||
formData.append('seenEditTime', post.lastEditTime);
|
||||
formData.append('lastEditTime', post.lastEditTime);
|
||||
|
||||
if (privileges.canChangeContent && postContent) {
|
||||
formData.append('content', postContent);
|
||||
@ -126,11 +146,14 @@ App.Presenters.PostEditPresenter = function(
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
@ -150,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);
|
||||
|
@ -45,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() {
|
||||
@ -217,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.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,
|
||||
@ -71,7 +72,9 @@ App.Presenters.PostPresenter = function(
|
||||
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}],
|
||||
[postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}],
|
||||
[commentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
|
||||
function() { });
|
||||
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,13 +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,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
formatFileSize: util.formatFileSize,
|
||||
util: util,
|
||||
|
||||
historyTemplate: templates.history,
|
||||
|
||||
@ -179,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);
|
||||
@ -208,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);
|
||||
@ -216,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) {
|
||||
@ -324,6 +346,7 @@ App.Presenters.PostPresenter = function(
|
||||
App.DI.register('postPresenter', [
|
||||
'_',
|
||||
'jQuery',
|
||||
'appState',
|
||||
'util',
|
||||
'promise',
|
||||
'api',
|
||||
|
@ -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,34 +233,75 @@ App.Presenters.PostUploadPresenter = function(
|
||||
stopUpload();
|
||||
}
|
||||
|
||||
function addPostFromFile(file) {
|
||||
var post = _.extend({}, getDefaultPost(), {fileName: file.name, file: file});
|
||||
|
||||
fileDropper.readAsDataURL(file, function(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;
|
||||
}
|
||||
|
||||
@ -294,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());
|
||||
@ -314,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() {
|
||||
@ -360,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() {
|
||||
@ -409,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) {
|
||||
@ -418,7 +460,7 @@ App.Presenters.PostUploadPresenter = function(
|
||||
|
||||
var tagFilter = function(post) {
|
||||
return function(tag) {
|
||||
return post.tags.indexOf(tag) !== -1;
|
||||
return hasTag(post, tag);
|
||||
};
|
||||
};
|
||||
|
||||
@ -465,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);
|
||||
@ -475,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);
|
||||
});
|
||||
@ -500,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() {
|
||||
|
@ -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.';
|
||||
|
@ -36,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() {
|
||||
@ -108,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);
|
||||
});
|
||||
|
@ -66,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();
|
||||
@ -81,14 +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,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
tagCategories: getTagCategories(),
|
||||
util: util,
|
||||
historyTemplate: templates.history,
|
||||
}));
|
||||
$el.find('.post-list').hide();
|
||||
@ -127,7 +135,7 @@ 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.');
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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))
|
||||
|
@ -35,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() {
|
||||
@ -76,8 +76,7 @@ App.Presenters.UserListPresenter = function(
|
||||
_.each(users, function(user) {
|
||||
var $item = jQuery('<li>' + templates.listItem(_.extend({
|
||||
user: user,
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
util: util,
|
||||
}, privileges)) + '</li>');
|
||||
$target.append($item);
|
||||
});
|
||||
|
@ -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,8 +74,7 @@ App.Presenters.UserPresenter = function(
|
||||
$el.html(templates.user({
|
||||
user: user,
|
||||
isLoggedIn: auth.isLoggedIn(user.name),
|
||||
formatRelativeTime: util.formatRelativeTime,
|
||||
formatAbsoluteTime: util.formatAbsoluteTime,
|
||||
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 {
|
||||
|
@ -193,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;
|
||||
};
|
||||
|
||||
@ -206,6 +218,8 @@ 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
|
||||
@ -215,12 +229,6 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
|
||||
//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;
|
||||
};
|
||||
|
||||
@ -237,6 +245,17 @@ 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,
|
||||
@ -249,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>
|
||||
|
||||
|
||||
|
@ -27,8 +27,8 @@
|
||||
<% } %>
|
||||
</span>
|
||||
|
||||
<span class="date" title="<%= formatAbsoluteTime(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>
|
||||
|
@ -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>
|
||||
|
@ -22,8 +22,8 @@ var showDifference = function(className, difference) {
|
||||
<tbody>
|
||||
<% _.each(history, function( historyEntry) { %>
|
||||
<tr>
|
||||
<td class="time" title="<%= formatAbsoluteTime(historyEntry.time) %>">
|
||||
<%= formatRelativeTime(historyEntry.time) %>
|
||||
<td class="time" title="<%= util.formatAbsoluteTime(historyEntry.time) %>">
|
||||
<%= util.formatRelativeTime(historyEntry.time) %>
|
||||
</td>
|
||||
|
||||
<td class="user">
|
||||
|
@ -1,11 +1,29 @@
|
||||
<% 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 class="subheader">
|
||||
Serving <%= globals.postCount || 0 %> posts (<%= formatFileSize(globals.postSize || 0) %>)
|
||||
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>
|
||||
|
||||
@ -25,29 +43,16 @@
|
||||
<% } %>
|
||||
|
||||
uploaded
|
||||
<%= formatRelativeTime(post.uploadTime) %>
|
||||
<%= util.formatRelativeTime(post.creationTime) %>
|
||||
by
|
||||
<% showUser(post.user.name) %>
|
||||
</span>
|
||||
|
||||
<span class="right">
|
||||
featured
|
||||
<%= formatRelativeTime(post.lastFeatureTime) %>
|
||||
<%= util.formatRelativeTime(post.lastFeatureTime) %>
|
||||
by
|
||||
|
||||
<% 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>
|
||||
<% } %>
|
||||
<% showUser(user.name) %>
|
||||
</span>
|
||||
|
||||
</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,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"/>
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
<% 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">
|
||||
|
@ -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 class="enabled" 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>
|
||||
@ -32,7 +41,7 @@
|
||||
<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>
|
||||
<% } %>
|
||||
@ -72,6 +81,7 @@
|
||||
<% } %>
|
||||
</ul>
|
||||
|
||||
<div class="box">
|
||||
<h1>Tags (<%= _.size(post.tags) %>)</h1>
|
||||
<ul class="tags">
|
||||
<% _.each(post.tags, function(tag) { %>
|
||||
@ -87,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 %>">
|
||||
@ -109,13 +120,12 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<span class="date" title="<%= formatAbsoluteTime(post.uploadTime) %>">
|
||||
<%= formatRelativeTime(post.uploadTime) %>
|
||||
<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 %>">
|
||||
@ -126,7 +136,7 @@
|
||||
<% if (post.originalFileSize) { %>
|
||||
<li>
|
||||
File size:
|
||||
<%= formatFileSize(post.originalFileSize) %>
|
||||
<%= util.formatFileSize(post.originalFileSize) %>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
@ -137,11 +147,11 @@
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (post.lastEditTime !== post.uploadTime) { %>
|
||||
<% if (post.lastEditTime !== post.creationTime) { %>
|
||||
<li>
|
||||
Edited:
|
||||
<span title="<%= formatAbsoluteTime(post.lastEditTime) %>">
|
||||
<%= formatRelativeTime(post.lastEditTime) %>
|
||||
<span title="<%= util.formatAbsoluteTime(post.lastEditTime) %>">
|
||||
<%= util.formatRelativeTime(post.lastEditTime) %>
|
||||
</span>
|
||||
</li>
|
||||
<% } %>
|
||||
@ -149,7 +159,7 @@
|
||||
<% 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>
|
||||
<% } %>
|
||||
|
||||
@ -187,8 +197,10 @@
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<% if (_.any(post.relations)) { %>
|
||||
<div class="box">
|
||||
<h1>Related posts</h1>
|
||||
<ul class="related">
|
||||
<% _.each(post.relations, function(relatedPost) { %>
|
||||
@ -199,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>
|
||||
@ -213,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
|
||||
@ -245,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
|
||||
@ -258,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">
|
||||
@ -277,8 +296,7 @@
|
||||
<h1>History</h1>
|
||||
<%= historyTemplate({
|
||||
history: postHistory,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
formatAbsoluteTime: formatAbsoluteTime,
|
||||
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,8 +103,7 @@
|
||||
<h3>History</h3>
|
||||
<%= historyTemplate({
|
||||
history: tag.history,
|
||||
formatRelativeTime: formatRelativeTime,
|
||||
formatAbsoluteTime: formatAbsoluteTime,
|
||||
util: util,
|
||||
}) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
@ -19,11 +19,11 @@
|
||||
<%= user.name %>
|
||||
<% } %>
|
||||
</h1>
|
||||
<div class="date-joined" title="<%= formatAbsoluteTime(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" title="<%= formatAbsoluteTime(user.lastLoginTime) %>">
|
||||
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,15 +51,15 @@
|
||||
<table>
|
||||
<tr>
|
||||
<td>Registered:</td>
|
||||
<td title="<%= formatAbsoluteTime(user.registrationTime) %>">
|
||||
<%= formatRelativeTime(user.registrationTime) %>
|
||||
<td title="<%= util.formatAbsoluteTime(user.creationTime) %>">
|
||||
<%= util.formatRelativeTime(user.creationTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Seen:</td>
|
||||
<td title="<%= formatAbsoluteTime(user.lastLoginTime) %>">
|
||||
<%= formatRelativeTime(user.lastLoginTime) %>
|
||||
<td title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
|
||||
<%= util.formatRelativeTime(user.lastLoginTime) %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
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();
|
||||
}
|
||||
}
|
35
scripts/fix-dimensions
Executable file
35
scripts/fix-dimensions
Executable file
@ -0,0 +1,35 @@
|
||||
#!/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;
|
||||
use Szurubooru\Services\ImageConverter;
|
||||
use Szurubooru\Services\ImageManipulation\ImageManipulator;
|
||||
|
||||
$publicFileDao = Injector::get(PublicFileDao::class);
|
||||
$postDao = Injector::get(PostDao::class);
|
||||
$imageConverter = Injector::get(ImageConverter::class);
|
||||
$imageManipulator = Injector::get(ImageManipulator::class);
|
||||
|
||||
if (!isset($argv[1]))
|
||||
{
|
||||
echo "No post ID specified.";
|
||||
return;
|
||||
}
|
||||
$postId = intval($argv[1]);
|
||||
$post = $postDao->findById($postId);
|
||||
if (!$post)
|
||||
{
|
||||
echo "Post with this ID was not found in the database.";
|
||||
return;
|
||||
}
|
||||
|
||||
$image = $imageConverter->createImageFromBuffer($post->getContent());
|
||||
$post->setImageWidth($imageManipulator->getImageWidth($image));
|
||||
$post->setImageHeight($imageManipulator->getImageHeight($image));
|
||||
$postDao->save($post);
|
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 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();
|
@ -64,8 +64,7 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
|
||||
public function findAll()
|
||||
{
|
||||
$query = $this->pdo->from($this->tableName);
|
||||
$arrayEntities = iterator_to_array($query);
|
||||
return $this->arrayToEntities($arrayEntities);
|
||||
return $this->arrayToEntities($query);
|
||||
}
|
||||
|
||||
public function findById($entityId)
|
||||
@ -248,7 +247,7 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
|
||||
$query->where($sql, $bindings);
|
||||
}
|
||||
|
||||
protected function arrayToEntities(array $arrayEntities, $entityConverter = null)
|
||||
protected function arrayToEntities($arrayEntities, $entityConverter = null)
|
||||
{
|
||||
if ($entityConverter === null)
|
||||
$entityConverter = $this->entityConverter;
|
||||
|
@ -11,7 +11,7 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||
[
|
||||
'name' => $entity->getName(),
|
||||
'userId' => $entity->getUserId(),
|
||||
'uploadTime' => $this->entityTimeToDbTime($entity->getUploadTime()),
|
||||
'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
|
||||
'lastEditTime' => $this->entityTimeToDbTime($entity->getLastEditTime()),
|
||||
'safety' => $entity->getSafety(),
|
||||
'contentType' => $entity->getContentType(),
|
||||
@ -33,7 +33,7 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||
$entity = new Post(intval($array['id']));
|
||||
$entity->setName($array['name']);
|
||||
$entity->setUserId($array['userId']);
|
||||
$entity->setUploadTime($this->dbTimeToEntityTime($array['uploadTime']));
|
||||
$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
|
||||
$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
|
||||
$entity->setSafety(intval($array['safety']));
|
||||
$entity->setContentType(intval($array['contentType']));
|
||||
|
@ -11,6 +11,7 @@ class TagEntityConverter extends AbstractEntityConverter implements IEntityConve
|
||||
[
|
||||
'name' => $entity->getName(),
|
||||
'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
|
||||
'lastEditTime' => $this->entityTimeToDbTime($entity->getLastEditTime()),
|
||||
'banned' => intval($entity->isBanned()),
|
||||
'category' => $entity->getCategory(),
|
||||
];
|
||||
@ -21,6 +22,7 @@ class TagEntityConverter extends AbstractEntityConverter implements IEntityConve
|
||||
$entity = new Tag(intval($array['id']));
|
||||
$entity->setName($array['name']);
|
||||
$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
|
||||
$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
|
||||
$entity->setMeta(Tag::META_USAGES, intval($array['usages']));
|
||||
$entity->setBanned($array['banned']);
|
||||
$entity->setCategory($array['category']);
|
||||
|
@ -15,7 +15,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||
'passwordHash' => $entity->getPasswordHash(),
|
||||
'passwordSalt' => $entity->getPasswordSalt(),
|
||||
'accessRank' => $entity->getAccessRank(),
|
||||
'registrationTime' => $this->entityTimeToDbTime($entity->getRegistrationTime()),
|
||||
'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
|
||||
'lastLoginTime' => $this->entityTimeToDbTime($entity->getLastLoginTime()),
|
||||
'avatarStyle' => $entity->getAvatarStyle(),
|
||||
'browsingSettings' => json_encode($entity->getBrowsingSettings()),
|
||||
@ -33,7 +33,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
|
||||
$entity->setPasswordHash($array['passwordHash']);
|
||||
$entity->setPasswordSalt($array['passwordSalt']);
|
||||
$entity->setAccessRank(intval($array['accessRank']));
|
||||
$entity->setRegistrationTime($this->dbTimeToEntityTime($array['registrationTime']));
|
||||
$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
|
||||
$entity->setLastLoginTime($this->dbTimeToEntityTime($array['lastLoginTime']));
|
||||
$entity->setAvatarStyle(intval($array['avatarStyle']));
|
||||
$entity->setBrowsingSettings(json_decode($array['browsingSettings']));
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace Szurubooru\Dao;
|
||||
use Szurubooru\Dao\EntityConverters\FavoriteEntityConverter;
|
||||
use Szurubooru\Dao\PostDao;
|
||||
use Szurubooru\Dao\UserDao;
|
||||
use Szurubooru\DatabaseConnection;
|
||||
use Szurubooru\Entities\Entity;
|
||||
use Szurubooru\Entities\Favorite;
|
||||
@ -10,10 +12,14 @@ use Szurubooru\Services\TimeService;
|
||||
|
||||
class FavoritesDao extends AbstractDao implements ICrudDao
|
||||
{
|
||||
private $userDao;
|
||||
private $postDao;
|
||||
private $timeService;
|
||||
|
||||
public function __construct(
|
||||
DatabaseConnection $databaseConnection,
|
||||
UserDao $userDao,
|
||||
PostDao $postDao,
|
||||
TimeService $timeService)
|
||||
{
|
||||
parent::__construct(
|
||||
@ -21,6 +27,8 @@ class FavoritesDao extends AbstractDao implements ICrudDao
|
||||
'favorites',
|
||||
new FavoriteEntityConverter());
|
||||
|
||||
$this->userDao = $userDao;
|
||||
$this->postDao = $postDao;
|
||||
$this->timeService = $timeService;
|
||||
}
|
||||
|
||||
@ -58,6 +66,23 @@ class FavoritesDao extends AbstractDao implements ICrudDao
|
||||
$this->deleteById($favorite->getId());
|
||||
}
|
||||
|
||||
protected function afterLoad(Entity $favorite)
|
||||
{
|
||||
$favorite->setLazyLoader(
|
||||
Favorite::LAZY_LOADER_USER,
|
||||
function (Favorite $favorite)
|
||||
{
|
||||
return $this->userDao->findById($favorite->getUserId());
|
||||
});
|
||||
|
||||
$favorite->setLazyLoader(
|
||||
Favorite::LAZY_LOADER_POST,
|
||||
function (Favorite $favorite)
|
||||
{
|
||||
return $this->postDao->findById($favorite->getPostId());
|
||||
});
|
||||
}
|
||||
|
||||
private function get(User $user, Entity $entity)
|
||||
{
|
||||
$query = $this->pdo->from($this->tableName)->where('userId', $user->getId());
|
||||
|
@ -44,10 +44,53 @@ class FileDao implements IFileDao
|
||||
return $this->directory . DIRECTORY_SEPARATOR . $fileName;
|
||||
}
|
||||
|
||||
public function listAll()
|
||||
{
|
||||
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory));
|
||||
$files = [];
|
||||
foreach ($iterator as $path)
|
||||
{
|
||||
if (!$path->isDir())
|
||||
$files[] = $this->getRelativePath($this->directory, $path->getPathName());
|
||||
}
|
||||
return $files;
|
||||
}
|
||||
|
||||
private function createFolders($fileName)
|
||||
{
|
||||
$fullPath = dirname($this->getFullPath($fileName));
|
||||
if (!file_exists($fullPath))
|
||||
mkdir($fullPath, 0777, true);
|
||||
}
|
||||
|
||||
private function getRelativePath($from, $to)
|
||||
{
|
||||
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
|
||||
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
|
||||
$from = explode('/', str_replace('\\', '/', $from));
|
||||
$to = explode('/', str_replace('\\', '/', $to));
|
||||
$relPath = $to;
|
||||
foreach ($from as $depth => $dir)
|
||||
{
|
||||
if ($dir === $to[$depth])
|
||||
{
|
||||
array_shift($relPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
$remaining = count($from) - $depth;
|
||||
if ($remaining > 1)
|
||||
{
|
||||
$padLength = (count($relPath) + $remaining - 1) * -1;
|
||||
$relPath = array_pad($relPath, $padLength, '..');
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
$relPath[0] = $relPath[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode('/', $relPath);
|
||||
}
|
||||
}
|
||||
|
@ -149,7 +149,7 @@ class PostDao extends AbstractDao implements ICrudDao
|
||||
return;
|
||||
}
|
||||
|
||||
elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT)
|
||||
elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT_AUTHOR)
|
||||
{
|
||||
foreach ($requirement->getValue()->getValues() as $userName)
|
||||
{
|
||||
@ -194,6 +194,17 @@ class PostDao extends AbstractDao implements ICrudDao
|
||||
return;
|
||||
}
|
||||
|
||||
elseif ($requirement->getType() === PostFilter::REQUIREMENT_TUMBLEWEED)
|
||||
{
|
||||
$sql = 'posts.score = 0
|
||||
AND posts.commentCount = 0
|
||||
AND posts.favCount = 0';
|
||||
if ($requirement->isNegated())
|
||||
$sql = 'NOT (' . $sql . ')';
|
||||
$query->where($sql, true);
|
||||
return;
|
||||
}
|
||||
|
||||
parent::decorateQueryFromRequirement($query, $requirement);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ use Szurubooru\Services\ThumbnailService;
|
||||
class UserDao extends AbstractDao implements ICrudDao
|
||||
{
|
||||
const ORDER_NAME = 'name';
|
||||
const ORDER_REGISTRATION_TIME = 'registrationTime';
|
||||
const ORDER_CREATION_TIME = 'creationTime';
|
||||
|
||||
private $fileDao;
|
||||
private $thumbnailService;
|
||||
|
@ -65,9 +65,12 @@ final class Dispatcher
|
||||
$json['__statements'] = $this->databaseConnection->getPDO()->getStatements();
|
||||
}
|
||||
|
||||
if (!$this->httpHelper->isRedirecting())
|
||||
{
|
||||
$this->httpHelper->setResponseCode($code);
|
||||
$this->httpHelper->setHeader('Content-Type', 'application/json');
|
||||
$this->httpHelper->outputJSON($json);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ final class Post extends Entity
|
||||
const POST_TYPE_FLASH = 2;
|
||||
const POST_TYPE_VIDEO = 3;
|
||||
const POST_TYPE_YOUTUBE = 4;
|
||||
const POST_TYPE_ANIMATED_IMAGE = 5;
|
||||
|
||||
const FLAG_LOOP = 1;
|
||||
|
||||
@ -28,7 +29,7 @@ final class Post extends Entity
|
||||
|
||||
private $name;
|
||||
private $userId;
|
||||
private $uploadTime;
|
||||
private $creationTime;
|
||||
private $lastEditTime;
|
||||
private $safety;
|
||||
private $contentType;
|
||||
@ -78,14 +79,14 @@ final class Post extends Entity
|
||||
$this->safety = $safety;
|
||||
}
|
||||
|
||||
public function getUploadTime()
|
||||
public function getCreationTime()
|
||||
{
|
||||
return $this->uploadTime;
|
||||
return $this->creationTime;
|
||||
}
|
||||
|
||||
public function setUploadTime($uploadTime)
|
||||
public function setCreationTime($creationTime)
|
||||
{
|
||||
$this->uploadTime = $uploadTime;
|
||||
$this->creationTime = $creationTime;
|
||||
}
|
||||
|
||||
public function getLastEditTime()
|
||||
|
@ -5,6 +5,7 @@ final class Tag extends Entity
|
||||
{
|
||||
private $name;
|
||||
private $creationTime;
|
||||
private $lastEditTime;
|
||||
private $banned = false;
|
||||
private $category = 'default';
|
||||
|
||||
@ -33,6 +34,16 @@ final class Tag extends Entity
|
||||
$this->creationTime = $creationTime;
|
||||
}
|
||||
|
||||
public function getLastEditTime()
|
||||
{
|
||||
return $this->lastEditTime;
|
||||
}
|
||||
|
||||
public function setLastEditTime($lastEditTime)
|
||||
{
|
||||
$this->lastEditTime = $lastEditTime;
|
||||
}
|
||||
|
||||
public function isBanned()
|
||||
{
|
||||
return $this->banned;
|
||||
|
@ -23,7 +23,7 @@ final class User extends Entity
|
||||
private $passwordHash;
|
||||
private $passwordSalt;
|
||||
private $accessRank;
|
||||
private $registrationTime;
|
||||
private $creationTime;
|
||||
private $lastLoginTime;
|
||||
private $avatarStyle;
|
||||
private $browsingSettings;
|
||||
@ -110,14 +110,14 @@ final class User extends Entity
|
||||
$this->accessRank = $accessRank;
|
||||
}
|
||||
|
||||
public function getRegistrationTime()
|
||||
public function getCreationTime()
|
||||
{
|
||||
return $this->registrationTime;
|
||||
return $this->creationTime;
|
||||
}
|
||||
|
||||
public function setRegistrationTime($registrationTime)
|
||||
public function setCreationTime($creationTime)
|
||||
{
|
||||
$this->registrationTime = $registrationTime;
|
||||
$this->creationTime = $creationTime;
|
||||
}
|
||||
|
||||
public function getLastLoginTime()
|
||||
|
@ -14,7 +14,7 @@ class PostEditFormData implements IValidatable
|
||||
public $relations;
|
||||
public $flags;
|
||||
|
||||
public $seenEditTime;
|
||||
public $lastEditTime;
|
||||
|
||||
public function __construct($inputReader = null)
|
||||
{
|
||||
@ -29,7 +29,7 @@ class PostEditFormData implements IValidatable
|
||||
$this->tags = preg_split('/[\s+]/', $inputReader->tags);
|
||||
if ($inputReader->relations !== null)
|
||||
$this->relations = array_filter(preg_split('/[\s+]/', $inputReader->relations));
|
||||
$this->seenEditTime = $inputReader->seenEditTime;
|
||||
$this->lastEditTime = $inputReader->lastEditTime;
|
||||
$this->flags = new \StdClass;
|
||||
$this->flags->loop = !empty($inputReader->loop);
|
||||
}
|
||||
|
@ -41,4 +41,3 @@ class TagEditFormData implements IValidatable
|
||||
$validator->validatePostTags($this->suggestions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,4 +39,3 @@ class UploadFormData implements IValidatable
|
||||
$validator->validatePostSource($this->source);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ class EnumHelper
|
||||
'video' => Post::POST_TYPE_VIDEO,
|
||||
'flash' => Post::POST_TYPE_FLASH,
|
||||
'youtube' => Post::POST_TYPE_YOUTUBE,
|
||||
'animation' => Post::POST_TYPE_ANIMATED_IMAGE,
|
||||
];
|
||||
|
||||
private static $snapshotTypeMap =
|
||||
@ -103,7 +104,12 @@ class EnumHelper
|
||||
$key = trim(strtolower($enumString));
|
||||
$lowerEnumMap = array_change_key_case($enumMap, \CASE_LOWER);
|
||||
if (!isset($lowerEnumMap[$key]))
|
||||
throw new \DomainException('Unrecognized value: ' . $enumString);
|
||||
{
|
||||
throw new \DomainException(sprintf(
|
||||
'Unrecognized value: %s.' . PHP_EOL . 'Possible values: %s',
|
||||
$enumString,
|
||||
implode(', ', array_keys($lowerEnumMap))));
|
||||
}
|
||||
|
||||
return $lowerEnumMap[$key];
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ namespace Szurubooru\Helpers;
|
||||
|
||||
class HttpHelper
|
||||
{
|
||||
private $redirected = false;
|
||||
|
||||
public function setResponseCode($code)
|
||||
{
|
||||
http_response_code($code);
|
||||
@ -29,9 +31,26 @@ class HttpHelper
|
||||
}
|
||||
|
||||
public function getRequestHeaders()
|
||||
{
|
||||
if (function_exists('getallheaders'))
|
||||
{
|
||||
return getallheaders();
|
||||
}
|
||||
$result = [];
|
||||
foreach ($_SERVER as $key => $value)
|
||||
{
|
||||
if (substr($key, 0, 5) === "HTTP_")
|
||||
{
|
||||
$key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
|
||||
$result[$key] = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
$result[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getRequestHeader($key)
|
||||
{
|
||||
@ -50,4 +69,23 @@ class HttpHelper
|
||||
$requestUri = preg_replace('/\?.*$/', '', $requestUri);
|
||||
return $requestUri;
|
||||
}
|
||||
|
||||
public function redirect($destination)
|
||||
{
|
||||
$this->setResponseCode(307);
|
||||
$this->setHeader('Location', $destination);
|
||||
$this->redirected = true;
|
||||
}
|
||||
|
||||
public function nonCachedRedirect($destination)
|
||||
{
|
||||
$this->setResponseCode(303);
|
||||
$this->setHeader('Location', $destination);
|
||||
$this->redirected = true;
|
||||
}
|
||||
|
||||
public function isRedirecting()
|
||||
{
|
||||
return $this->redirected;
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,9 @@ final class InputReader extends \ArrayObject
|
||||
if (!isset($_FILES[$fileName]))
|
||||
return null;
|
||||
|
||||
if (!$_FILES[$fileName]['tmp_name'])
|
||||
throw new \Exception('File is probably too big.');
|
||||
|
||||
return file_get_contents($_FILES[$fileName]['tmp_name']);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,12 @@ class MimeHelper
|
||||
return self::getMimeTypeFrom16Bytes(substr($buffer, 0, 16));
|
||||
}
|
||||
|
||||
public static function isBufferAnimatedGif($buffer)
|
||||
{
|
||||
return strtolower(self::getMimeTypeFromBuffer($buffer)) === 'image/gif'
|
||||
and preg_match_all('#\x21\xf9\x04.{4}\x00[\x2c\x21]#s', $buffer) > 1;
|
||||
}
|
||||
|
||||
public static function isFlash($mime)
|
||||
{
|
||||
return strtolower($mime) === 'application/x-shockwave-flash';
|
||||
|
@ -3,8 +3,8 @@ namespace Szurubooru;
|
||||
|
||||
class NotSupportedException extends \BadMethodCallException
|
||||
{
|
||||
public function __construct()
|
||||
public function __construct($message = null)
|
||||
{
|
||||
parent::__construct('Not supported');
|
||||
parent::__construct($message === null ? 'Not supported' : $message);
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +49,6 @@ class AddComment extends AbstractCommentRoute
|
||||
|
||||
$post = $this->postService->getByNameOrId($args['postNameOrId']);
|
||||
$comment = $this->commentService->createComment($post, $this->inputReader->text);
|
||||
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
|
||||
return ['comment' => $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig())];
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,6 @@ class DeleteComment extends AbstractCommentRoute
|
||||
? Privilege::DELETE_OWN_COMMENTS
|
||||
: Privilege::DELETE_ALL_COMMENTS);
|
||||
|
||||
return $this->commentService->deleteComment($comment);
|
||||
$this->commentService->deleteComment($comment);
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,6 @@ class EditComment extends AbstractCommentRoute
|
||||
: Privilege::EDIT_ALL_COMMENTS);
|
||||
|
||||
$comment = $this->commentService->updateComment($comment, $this->inputReader->text);
|
||||
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
|
||||
return ['comment' => $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig())];
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ class GetComments extends AbstractCommentRoute
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'comments' => $data,
|
||||
'pageSize' => $result->getPageSize(),
|
||||
'totalRecords' => $result->getTotalRecords()];
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user