158 Commits
1.0.2 ... 1.x

Author SHA1 Message Date
rr-
31f336d690 Added script for fixing image dimensions 2016-07-28 19:14:43 +02:00
rr-
0e4365795b Reduced Imagick memory consumption 2016-07-28 19:14:23 +02:00
rr-
96769f52cf Allowed colons at the end of tags 2016-06-18 22:01:03 +02:00
rr-
6660ee77e1 Bumped version to 1.1.0
(Renamed 0.9.x to 1.0.x on GH)
2016-04-13 11:17:52 +02:00
7f4bebe404 Merge pull request #81 from kotcrab/arrows
Added arrow keys support
2016-04-06 08:10:47 +02:00
6a7792239e Add arrow keys support 2016-04-05 23:34:50 +02:00
rr-
f248a6ab4e Fixed user Markdown links 2016-03-13 15:54:00 +01:00
rr-
f12ce7a7c5 Fixed serving content w/o ranges caused errors 2016-03-10 14:36:41 +01:00
rr-
f8514bfdc7 Fix authorization test 2016-03-09 21:07:04 +01:00
01a256bbbc Merge pull request #79 from kotcrab/gruntfile-fix
Fix mousetrap library path
2016-03-09 20:22:37 +01:00
abae786748 Fix mousetrap library path 2016-03-09 20:12:04 +01:00
rr-
d3b794c9da Added MySQL to the list of dependencies 2016-03-08 23:24:13 +01:00
rr-
9d15bbfcce Updated the installation guide 2016-03-08 22:53:14 +01:00
rr-
525f05b570 Fixed npm bugging about unused fields 2016-03-08 22:11:16 +01:00
rr-
54d95c11c5 Corrected software dependencies in install guide 2016-03-08 22:10:11 +01:00
rr-
0a58e12827 Small OCD fixes to documentation 2016-03-08 21:59:18 +01:00
rr-
818d9ac3c8 Replaced SJIS border with background 2016-02-29 01:02:39 +01:00
rr-
9eaab55dab Increased maximum comment length to 5000 chars 2016-02-29 00:59:54 +01:00
rr-
36d2842b6e Added [sjis]...[/sjis] tag 2016-02-29 00:59:54 +01:00
rr-
87681f8c0d Added file_size search syntax 2016-02-19 09:54:10 +01:00
rr-
f49a8fabab Added image_{width,height,area} search order kinds 2016-02-19 09:51:37 +01:00
rr-
2bba57e8de Added image_{width,height,area} search syntax 2016-02-19 09:44:46 +01:00
rr-
f7df1cb536 Added image search for animations 2016-02-08 07:30:59 +01:00
rr-
2ab636e569 Added protection against unknown image sizes 2016-01-21 12:15:14 +01:00
rr-
21ddb8a90b Fixed post note link being shown for videos and YT 2016-01-13 21:25:12 +01:00
rr-
1ce16c80ec Fixed inability to add post notes to GIFs 2016-01-13 21:24:45 +01:00
rr-
0eabc4ed41 Added support for HTTP ranges (= .webm + seeking) 2015-12-30 20:08:16 +01:00
rr-
965f772515 Added special:tumbleweed 2015-12-29 12:24:35 +01:00
rr-
770dba8a41 Tweaked avatar margins in comment lists 2015-12-29 12:02:53 +01:00
rr-
0ea40ce6d0 Fixed CSS of lists in comment contents 2015-12-29 12:02:36 +01:00
rr-
e2bc5d3415 Added feature_count to search terms 2015-12-29 11:57:25 +01:00
rr-
5df5a78df5 Improved Markdown parsing of permalinks 2015-12-29 11:53:38 +01:00
rr-
13d01dee27 Changed youtube videos to 16:9 2015-12-27 17:05:48 +01:00
rr-
9fd34f06aa Limited post note width 2015-12-24 21:00:31 +01:00
rr-
92631df9a4 Fixed notes disappearing off the screen 2015-12-24 20:59:39 +01:00
rr-
e623513e3d Fixed SQL column ambiguity in filter parsing 2015-12-17 22:52:21 +01:00
rr-
7e2e90ad3f Removed hack for GIF preloading
It was effectively destroying caching.
2015-12-13 23:57:25 +01:00
rr-
7645c012a5 Added "upload:" alias as requested by John Doe 2015-12-13 21:48:53 +01:00
rr-
ecb3901bbe Fixed tests 2015-11-25 11:39:45 +01:00
rr-
ee09a09833 Fixed lazy loading users and posts for Favorite 2015-11-25 11:35:18 +01:00
rr-
13d77dd14a Fixed whitespace 2015-11-25 09:48:03 +01:00
rr-
b8f90dbd95 Fixed stats cron job 2015-11-25 08:13:25 +01:00
rr-
5305bb68a4 Simplified search parsing
Reduced execution flow dependencies and made all search parsers share
the basic code rather than implementing everything all over again in
each parser through awkward protected functions.
2015-11-25 01:25:43 +01:00
rr-
5aa75a4150 Fixed sorting users by registration time 2015-11-25 01:06:19 +01:00
rr-
b3c5212c84 Added ability to search tags by usage count 2015-11-24 21:57:14 +01:00
rr-
96195f0efc Added ability to search tags by creation/edit time 2015-11-24 21:57:14 +01:00
rr-
d769eaed61 Added ability to search posts by edit dates 2015-11-24 21:41:08 +01:00
rr-
2df43201ba Added order:edit_time support for tag list 2015-11-24 18:23:47 +01:00
rr-
40e869b848 Added support to search for posts by creation time 2015-11-24 18:14:11 +01:00
rr-
b7456463eb Commonized naming of "creation time" property
Rather than having "creation time", "upload time", "registration time"
etc. I think it is better to have a single "creation time"
entity-agnostic property (like the one Tags had thus far).
2015-11-24 18:14:11 +01:00
rr-
d49f76c9f1 Added ability to sort posts by feature count 2015-11-24 18:14:11 +01:00
rr-
645573a272 Changed global comment list sort to creation time
Before this change, it was using edit time, which resulted in bumping
old posts every time a comment was edited. Users reported this behavior
as awkward and unintuitive, so I've changed it.
2015-11-24 18:14:11 +01:00
rr-
f5aed19bf3 Added verbosity to image conversion errors 2015-11-23 11:35:09 +01:00
rr-
28bba097c3 Added ffmpeg support in Flash thumbnails creation 2015-11-01 11:02:50 +01:00
rr-
105a564c7d Fixed problem with %-encoded URIs 2015-09-30 20:03:32 +02:00
rr-
15739ac7cc Fixed adding ghost post notes after removing some 2015-09-16 07:43:18 +02:00
rr-
0edbd9bf40 Fixed editing new post notes duplicating them 2015-09-16 07:40:07 +02:00
rr-
a31d5849fc Improved connection error reporting 2015-09-09 22:32:18 +02:00
rr-
180252cc64 Removed masking of image extensions' exceptions 2015-09-06 22:57:53 +02:00
rr-
48bb4fc803 Added option to set image manipulation extension 2015-09-06 22:57:28 +02:00
rr-
58768acc1c Increased control over tag categories 2015-08-05 18:08:02 +02:00
rr-
42f37d8fee Further improved text of some error messages 2015-08-05 17:19:10 +02:00
rr-
ec5ff5f230 Increased margin for error messages 2015-08-05 16:57:33 +02:00
rr-
7350b89a33 Fixed mass tag
Regression from b3def7f.
2015-08-04 21:34:59 +02:00
rr-
91f33c9e08 Fixed pager not showing in recent edits
Regression from b3def7f.
2015-08-04 19:52:47 +02:00
rr-
6b933132a5 Added helpful messages for invalid search orders 2015-08-04 19:47:18 +02:00
rr-
8c87a93774 Added support for note count based post searches 2015-08-04 19:31:57 +02:00
rr-
7ca582186b Fixed redirection after tag editing
Regression from b3def7f.
2015-08-04 19:22:03 +02:00
rr-
465a61ff4a Improved API for post editing 2015-08-03 19:27:25 +02:00
rr-
1ad5d7475c Enabled restricted users to delete own accounts 2015-08-03 19:23:11 +02:00
rr-
ebd25cd9a9 Fixed removing users always logging out 2015-08-03 19:23:11 +02:00
rr-
b3def7fc21 Improved API responses 2015-08-03 19:23:11 +02:00
rr-
5a537ba168 Fixed "original width" fit on Webkit 2015-07-29 19:39:10 +02:00
rr-
b4db90bcdc Hidden most edit controls by default
This reduces form size in half, which should improve editing experience.
2015-07-19 19:32:09 +02:00
rr-
c6a17d33af Fixed fit modes not appearing in sidebar 2015-07-19 19:32:09 +02:00
rr-
37eabe1556 Improved post view on small layout
- post notes no longer disappear
- post image is larger
- sidebar stacks into columns
- things from sidebar are centered
2015-07-19 19:32:08 +02:00
rr-
1969f0e3fa Improved label naming in browsing settings 2015-07-19 19:32:06 +02:00
rr-
c0a474ed82 Added new fit mode for both dimensions 2015-07-19 19:00:53 +02:00
rr-
6380043a9a Added option to upscale small posts 2015-07-19 19:00:45 +02:00
rr-
b75df289e9 Fixed "fit to height" upscaling small posts 2015-07-19 18:05:16 +02:00
rr-
8db72633f6 Fixed broken post notes after fit mode changes 2015-07-19 18:03:47 +02:00
rr-
44ef66f65c Fixed fit mode state disappearing after AJAX calls 2015-07-19 12:35:55 +02:00
rr-
7511430b2a Fixed jshint warnings 2015-07-19 12:28:01 +02:00
rr-
362087ee63 Split cycle fit mode button to 1 for each fit mode 2015-07-19 12:26:11 +02:00
rr-
5ad854e38a Added fit mode to browsing settings 2015-07-19 11:53:34 +02:00
rr-
5882998c20 Added option to cycle fit mode to sidebar 2015-07-19 11:32:11 +02:00
rr-
579e59e7df Added fit to height mode to [F] hotkey 2015-07-19 11:29:14 +02:00
rr-
64ae9a7c74 Moved [F]ullscreen hotkey to PostContentPresenter 2015-07-19 10:58:50 +02:00
rr-
6b6acb0bbf Fixed appearance on luakit 2015-07-12 19:19:08 +02:00
rr-
11648e055c Added support for explicit HTTP permalinks 2015-07-02 20:24:01 +02:00
rr-
3c83f711c9 Removed MaxCDN dependency 2015-07-02 20:15:20 +02:00
rr-
bd7dd9a2ad Stripped file extensions from executables 2015-06-28 12:29:16 +02:00
rr-
02c8353175 Added shebangs and +x to scripts 2015-06-28 12:27:50 +02:00
rr-
77e51c2e10 Replaced some more whitespace 2015-06-28 12:26:10 +02:00
rr-
fd448bac87 Switched to PHPMailer 2015-06-28 12:24:46 +02:00
rr-
027b98ce76 Fixed dangling tags on MySQL after post removal 2015-06-28 10:24:40 +02:00
rr-
edee487ff9 Removed trailing newlines 2015-06-28 10:10:07 +02:00
rr-
2702518e31 Switched to spaces 2015-06-28 10:07:56 +02:00
rr-
79df9b56d3 Added FPM-based support for too big files 2015-06-28 09:59:47 +02:00
rr-
3b1544eff3 Fixed OOM errors in scripts 2015-06-27 19:03:47 +02:00
rr-
c74edbee51 Fixed overwriting redirection HTTP status codes 2015-06-27 18:35:21 +02:00
rr-
8407a3f70e Fixed getRequestHeaders for CGI servers 2015-06-27 18:07:31 +02:00
9c1db78b69 Fixed typo 2015-06-27 00:03:31 +02:00
9b2238d423 Fixed tagging uploaded posts in Chrome 2015-06-26 23:40:27 +02:00
b5d6e4837d Removed a way to repeat a tag in uploaded post 2015-06-24 23:51:11 +02:00
d20fe3d95a Fixed post labels taking unwanted focus 2015-06-05 09:55:25 +02:00
a7a2f31dc2 Fixed history navigation in comment list 2015-06-04 10:48:16 +02:00
b26fd88d6f Stripped www. from domain names 2015-06-03 22:49:46 +02:00
a69f8563e8 Fixed prev/next button behavior in pager 2015-05-29 16:51:15 +02:00
24d8bf5295 Improved search error messages 2015-05-23 10:46:17 +02:00
5412ac14b9 Added GIF detection 2015-05-23 10:05:05 +02:00
38bfbfb8f3 Added Tab and Shift+Tab support to autocomplete 2015-05-22 22:44:25 +02:00
4ba855871f Added invisible label for tag inputs
Improves integration with Vimperator
2015-05-16 12:36:43 +02:00
0727433a9e Wrapped thumbnails with links in post upload form
Improves integration with Vimperator
2015-05-16 12:31:27 +02:00
627a8db5f3 Added prev/next buttons in post upload form 2015-05-16 12:31:14 +02:00
740cc85775 Fixed jshint warning 2015-05-16 12:30:48 +02:00
48004f1117 Added prev/next page buttons for pager
Integrated better with vimperator.
2015-05-14 23:29:42 +02:00
4126de8e25 Added blurring of active element on post edit
Integrates better with vimperator.
2015-05-14 23:13:55 +02:00
c569504ce7 Added ability to turn keyboard shortcuts off 2015-05-14 23:04:30 +02:00
8d119d2b62 Fixed upload messages margin on screen screens 2015-05-09 19:32:04 +02:00
f8851bf26d Fixed jshint warnings 2015-05-08 18:39:45 +02:00
19e7fa94f7 Fixed autocomplete position near page bottom 2015-05-08 18:39:20 +02:00
06180f5b50 Fixed showing preview link for non-images in upload 2015-03-21 08:17:58 +01:00
aa228d5125 Further improved memory footprint in post upload 2015-03-21 08:16:02 +01:00
5f4260d0a7 Fixed uploading posts from URLs 2015-03-20 13:35:10 +01:00
e7e50cfb3a Fixed one-letter hotkeys not firing in radioboxes 2015-03-19 23:02:53 +01:00
09d8e5ae1c Fixed prev/next post selectors moving page 2015-03-19 22:57:37 +01:00
fce9c3483a Added link to full image preview in post uploads 2015-03-19 22:49:24 +01:00
a3157a48ec Removed lightbox from post uploads 2015-03-19 22:49:24 +01:00
c35ed15946 Increased preview size in uploads 2015-03-19 22:49:19 +01:00
f75b4505a1 Reduced memory footprint for long upload sessions 2015-03-19 22:07:43 +01:00
d98474cc6a Fixed broken home page for anonymous users 2015-03-14 23:07:34 +01:00
2e06422b62 Shortened GIF guard 2015-03-13 21:16:34 +01:00
0aad36228a Added featured post uploader name to home page 2015-03-13 21:00:09 +01:00
7c77c7a87b Fixed post resizing, alignment etc. 2015-03-13 20:56:12 +01:00
0cf29a657a Fixed Upgrade35 wiping tag and post relations 2015-03-13 09:47:30 +01:00
fdb029eb5c Increased post notes placement precision 2015-03-13 09:47:07 +01:00
eb3b02c28d Improved post sizing, added [F]ullscreen hotkey 2015-03-13 09:38:50 +01:00
65bc6705d3 Added script for finding dead posts 2015-03-11 10:15:01 +01:00
5f0706c0b4 Fixed orphan records and denormalization errors 2015-02-23 20:42:12 +01:00
e7ea60f293 Fixed bad arrows behavior while editing post notes 2015-02-22 19:30:07 +01:00
b416868aa7 Added arrow hotkeys to Draggable and Resizable
Which means better control over the post notes placement for all the
keyboard lovers.
2015-02-22 18:56:35 +01:00
90406b1278 Refactored Resizable to match Draggable 2015-02-22 18:47:32 +01:00
04a16a2a36 Added delete key support for auto complete 2015-02-22 18:43:35 +01:00
72e9400e1d Fixed upload button margin 2015-02-22 12:11:20 +01:00
d425b0df2e Fixed inconsistent margins 2015-02-22 10:32:55 +01:00
4cad09b85e Added space after tags in tag input 2015-02-14 14:00:16 +01:00
a59a57fe70 Increased limit for pasted text length 2015-02-13 08:35:22 +01:00
0c4d984157 Reduced font size by 2px 2015-01-27 09:22:41 +01:00
ea5262fa2b Fixed font size for form elements 2015-01-26 22:15:12 +01:00
2ab4da11fc Improved font scaling on Android 2015-01-26 08:50:25 +01:00
9090ac6fb9 Fixed broken test 2014-12-26 09:49:35 +01:00
eb77b6811a Fixed negative order in searches 2014-12-26 09:48:04 +01:00
0945ed64ee Added logging of exceptions thrown by templates
(finally)
2014-12-20 12:54:39 +01:00
4d9fc51819 Fixed global comment list 2014-12-20 12:51:22 +01:00
1897297127 Added search query minifying
Seeing 'page=1' and 'query=' in every other link was tiresome. I changed
the rules so that such keys are appended only if they hold nontrivial
values.
2014-12-20 10:36:29 +01:00
970b9bf06d Simplified util/misc.js requires 2014-12-20 10:30:10 +01:00
e5f2e293f0 Bumped version to 0.9.2 2014-12-14 21:00:38 +01:00
395 changed files with 22171 additions and 20738 deletions

View File

@ -1,28 +1,40 @@
Prerequisites 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 - `Apache` 2.4+
- mod_rewrite - `mod_rewrite`
- mod_mime_magic (recommended) - `mod_mime_magic` (recommended)
- PHP 5.6.0 - `PHP` 5.6.0+
- pdo_sqlite - `pdo_mysql`
- imagick or gd - `imagick` or `gd`
- composer (PHP package manager) - `MySQL` or `MariaDB`
- npm (node.js package manager) - `composer` (`PHP` package manager)
- `npm` (`node.js` package manager)
Optional modules: Optional software:
- dump-gnash or swfrender for flash thumbnails - `dump-gnash`, `swfrender` or `ffmpeg` for Flash thumbnails
- ffmpegthumbnailer or ffmpeg for video 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 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: commands in the terminal:
composer update 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 `szurubooru` uses `grunt` to run tasks like database upgrades and tests. In
to use grunt from the terminal, you can use: order to use `grunt` from the terminal, you can use:
node_modules/grunt-cli/bin/grunt [TASK] node_modules/grunt-cli/bin/grunt [TASK]
@ -43,25 +55,25 @@ administrator:
npm install -g grunt-cli 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] 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): your setup):
;Linux ;Linux
extension=pdo_sqlite.so extension=pdo_mysql.so
;Windows ;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 ;Linux
extension=imagick.so 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 Creating virtual server in Apache
--------------------------------- ---------------------------------
In order to make Szurubooru visible in your browser, you need to create a 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 virtual server. This guide focuses on `Apache` web server. Note that although
should be also possible to host szurubooru with nginx, you'd need to manually it should be also possible to host `szurubooru` with `nginx`, you'd need to
translate the rules inside public_html/.htaccess into nginx configuration. 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: need is the most basic configuration:
<VirtualHost *:80> <VirtualHost *:80>
@ -99,30 +102,34 @@ need is the most basic configuration:
DocumentRoot /path/to/szurubooru/public_html/ DocumentRoot /path/to/szurubooru/public_html/
</VirtualHost> </VirtualHost>
ServerName specifies the domain under which szurubooru will be hosted. `ServerName` specifies the domain under which `szurubooru` will be hosted.
DocumentRoot should point to the public_html/ directory. `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 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): on your setup):
LoadModule rewrite_module mod_rewrite.so ;Linux LoadModule rewrite_module mod_rewrite.so ;Linux
LoadModule rewrite_module modules/mod_rewrite.so ;Windows 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 /usr/lib/apache2/modules/libphp5.so ;Linux
LoadModule php5_module /path/to/php/php5apache2_4.dll ;Windows LoadModule php5_module /path/to/php/php5apache2_4.dll ;Windows
AddType application/x-httpd-php .php AddType application/x-httpd-php .php
PHPIniDir /path/to/php/ PHPIniDir /path/to/php/
Enable MIME auto-detection (not required, but recommended - szurubooru doesn't Enable MIME auto-detection (not required, but recommended - `szurubooru`
use file extensions, and reporting correct Content-Type to browser is always a doesn't use file extensions, and reporting correct `Content-Type` to browser is
good thing): always a good thing):
;Linux ;Linux
LoadModule mime_magic_module mod_mime_magic.so LoadModule mime_magic_module mod_mime_magic.so
@ -137,21 +144,40 @@ good thing):
</IfModule> </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 Overwriting configuration
------------------------- -------------------------
Everything that can be configured is stored in data/config.ini file. In order 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 to make changes there, copy the file and name it `local.ini` and place it in
edit the file itself, especially if you want to contribute. `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 grunt build
This should create public_html/app.min.js, public_html/app.min.css and 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, `public_html/app.min.html`. `.htaccess` is configured so that if these files
it will load them instead of development environment. To delete these exist, it will load them instead of development environment. To delete these
conveniently, you can run: conveniently, you can run:
grunt clean 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 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, connect to the server, make sure that connections are open, for example,
like this: like this:
@ -187,40 +228,28 @@ Troubleshooting
Require all granted Require all granted
</Directory> </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 Additionally, in order to access the virtual host from your machine, make
the domain name "example.com" supplied in <VirtualHost/> section is sure the domain name `example.com` supplied in `<VirtualHost/>` section is
included in your hosts file (usually /etc/hosts on Linux and included in your `hosts` file (usually `/etc/hosts` on Linux and
C:/windows/system32/drivers/etc/hosts in Windows). `C:/windows/system32/drivers/etc/hosts` on Windows).
If the site doesn't work for you, make sure Apache can parse .htaccess 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 files. If it can't, you need to set `AllowOverride` option to `yes`, for
example by putting following snippet inside <VirtualHost/> section: example by putting following snippet inside the `<VirtualHost/>` section:
<Directory /path/to/szurubooru/public_html/> <Directory /path/to/szurubooru/public_html/>
AllowOverride All AllowOverride All
</Directory> </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 Make sure your `php.ini` path is correct. Make sure all the modules are
actually loaded by inspecting phpinfo - create small file containing: actually loaded by inspecting results of `phpinfo()` call - create small
file containing:
<?php phpinfo(); ?> <?php phpinfo(); ?>
Then, run it in your browser and inspect the output, looking for missing Then, run it in your browser and inspect the output, looking for missing
modules that were supposed to be loaded. 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.

View File

@ -5,19 +5,21 @@ szurubooru
## What is it? ## What is it?
Szurubooru is a Danbooru-style board, a gallery where users can upload, browse, `szurubooru` is a Danbooru-style board, a gallery where users can upload,
tag and comment images, video clips and flash animations. browse, tag and comment images, video clips and flash animations.
Its name have its roots in Polish language and has onomatopoeic meaning of Its name have its roots in Polish language and has onomatopoeic meaning of
scraping or scrubbing. It is pronounced *"shoorubooru"* [ˌʃuruˈburu]. scraping or scrubbing. It is pronounced *"shoorubooru"* [ˌʃuruˈburu].
## Licensing ## 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 ## 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 ## Bugs and feature requests
@ -29,8 +31,8 @@ please do following:
to your problem, comment on that issue instead of opening a new one. 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. 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. 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 4. If you're filing a feature request, provide examples - what might be
to you, might not be so obvious to the developers. obvious to you, might not be so obvious to the developers.
## Contributing the code ## Contributing the code
@ -40,13 +42,14 @@ Here are some guidelines on how to contribute:
- Respect coding standards - be consistent with existing code base. - Respect coding standards - be consistent with existing code base.
- Watch your whitespace - don't leave any characters at the end of the lines. - Watch your whitespace - don't leave any characters at the end of the lines.
- Always run tests before pushing. - 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 - Use `grunt` to do automatic tasks like minifying Javascript files or running
tests. Run `grunt --help` to see full list of available tasks. tests. Run `grunt --help` to see full list of available tasks.
## API ## 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/` documentation; source code behind REST layer lies in `src/Controllers/`
directory. In order to use the API, bear in mind that you need to: directory. In order to use the API, bear in mind that you need to:

View File

@ -1,6 +1,7 @@
{ {
"require": { "require": {
"mnapoli/php-di": "~4.4" "mnapoli/php-di": "~4.4",
"phpmailer/phpmailer": "~5.2"
}, },
"require-dev": { "require-dev": {

View File

@ -3,8 +3,12 @@ serviceName = szurubooru
serviceBaseUrl = http://localhost/ serviceBaseUrl = http://localhost/
[mail] [mail]
botName = szurubooru bot smtpHost = localhost
botAddress = noreply@localhost smtpPort = 25
smtpUserName = bot
smtpUserPass = groovy123
smtpFrom = noreply@szurubooru
smtpFromName = szurubooru bot
passwordResetSubject = szurubooru - password reset passwordResetSubject = szurubooru - password reset
passwordResetBodyPath = mail/password-reset.txt passwordResetBodyPath = mail/password-reset.txt
activationSubject = szurubooru - account activation activationSubject = szurubooru - account activation
@ -19,7 +23,7 @@ maxCustomThumbnailSize = 1048576 ;1mb
[database.tests] [database.tests]
dsn = mysql:host=localhost dsn = mysql:host=localhost
user = szuru_test user = szuru-test
password = cat password = cat
[security] [security]
@ -27,12 +31,13 @@ secret = change
minPasswordLength = 5 minPasswordLength = 5
needEmailActivationToRegister = 1 needEmailActivationToRegister = 1
defaultAccessRank = restrictedUser defaultAccessRank = restrictedUser
forceHttpInPermalinks = 0
[security.privileges] [security.privileges]
register = anonymous register = anonymous
listUsers = regularUser, powerUser, moderator, administrator listUsers = regularUser, powerUser, moderator, administrator
viewUsers = regularUser, powerUser, moderator, administrator viewUsers = regularUser, powerUser, moderator, administrator
deleteOwnAccount = regularUser, powerUser, moderator, administrator deleteOwnAccount = restrictedUser, regularUser, powerUser, moderator, administrator
deleteAllAccounts = administrator deleteAllAccounts = administrator
changeOwnName = regularUser, powerUser, moderator, administrator changeOwnName = regularUser, powerUser, moderator, administrator
changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator changeOwnAvatarStyle = regularUser, powerUser, moderator, administrator
@ -94,12 +99,13 @@ usersPerPage = 20
postsPerPage = 40 postsPerPage = 40
[tags] [tags]
categories[] = meta categories[] = 'meta, meta, #aaa'
categories[] = artist categories[] = 'artist, artist, #a00'
categories[] = character categories[] = 'character, character, #0a0'
categories[] = copyright categories[] = 'copyright, copyright, #a0a'
[misc] [misc]
thumbnailCropStyle = outside thumbnailCropStyle = outside
customFaviconUrl = /favicon.png customFaviconUrl = /favicon.png
dumpSqlIntoQueries = 0 dumpSqlIntoQueries = 0
imageExtension = imagick

View File

@ -81,12 +81,13 @@ module.exports = function(grunt) {
files: [ files: [
{ src: 'node_modules/jquery/dist/jquery.min.js', dest: 'public_html/lib/jquery.min.js' }, { 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/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/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/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/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.js', dest: 'public_html/lib/nprogress.js' },
{ src: 'node_modules/nprogress/nprogress.css', dest: 'public_html/lib/nprogress.css' }, { 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), templates: readTemplates(grunt),
timestamp: grunt.template.today('isoDateTime'), timestamp: grunt.template.today('isoDateTime'),
maxPostSize: config.database.maxPostSize, maxPostSize: config.database.maxPostSize,
tagCategories: config.tags.categories, tagCategories: config.tags.categories.map(function(s) { return s.split(/,\s*/); }),
} }
}, },
dist: { dist: {
@ -162,7 +163,7 @@ module.exports = function(grunt) {
}); });
grunt.registerTask('update', 'Upgrade database to newest version.', function() { grunt.registerTask('update', 'Upgrade database to newest version.', function() {
exec('php scripts/upgrade.php'); exec('php scripts/upgrade');
}); });
grunt.registerTask('upgrade', ['update']); grunt.registerTask('upgrade', ['update']);

View File

@ -1,24 +1,25 @@
{ {
"name": "szurubooru", "name": "szurubooru",
"version": "0.9.0", "version": "1.0.3",
"private": true,
"dependencies": { "dependencies": {
"jquery.cookie": "1.4.1",
"jquery": "~2.1.1",
"underscore": "1.7.0",
"Mousetrap": "git://github.com/ccampbell/mousetrap.git", "Mousetrap": "git://github.com/ccampbell/mousetrap.git",
"marked": "~0.3.2", "font-awesome": "^4.3.0",
"nprogress": "git://github.com/rstacruz/nprogress.git",
"requirejs": "*",
"ini": "*",
"grunt": "~0.4.5", "grunt": "~0.4.5",
"grunt-processhtml": "*", "grunt-cli": "*",
"grunt-contrib-uglify": "*", "grunt-contrib-copy": "*",
"grunt-contrib-cssmin": "*", "grunt-contrib-cssmin": "*",
"grunt-contrib-jshint": "~0.10.0", "grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-copy": "*", "grunt-contrib-uglify": "*",
"grunt-cli": "*", "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", "shelljs": "~0.3.0",
"rimraf": "~2.1" "underscore": "1.7.0"
} }
} }

View File

@ -145,12 +145,6 @@
<!-- Indentation --> <!-- 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) --> <!-- Check the position of the open curly brace in a control structure (if) -->
<!-- sl = same line --> <!-- sl = same line -->
<!-- nl = new line --> <!-- nl = new line -->

View File

@ -4,6 +4,9 @@ DirectoryIndex index.html
ErrorDocument 404 /404.html ErrorDocument 404 /404.html
RewriteEngine On RewriteEngine On
RewriteBase /
RewriteCond %{HTTP_HOST} ^www\.(.*)$ [NC]
RewriteRule ^(.*)$ http://%1/$1 [R=301,L]
RewriteRule ^/?404.html$ /#/404 [NE,R,L] RewriteRule ^/?404.html$ /#/404 [NE,R,L]

View File

@ -10,27 +10,33 @@
display: none; display: none;
} }
.comments ul { ul.comments {
list-style-type: none; list-style-type: none;
margin: 1em 0; margin: 1em 0;
padding: 0; padding: 0;
} }
.comment ul {
list-style-position: inside;
margin: 1em 0;
padding: 0;
}
.comment { .comment {
margin: 0 0 1em 0; margin: 0 0 1em 0;
padding: 0; padding: 0;
display: -webkit-flex;
display: flex; display: flex;
} }
.comment .avatar { .comment .avatar {
margin-right: 0.5em; margin-top: 0.2em;
margin-right: 0.75em;
-webkit-flex-shrink: 0;
flex-shrink: 0; flex-shrink: 0;
vertical-align: top; vertical-align: top;
} }
.comment .content {
margin-top: 0.25em;
}
.comment .content p:first-child { .comment .content p:first-child {
margin-top: 0; margin-top: 0;
} }
@ -83,14 +89,18 @@
margin-bottom: 2em; margin-bottom: 2em;
} }
#global-comment-list .post-comment { #global-comment-list .post-comment {
display: -webkit-flex;
display: flex; display: flex;
} }
@media all and (max-width: 40em) { @media all and (max-width: 40em) {
#global-comment-list .post-comment { #global-comment-list .post-comment {
-webkit-flex-direction: column;
flex-direction: column; flex-direction: column;
} }
} }
#global-comment-list .post { #global-comment-list .post {
-webkit-flex-shrink: 0;
-webkit-flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
flex-grow: 0; flex-grow: 0;
margin-right: 1em; margin-right: 1em;
@ -100,6 +110,19 @@
#global-comment-list .comments>h1 { #global-comment-list .comments>h1 {
display: none; display: none;
} }
#global-comment-list .post-small a { #global-comment-list .post-small .link {
margin: 0; margin: 0;
} }
.sjis {
font-family: 'MS PGothic', ' 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;
}

View File

@ -5,13 +5,19 @@ body {
background: #fff; background: #fff;
color: #555; color: #555;
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
font-size: 17px; font-size: 15px;
overflow-y: scroll; overflow-y: scroll;
} }
@media all and (max-width: 40em) {
body {
font-size: 13px;
}
}
h1 { h1 {
font-weight: normal; font-weight: normal;
font-size: 30px; font-size: 160%;
} }
h2 { h2 {
@ -21,11 +27,11 @@ h2 {
h3 { h3 {
font-weight: normal; font-weight: normal;
font-size: 20px; font-size: 120%;
} }
small { small {
font-size: 13px; font-size: 87%;
} }
#middle { #middle {

View File

@ -40,7 +40,7 @@ input[type=password] {
box-shadow: 0 1px 2px -1px #e0e0e0 inset; box-shadow: 0 1px 2px -1px #e0e0e0 inset;
background: #fafafa; background: #fafafa;
font-family: 'Inconsolata', monospace; font-family: 'Inconsolata', monospace;
font-size: 17px; font-size: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
@ -200,7 +200,6 @@ input[type=checkbox]:focus + label {
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
margin: 1px; margin: 1px;
padding: 2px 4px; padding: 2px 4px;
font-size: 15px;
} }
.tag-input input { .tag-input input {
border: none; border: none;
@ -210,13 +209,13 @@ input[type=checkbox]:focus + label {
color: black; color: black;
} }
.tag-input li a.close { .tag-input li a.close {
font-size: 14px; font-size: 85%;
margin-left: 0.75em; margin-left: 0.5em;
} }
.related-tags { .related-tags {
line-height: 200%; line-height: 200%;
font-size: 15px; font-size: 95%;
display: none; display: none;
margin: 0.5em 0.5em 1em 0.5em; margin: 0.5em 0.5em 1em 0.5em;
} }

View File

@ -10,10 +10,11 @@
} }
#home .post { #home .post {
text-align: left; text-align: center;
margin: 0 auto; margin: 0 auto;
display: inline-block; display: inline-block;
max-width: 60%; max-width: 60%;
min-width: 40em;
} }
#home .post .left { #home .post .left {
display: inline-block; display: inline-block;

View File

@ -1,5 +1,5 @@
.message { .message {
margin: 0 auto 0.2em auto; margin: 1em auto;
padding: 0.4em 0.5em; padding: 0.4em 0.5em;
text-align: center; text-align: center;
max-width: 40em; max-width: 40em;

View File

@ -52,10 +52,16 @@
.post-list ul.safety .safety-unsafe.disabled:before { background: linear-gradient(#DDB7B7, #C9A195); } .post-list ul.safety .safety-unsafe.disabled:before { background: linear-gradient(#DDB7B7, #C9A195); }
.post-list ul.posts { .post-list ul.posts {
display: -webkit-flex;
-webkit-justify-content: center;
-webkit-align-content: center;
-webkit-flex-wrap: wrap;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-content: center; align-content: center;
flex-wrap: wrap; flex-wrap: wrap;
list-style-type: none; list-style-type: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -69,8 +75,8 @@
position: relative; position: relative;
} }
.post-small .link { .post-small .link {
display: inline-block; display: block;
margin: 0.2em; margin: 0.3em;
border: 1px solid #999; border: 1px solid #999;
z-index: 1; z-index: 1;
position: relative; position: relative;
@ -134,6 +140,7 @@
} }
.post-small:not(.post-type-image) .link::after { .post-small:not(.post-type-image) .link::after {
pointer-events: none;
display: block; display: block;
content: '...'; content: '...';
z-index: 3; z-index: 3;
@ -158,6 +165,9 @@
.post-small.post-type-flash .link::after { .post-small.post-type-flash .link::after {
content: 'flash'; content: 'flash';
} }
.post-small.post-type-animation .link::after {
content: 'anim';
}
.post-small .action { .post-small .action {
display: none; display: none;

View File

@ -14,7 +14,7 @@
position: relative; position: relative;
} }
#post-upload-step1 .url-handler .input-wrapper { #post-upload-step1 .url-handler .input-wrapper {
margin-right: 8.5em; margin-right: 9.5em;
} }
#post-upload-step1 .url-handler button { #post-upload-step1 .url-handler button {
position: absolute; position: absolute;
@ -118,14 +118,14 @@
text-align: left; text-align: left;
} }
#post-upload-step2 .messages { #post-upload-step2 .messages {
margin-bottom: 1em; margin: 1em 0;
} }
#post-upload-step2 .form-slider { #post-upload-step2 .form-slider {
text-align: center; text-align: center;
} }
#post-upload-step2 .form-slider .thumbnail img { #post-upload-step2 .form-slider .thumbnail img {
max-width: 100%; max-width: 100%;
max-height: 300px; max-height: 450px;
margin: 0 auto 1em auto; margin: 0 auto 1em auto;
} }
@ -140,35 +140,6 @@
display: none; 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 { #uploading-alert {
display: none; display: none;
text-align: left; text-align: left;

View File

@ -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 { #post-current-search-wrapper {
text-align: center; text-align: center;
} }
@ -41,45 +20,52 @@
#post-view-wrapper #sidebar { #post-view-wrapper #sidebar {
line-height: 1.33em; line-height: 1.33em;
font-size: 90%;
} }
#post-view-wrapper #sidebar h1 { #post-view-wrapper #sidebar .box {
margin-top: 1.5em; margin-bottom: 1.5em;
} text-align: left;
#post-view-wrapper #sidebar h1:first-of-type {
margin-top: 0;
} }
@media all and (min-width: 62.5em) { @media all and (min-width: 62.5em) {
#post-view-wrapper { #post-view-wrapper {
display: -webkit-flex;
display: flex; display: flex;
} }
#post-view-wrapper #sidebar { #post-view-wrapper #sidebar {
min-width: 15em; min-width: 15em;
margin-right: 1em; margin-right: 1em;
-webkit-flex: 1;
flex: 1; flex: 1;
} }
#post-view-wrapper #post-view { #post-view-wrapper #post-view {
-webkit-flex: 5;
flex: 5; flex: 5;
} }
} }
@media all and (max-width: 62.5em) { @media all and (max-width: 62.5em) {
#post-view-wrapper { #post-view-wrapper {
display: -webkit-flex;
-webkit-flex-direction: column;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#post-view-wrapper #sidebar { #post-view-wrapper #sidebar {
order: 2; order: 2;
margin-bottom: 1em; margin-bottom: 1em;
text-align: center;
}
#post-view-wrapper #sidebar .box {
display: inline-block;
width: 15em;
vertical-align: top;
} }
#post-view-wrapper #post-view { #post-view-wrapper #post-view {
margin: 0 auto; margin: 0 auto;
max-width: 100%; width: 100%;
order: 1; order: 1;
} }
} }
@ -135,11 +121,19 @@
line-height: 150%; line-height: 150%;
} }
#sidebar .fit-mode a {
opacity: .25;
}
#sidebar .fit-mode a.active {
opacity: 1;
}
#sidebar .essential { #sidebar .essential {
display: -webkit-flex;
-webkit-justify-content: space-around;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
margin-bottom: 2em; margin-bottom: 2em;
max-width: 30em;
} }
#sidebar .essential li { #sidebar .essential li {
display: block; display: block;
@ -147,12 +141,12 @@
vertical-align: top; vertical-align: top;
} }
#sidebar .essential li i.fa { #sidebar .essential li i.fa {
font-size: 30px; font-size: 200%;
} }
#sidebar .essential li a { #sidebar .essential li a {
display: block; display: block;
text-align: center; text-align: center;
font-size: 12px; font-size: 87%;
} }
#post-view #post-edit-target { #post-view #post-edit-target {
@ -172,6 +166,9 @@
z-index: -1; 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 { #post-edit-target .file-handler {
margin: 0.5em 0; margin: 0.5em 0;
} }
@ -186,6 +183,25 @@
position: relative; position: relative;
margin-bottom: 0.5em; 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 { .post-notes-target {
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
@ -225,12 +241,17 @@
} }
.post-note { .post-note {
outline: 0;
pointer-events: auto; pointer-events: auto;
position: absolute; position: absolute;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3); border: 1px solid rgba(0, 0, 0, 0.3);
font-size: 12pt; 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 { .post-note .text-wrapper {
position: absolute; position: absolute;
display: none; display: none;
@ -242,15 +263,13 @@
width: -webkit-max-content; width: -webkit-max-content;
width: -moz-max-content; width: -moz-max-content;
width: max-content; width: max-content;
max-width: 22.5em;
} }
.post-note .text { .post-note .text {
padding: 0.5em; padding: 0.5em;
background: lemonchiffon; background: lemonchiffon;
border: 1px solid black; border: 1px solid black;
} }
.post-note:hover .text-wrapper {
display: block;
}
.post-note .text p:first-of-type { .post-note .text p:first-of-type {
margin-top: 0; margin-top: 0;

View File

@ -78,19 +78,6 @@
word-break: break-all; word-break: break-all;
} }
.tag-category-character, *[class*='tag-category-']:not(.tag-category-default) a {
.tag-category-character a { color: inherit;
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;
} }

View File

@ -28,7 +28,7 @@
line-height: normal; line-height: normal;
} }
#tag-view small { #tag-view small {
font-size: 12px; font-size: 0.85em;
} }
#tag-view .siblings ul { #tag-view .siblings ul {

View File

@ -17,7 +17,7 @@
text-transform: lowercase; text-transform: lowercase;
font-variant: small-caps; font-variant: small-caps;
padding: 0.5em 1em; padding: 0.5em 1em;
font-size: 15px; font-size: 0.9em;
} }
#top-navigation li a:focus, #top-navigation li a:focus,
@ -34,7 +34,7 @@
} }
#top-navigation i { #top-navigation i {
font-size: 40px; font-size: 3em;
margin: 0 10px 5px; margin: 0 10px 5px;
} }

View File

@ -56,5 +56,5 @@
#user-list .user h1 { #user-list .user h1 {
margin-top: 0; margin-top: 0;
font-weight: normal; font-weight: normal;
font-size: 16pt; font-size: 1.25em;
} }

View File

@ -5,7 +5,7 @@
data-version="dev" data-version="dev"
data-build-time="" data-build-time=""
data-max-post-size="10485760" 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 -->
<!-- build:template <!-- build:template
<head <head
@ -15,6 +15,7 @@
data-tag-categories='<%= JSON.stringify(tagCategories).replace(/'/g, '&#039;') %>'> data-tag-categories='<%= JSON.stringify(tagCategories).replace(/'/g, '&#039;') %>'>
/build --> /build -->
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<!-- build:remove --> <!-- build:remove -->
<title>szurubooru</title> <title>szurubooru</title>
@ -23,11 +24,26 @@
<title><%= serviceName %></title> <title><%= serviceName %></title>
/build --> /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 <!-- build:template
<link rel="stylesheet" type="text/css" href="app.min.css?<%= timestamp %>"/> <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 --> /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=Droid+Sans:400,700"/>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Inconsolata"> <link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Inconsolata">

View File

@ -141,6 +141,7 @@ App.Auth = function(_, jQuery, util, api, appState, promise) {
appState.set('loginToken', response.json.token && response.json.token.name); appState.set('loginToken', response.json.token && response.json.token.name);
appState.set('loggedIn', response.json.user && !!response.json.user.id); appState.set('loggedIn', response.json.user && !!response.json.user.id);
appState.set('loggedInUser', response.json.user); appState.set('loggedInUser', response.json.user);
appState.set('config', response.json.config);
} }
function isLoggedIn(userName) { function isLoggedIn(userName) {

View File

@ -32,6 +32,9 @@ App.BrowsingSettings = function(
sketchy: true, sketchy: true,
unsafe: true, unsafe: true,
}, },
keyboardShortcuts: true,
fitMode: 'fit-width',
upscale: false,
}; };
} }
@ -90,7 +93,6 @@ App.BrowsingSettings = function(
getSettings: getSettings, getSettings: getSettings,
setSettings: setSettings, setSettings: setSettings,
}; };
}; };
App.DI.registerSingleton('browsingSettings', ['promise', 'auth', 'api'], App.BrowsingSettings); App.DI.registerSingleton('browsingSettings', ['promise', 'auth', 'api'], App.BrowsingSettings);

View File

@ -6,7 +6,9 @@ App.Controls.AutoCompleteInput = function($input) {
var jQuery = App.DI.get('jQuery'); var jQuery = App.DI.get('jQuery');
var tagList = App.DI.get('tagList'); var tagList = App.DI.get('tagList');
var KEY_TAB = 9;
var KEY_RETURN = 13; var KEY_RETURN = 13;
var KEY_DELETE = 46;
var KEY_ESCAPE = 27; var KEY_ESCAPE = 27;
var KEY_UP = 38; var KEY_UP = 38;
var KEY_DOWN = 40; var KEY_DOWN = 40;
@ -17,6 +19,7 @@ App.Controls.AutoCompleteInput = function($input) {
maxResults: 15, maxResults: 15,
minLengthToArbitrarySearch: 3, minLengthToArbitrarySearch: 3,
onApply: null, onApply: null,
onDelete: null,
onRender: null, onRender: null,
additionalFilter: null, additionalFilter: null,
}; };
@ -63,27 +66,30 @@ App.Controls.AutoCompleteInput = function($input) {
} }
$input.bind('keydown', function(e) { $input.bind('keydown', function(e) {
var func = null;
if (isShown() && e.which === KEY_ESCAPE) { if (isShown() && e.which === KEY_ESCAPE) {
e.preventDefault(); func = hide;
e.stopPropagation(); } else if (isShown() && e.which === KEY_TAB) {
e.stopImmediatePropagation(); if (e.shiftKey) {
hide(); func = selectPrevious;
} else {
func = selectNext;
}
} else if (isShown() && e.which === KEY_DOWN) { } else if (isShown() && e.which === KEY_DOWN) {
e.preventDefault(); func = selectNext;
e.stopPropagation();
e.stopImmediatePropagation();
selectNext();
} else if (isShown() && e.which === KEY_UP) { } else if (isShown() && e.which === KEY_UP) {
e.preventDefault(); func = selectPrevious;
e.stopPropagation();
e.stopImmediatePropagation();
selectPrevious();
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) { } 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.preventDefault();
e.stopPropagation(); e.stopPropagation();
e.stopImmediatePropagation(); e.stopImmediatePropagation();
applyAutocomplete(); func();
hide();
} else { } else {
window.clearTimeout(showTimeout); window.clearTimeout(showTimeout);
showTimeout = window.setTimeout(showOrHide, 250); 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() { function applyAutocomplete() {
if (options.onApply) { if (options.onApply) {
options.onApply(results[activeResult].tag); options.onApply(results[activeResult].tag);
@ -229,9 +241,15 @@ App.Controls.AutoCompleteInput = function($input) {
options.onRender($list); options.onRender($list);
} }
refreshActiveResult(); 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({ $div.css({
left: ($input.offset().left) + 'px', left: x + 'px',
top: ($input.offset().top + $input.outerHeight() - 2) + 'px', top: y + 'px',
}); });
$div.show(); $div.show();
monitorInputHiding(); monitorInputHiding();

View File

@ -31,7 +31,9 @@ App.Controls.TagInput = function($underlyingInput) {
var $wrapper = jQuery('<div class="tag-input">'); var $wrapper = jQuery('<div class="tag-input">');
var $tagList = jQuery('<ul class="tags">'); 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 $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>'); var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
init(); init();
@ -54,6 +56,7 @@ App.Controls.TagInput = function($underlyingInput) {
function render() { function render() {
$underlyingInput.hide(); $underlyingInput.hide();
$wrapper.append($tagList); $wrapper.append($tagList);
$wrapper.append($label);
$wrapper.append($input); $wrapper.append($input);
$wrapper.insertAfter($underlyingInput); $wrapper.insertAfter($underlyingInput);
$wrapper.click(function(e) { $wrapper.click(function(e) {
@ -74,6 +77,10 @@ App.Controls.TagInput = function($underlyingInput) {
function initAutoComplete() { function initAutoComplete() {
var autoComplete = new App.Controls.AutoCompleteInput($input); var autoComplete = new App.Controls.AutoCompleteInput($input);
autoComplete.onDelete = function(text) {
removeTag(text);
$input.val('');
};
autoComplete.onApply = function(text) { autoComplete.onApply = function(text) {
processText(text, SOURCE_AUTOCOMPLETION); processText(text, SOURCE_AUTOCOMPLETION);
$input.val(''); $input.val('');
@ -112,7 +119,7 @@ App.Controls.TagInput = function($underlyingInput) {
pastedText = (e.originalEvent || e).clipboardData.getData('text/plain'); pastedText = (e.originalEvent || e).clipboardData.getData('text/plain');
} }
if (pastedText.length > 200) { if (pastedText.length > 2000) {
window.alert('Pasted text is too long.'); window.alert('Pasted text is too long.');
return; return;
} }
@ -288,7 +295,7 @@ App.Controls.TagInput = function($underlyingInput) {
$elem.attr('data-tag', tagName.toLowerCase()); $elem.attr('data-tag', tagName.toLowerCase());
var $tagLink = jQuery('<a class="tag">'); var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tagName); $tagLink.text(tagName + ' ' /* for easy copying */);
$tagLink.click(function(e) { $tagLink.click(function(e) {
e.preventDefault(); e.preventDefault();
showOrHideSiblings(tagName); showOrHideSiblings(tagName);
@ -380,7 +387,7 @@ App.Controls.TagInput = function($underlyingInput) {
return promise.make(function(resolve, reject) { return promise.make(function(resolve, reject) {
promise.wait(api.get('/tags/' + tagName + '/siblings')) promise.wait(api.get('/tags/' + tagName + '/siblings'))
.then(function(response) { .then(function(response) {
resolve(response.json.data); resolve(response.json.tags);
}).fail(function() { }).fail(function() {
reject(); reject();
}); });

View File

@ -1,7 +1,8 @@
var App = App || {}; var App = App || {};
App.Keyboard = function(jQuery, mousetrap) { App.Keyboard = function(jQuery, mousetrap, browsingSettings) {
var enabled = browsingSettings.getSettings().keyboardShortcuts;
var oldStopCallback = mousetrap.stopCallback; var oldStopCallback = mousetrap.stopCallback;
mousetrap.stopCallback = function(e, element, combo, sequence) { mousetrap.stopCallback = function(e, element, combo, sequence) {
if (combo.indexOf('ctrl') === -1 && e.ctrlKey) { if (combo.indexOf('ctrl') === -1 && e.ctrlKey) {
@ -14,21 +15,31 @@ App.Keyboard = function(jQuery, mousetrap) {
return false; return false;
} }
var $focused = jQuery(':focus').eq(0); 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; return true;
} }
if ($focused.prop('tagName').toLowerCase() === 'input' &&
$focused.attr('type').match(/checkbox|radio/i)) {
return false;
}
}
return oldStopCallback.apply(mousetrap, arguments); return oldStopCallback.apply(mousetrap, arguments);
}; };
function keyup(key, callback) { function keyup(key, callback) {
unbind(key); unbind(key);
if (enabled) {
mousetrap.bind(key, callback, 'keyup'); mousetrap.bind(key, callback, 'keyup');
} }
}
function keydown(key, callback) { function keydown(key, callback) {
unbind(key); unbind(key);
if (enabled) {
mousetrap.bind(key, callback); mousetrap.bind(key, callback);
} }
}
function reset() { function reset() {
mousetrap.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);

View File

@ -71,10 +71,7 @@ App.Pager = function(
var totalRecords = response.json.totalRecords; var totalRecords = response.json.totalRecords;
totalPages = Math.ceil(totalRecords / pageSize); totalPages = Math.ceil(totalRecords / pageSize);
resolve({ resolve(response);
entities: response.json.data,
totalRecords: totalRecords,
totalPages: totalPages});
}).fail(function(response) { }).fail(function(response) {
reject(response); reject(response);

View File

@ -53,7 +53,7 @@ App.Presenters.CommentListPresenter = function(
if (comments.length === 0) { if (comments.length === 0) {
promise.wait(api.get('/comments/' + params.post.id)) promise.wait(api.get('/comments/' + params.post.id))
.then(function(response) { .then(function(response) {
comments = response.json.data; comments = response.json.comments;
render(); render();
}).fail(function() { }).fail(function() {
console.log(arguments); console.log(arguments);
@ -72,8 +72,7 @@ App.Presenters.CommentListPresenter = function(
{ {
commentListItemTemplate: templates.commentListItem, commentListItemTemplate: templates.commentListItem,
commentFormTemplate: templates.commentForm, commentFormTemplate: templates.commentForm,
formatRelativeTime: util.formatRelativeTime, util: util,
formatMarkdown: util.formatMarkdown,
comments: comments, comments: comments,
post: post, post: post,
}, },
@ -102,9 +101,7 @@ App.Presenters.CommentListPresenter = function(
function renderComment($targetList, comment) { function renderComment($targetList, comment) {
var $item = jQuery('<li>' + templates.commentListItem({ var $item = jQuery('<li>' + templates.commentListItem({
comment: comment, comment: comment,
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
formatMarkdown: util.formatMarkdown,
canVote: auth.isLoggedIn(), canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.canEditOwnComments : privileges.canEditAllComments, canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.canEditOwnComments : privileges.canEditAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.canDeleteOwnComments : privileges.canDeleteAllComments, canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.canDeleteOwnComments : privileges.canDeleteAllComments,
@ -179,7 +176,7 @@ App.Presenters.CommentListPresenter = function(
p.then(function(response) { p.then(function(response) {
$textarea.val(''); $textarea.val('');
var comment = response.json; var comment = response.json.comment;
if (commentToEdit) { if (commentToEdit) {
$form.slideUp(function() { $form.slideUp(function() {

View File

@ -38,8 +38,8 @@ App.Presenters.GlobalCommentListPresenter = function(
baseUri: '#/comments', baseUri: '#/comments',
backendUri: '/comments', backendUri: '/comments',
$target: $el.find('.pagination-target'), $target: $el.find('.pagination-target'),
updateCallback: function($page, data) { updateCallback: function($page, response) {
renderComments($page, data.entities); renderComments($page, response.json.comments);
}, },
}, },
function() { function() {
@ -53,7 +53,7 @@ App.Presenters.GlobalCommentListPresenter = function(
function reinit(params, loaded) { function reinit(params, loaded) {
pagerPresenter.reinit({query: params.query}); pagerPresenter.reinit({query: params.query || {}});
loaded(); loaded();
} }
@ -65,12 +65,11 @@ App.Presenters.GlobalCommentListPresenter = function(
$el.html(templates.list()); $el.html(templates.list());
} }
function renderComments($page, data) { function renderComments($page, postComments) {
var $target = $page.find('.posts'); var $target = $page.find('.posts');
_.each(data, function(data) { _.each(postComments, function(postComments) {
var post = data.post; var post = postComments.post;
var comments = data.comments; var comments = postComments.comments;
var $post = jQuery('<li>' + templates.listItem({ var $post = jQuery('<li>' + templates.listItem({
util: util, util: util,
post: post, post: post,

View File

@ -31,8 +31,8 @@ App.Presenters.HistoryPresenter = function(
baseUri: '#/history', baseUri: '#/history',
backendUri: '/history', backendUri: '/history',
$target: $el.find('.pagination-target'), $target: $el.find('.pagination-target'),
updateCallback: function($page, data) { updateCallback: function($page, response) {
renderHistory($page, data.entities); renderHistory($page, response.json.history);
}, },
}, },
function() { function() {
@ -62,8 +62,7 @@ App.Presenters.HistoryPresenter = function(
function renderHistory($page, historyItems) { function renderHistory($page, historyItems) {
$page.append(templates.history({ $page.append(templates.history({
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
history: historyItems})); history: historyItems}));
} }
@ -73,7 +72,6 @@ App.Presenters.HistoryPresenter = function(
deinit: deinit, deinit: deinit,
render: render, render: render,
}; };
}; };
App.DI.register('historyPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.HistoryPresenter); App.DI.register('historyPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.HistoryPresenter);

View File

@ -41,7 +41,14 @@ App.Presenters.HomePresenter = function(
if ($el.find('#post-content-target').length > 0) { if ($el.find('#post-content-target').length > 0) {
presenterManager.initPresenters([ presenterManager.initPresenters([
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}]], [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) { }).fail(function(response) {
@ -58,8 +65,7 @@ App.Presenters.HomePresenter = function(
title: topNavigationPresenter.getBaseTitle(), title: topNavigationPresenter.getBaseTitle(),
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers), canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts), canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
formatRelativeTime: util.formatRelativeTime, util: util,
formatFileSize: util.formatFileSize,
version: jQuery('head').attr('data-version'), version: jQuery('head').attr('data-version'),
buildTime: jQuery('head').attr('data-build-time'), buildTime: jQuery('head').attr('data-build-time'),
})); }));

View File

@ -62,16 +62,8 @@ App.Presenters.PagerPresenter = function(
.fail(loaded); .fail(loaded);
if (!endlessScroll) { if (!endlessScroll) {
keyboard.keydown('a', function() { keyboard.keydown(['a', 'left'], navigateToPrevPage);
if (pager.prevPage()) { keyboard.keydown(['d', 'right'], navigateToNextPage);
syncUrl({page: pager.getPage()});
}
});
keyboard.keydown('d', function() {
if (pager.nextPage()) {
syncUrl({page: pager.getPage()});
}
});
} }
} }
@ -82,11 +74,12 @@ App.Presenters.PagerPresenter = function(
function getUrl(options) { function getUrl(options) {
return util.appendComplexRouteParam( return util.appendComplexRouteParam(
baseUri, baseUri,
util.simplifySearchQuery(
_.extend( _.extend(
{}, {},
pager.getSearchParams(), pager.getSearchParams(),
{page: pager.getPage()}, {page: pager.getPage()},
options)); options)));
} }
function syncUrl(options) { function syncUrl(options) {
@ -121,7 +114,15 @@ App.Presenters.PagerPresenter = function(
updateCallback($page, response); updateCallback($page, response);
refreshPageList(); 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'); messagePresenter.showInfo($messages, 'No data to show');
if (pager.getVisiblePages().length === 1) { if (pager.getVisiblePages().length === 1) {
hidePageList(); hidePageList();
@ -132,7 +133,7 @@ App.Presenters.PagerPresenter = function(
showPageList(); showPageList();
} }
if (pager.getPage() < response.totalPages) { if (pager.getPage() < pager.getTotalPages()) {
attachNextPageLoader(); attachNextPageLoader();
} }
@ -182,13 +183,28 @@ App.Presenters.PagerPresenter = function(
$pageList.hide(); $pageList.hide();
} }
function navigateToPrevPage() {
console.log('!');
if (pager.prevPage()) {
syncUrl({page: pager.getPage()});
}
}
function navigateToNextPage() {
if (pager.nextPage()) {
syncUrl({page: pager.getPage()});
}
}
function refreshPageList() { function refreshPageList() {
var $lastItem = $pageList.find('li:last-child');
var currentPage = pager.getPage();
var pages = pager.getVisiblePages(); var pages = pager.getVisiblePages();
$pageList.empty(); $pageList.find('li.page').remove();
var lastPage = 0; var lastPage = 0;
_.each(pages, function(page) { _.each(pages, function(page) {
if (page - lastPage > 1) { if (page - lastPage > 1) {
$pageList.append(jQuery('<li><a>&hellip;</a></li>')); jQuery('<li class="page ellipsis"><a>&hellip;</a></li>').insertBefore($lastItem);
} }
lastPage = page; lastPage = page;
@ -199,12 +215,19 @@ App.Presenters.PagerPresenter = function(
}); });
$a.addClass('big-button'); $a.addClass('big-button');
$a.text(page); $a.text(page);
if (page === pager.getPage()) { if (page === currentPage) {
$a.addClass('active'); $a.addClass('active');
} }
var $li = jQuery('<li/>'); jQuery('<li class="page"/>').append($a).insertBefore($lastItem);
$li.append($a); });
$pageList.append($li);
$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();
}); });
} }

View File

@ -5,12 +5,15 @@ App.Presenters.PostContentPresenter = function(
jQuery, jQuery,
util, util,
promise, promise,
keyboard,
presenterManager, presenterManager,
postNotesPresenter) { postNotesPresenter,
browsingSettings) {
var post; var post;
var templates = {}; var templates = {};
var $target; var $target;
var $wrapper;
function init(params, loaded) { function init(params, loaded) {
$target = params.$target; $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() { function render() {
$target.html(templates.postContent({post: post})); $target.html(templates.postContent({post: post}));
$wrapper = $target.find('.object-wrapper');
if (post.contentType === 'image') { if (post.contentType === 'image' || post.contentType === 'animation') {
loadPostNotes(); loadPostNotes();
updatePostNotesSize(); updatePostNotesSize();
} }
changeFitMode({
style: browsingSettings.getSettings().fitMode,
upscale: browsingSettings.getSettings().upscale,
});
keyboard.keyup('f', cycleFitMode);
jQuery(window).resize(updatePostNotesSize); jQuery(window).resize(updatePostNotesSize);
} }
@ -45,8 +129,14 @@ App.Presenters.PostContentPresenter = function(
} }
function updatePostNotesSize() { function updatePostNotesSize() {
$target.find('.post-notes-target').width($target.find('.image-wrapper').outerWidth()); var $postNotes = $target.find('.post-notes-target');
$target.find('.post-notes-target').height($target.find('.image-wrapper').outerHeight()); 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() { function addNewPostNote() {
@ -57,14 +147,19 @@ App.Presenters.PostContentPresenter = function(
init: init, init: init,
render: render, render: render,
addNewPostNote: addNewPostNote, addNewPostNote: addNewPostNote,
updatePostNotesSize: updatePostNotesSize,
getFitMode: getFitMode,
changeFitMode: changeFitMode,
cycleFitMode: cycleFitMode,
}; };
}; };
App.DI.register('postContentPresenter', [ App.DI.register('postContentPresenter', [
'jQuery', 'jQuery',
'util', 'util',
'promise', 'promise',
'keyboard',
'presenterManager', 'presenterManager',
'postNotesPresenter'], 'postNotesPresenter',
'browsingSettings'],
App.Presenters.PostContentPresenter); App.Presenters.PostContentPresenter);

View File

@ -2,6 +2,7 @@ var App = App || {};
App.Presenters = App.Presenters || {}; App.Presenters = App.Presenters || {};
App.Presenters.PostEditPresenter = function( App.Presenters.PostEditPresenter = function(
jQuery,
util, util,
promise, promise,
api, api,
@ -46,7 +47,20 @@ App.Presenters.PostEditPresenter = function(
} }
function render() { 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 = new App.Controls.FileDropper($target.find('form [name=content]'));
postContentFileDropper.onChange = postContentChanged; postContentFileDropper.onChange = postContentChanged;
@ -63,6 +77,12 @@ App.Presenters.PostEditPresenter = function(
$target.find('form').submit(editFormSubmitted); $target.find('form').submit(editFormSubmitted);
} }
function advancedTriggerClicked(e, $advanced, $advancedTrigger) {
$advancedTrigger.hide();
$advanced.show();
e.preventDefault();
}
function focus() { function focus() {
if (tagInput) { if (tagInput) {
tagInput.focus(); tagInput.focus();
@ -89,7 +109,7 @@ App.Presenters.PostEditPresenter = function(
function editPost() { function editPost() {
var $form = $target.find('form'); var $form = $target.find('form');
var formData = new FormData(); var formData = new FormData();
formData.append('seenEditTime', post.lastEditTime); formData.append('lastEditTime', post.lastEditTime);
if (privileges.canChangeContent && postContent) { if (privileges.canChangeContent && postContent) {
formData.append('content', postContent); formData.append('content', postContent);
@ -126,11 +146,14 @@ App.Presenters.PostEditPresenter = function(
return; return;
} }
jQuery(document.activeElement).blur();
promise.wait(api.post('/posts/' + post.id, formData)) promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) { .then(function(response) {
tagList.refreshTags(); tagList.refreshTags();
post = response.json.post;
if (typeof(updateCallback) !== 'undefined') { if (typeof(updateCallback) !== 'undefined') {
updateCallback(post = response.json); updateCallback(post);
} }
}).fail(function(response) { }).fail(function(response) {
showEditError(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);

View File

@ -45,8 +45,8 @@ App.Presenters.PostListPresenter = function(
baseUri: '#/posts', baseUri: '#/posts',
backendUri: '/posts', backendUri: '/posts',
$target: $el.find('.pagination-target'), $target: $el.find('.pagination-target'),
updateCallback: function($page, data) { updateCallback: function($page, response) {
renderPosts($page, data.entities); renderPosts($page, response.json.posts);
}, },
}, },
function() { function() {
@ -217,11 +217,11 @@ App.Presenters.PostListPresenter = function(
tags.push(params.query.massTag); tags.push(params.query.massTag);
} }
var formData = {}; var formData = {};
formData.seenEditTime = post.lastEditTime; formData.lastEditTime = post.lastEditTime;
formData.tags = tags.join(' '); formData.tags = tags.join(' ');
promise.wait(api.post('/posts/' + post.id, formData)) promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) { .then(function(response) {
post = response.json; post = response.json.post;
$post.data('post', post); $post.data('post', post);
softRenderPost($post); softRenderPost($post);
}).fail(function(response) { }).fail(function(response) {

View File

@ -50,7 +50,7 @@ App.Presenters.PostNotesPresenter = function(
privileges: privileges, privileges: privileges,
post: post, post: post,
notes: notes, notes: notes,
formatMarkdown: util.formatMarkdown})); util: util}));
$form = $target.find('.post-note-edit'); $form = $target.find('.post-note-edit');
var $postNotes = $target.find('.post-note'); var $postNotes = $target.find('.post-note');
@ -61,8 +61,10 @@ App.Presenters.PostNotesPresenter = function(
$postNote.data('postNote', postNote); $postNote.data('postNote', postNote);
$postNote.find('.text-wrapper').click(postNoteClicked); $postNote.find('.text-wrapper').click(postNoteClicked);
postNote.$element = $postNote; postNote.$element = $postNote;
draggable.makeDraggable($postNote, draggable.relativeDragStrategy); draggable.makeDraggable($postNote, draggable.relativeDragStrategy, true);
resizable.makeResizable($postNote); resizable.makeResizable($postNote, true);
$postNote.mouseenter(function() { postNoteMouseEnter(postNote); });
$postNote.mouseleave(function() { postNoteMouseLeave(postNote); });
}); });
$form.find('button').click(formSubmitted); $form.find('button').click(formSubmitted);
@ -97,7 +99,10 @@ App.Presenters.PostNotesPresenter = function(
promise.wait(api.delete('/notes/' + postNote.id)) promise.wait(api.delete('/notes/' + postNote.id))
.then(function() { .then(function() {
hideForm(); hideForm();
postNote.$element.remove(); notes = jQuery.grep(notes, function(otherNote) {
return otherNote.id !== postNote.id;
});
render();
}).fail(function(response) { }).fail(function(response) {
window.alert(response.json && response.json.error || response); window.alert(response.json && response.json.error || response);
}); });
@ -125,7 +130,7 @@ App.Presenters.PostNotesPresenter = function(
promise.wait(p) promise.wait(p)
.then(function(response) { .then(function(response) {
hideForm(); hideForm();
postNote.id = response.json.id; postNote.id = response.json.note.id;
postNote.$element.data('postNote', postNote); postNote.$element.data('postNote', postNote);
render(); render();
}).fail(function(response) { }).fail(function(response) {
@ -141,13 +146,25 @@ App.Presenters.PostNotesPresenter = function(
} }
function showPostNoteText(postNote) { 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) { function hidePostNoteText(postNote) {
postNote.$element.find('.text-wrapper').css('display', ''); postNote.$element.find('.text-wrapper').css('display', '');
} }
function postNoteMouseEnter(postNote) {
showPostNoteText(postNote);
}
function postNoteMouseLeave(postNote) {
hidePostNoteText(postNote);
}
function postNoteClicked(e) { function postNoteClicked(e) {
e.preventDefault(); e.preventDefault();
var $postNote = jQuery(e.currentTarget).parents('.post-note'); var $postNote = jQuery(e.currentTarget).parents('.post-note');
@ -163,7 +180,7 @@ App.Presenters.PostNotesPresenter = function(
$form.data('postNote', postNote); $form.data('postNote', postNote);
$form.find('textarea').val(postNote.text); $form.find('textarea').val(postNote.text);
$form.show(); $form.show();
draggable.makeDraggable($form, draggable.absoluteDragStrategy); draggable.makeDraggable($form, draggable.absoluteDragStrategy, false);
} }
function hideForm() { function hideForm() {

View File

@ -4,6 +4,7 @@ App.Presenters = App.Presenters || {};
App.Presenters.PostPresenter = function( App.Presenters.PostPresenter = function(
_, _,
jQuery, jQuery,
appState,
util, util,
promise, promise,
api, api,
@ -71,7 +72,9 @@ App.Presenters.PostPresenter = function(
[postContentPresenter, {post: post, $target: $el.find('#post-content-target')}], [postContentPresenter, {post: post, $target: $el.find('#post-content-target')}],
[postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}], [postEditPresenter, {post: post, $target: $el.find('#post-edit-target'), updateCallback: postEdited}],
[commentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]], [commentListPresenter, {post: post, $target: $el.find('#post-comments-target')}]],
function() { }); function() {
syncFitModeButtons();
});
}).fail(function() { }).fail(function() {
console.log(arguments); console.log(arguments);
@ -89,25 +92,25 @@ App.Presenters.PostPresenter = function(
if (nextPostUrl) { if (nextPostUrl) {
$nextPost.addClass('enabled'); $nextPost.addClass('enabled');
$nextPost.attr('href', nextPostUrl); $nextPost.attr('href', nextPostUrl);
keyboard.keyup('a', function() { keyboard.keyup(['a', 'left'], function() {
router.navigate(nextPostUrl); router.navigate(nextPostUrl);
}); });
} else { } else {
$nextPost.removeClass('enabled'); $nextPost.removeClass('enabled');
$nextPost.removeAttr('href'); $nextPost.removeAttr('href');
keyboard.unbind('a'); keyboard.unbind(['a', 'left']);
} }
if (prevPostUrl) { if (prevPostUrl) {
$prevPost.addClass('enabled'); $prevPost.addClass('enabled');
$prevPost.attr('href', prevPostUrl); $prevPost.attr('href', prevPostUrl);
keyboard.keyup('d', function() { keyboard.keyup(['d', 'right'], function() {
router.navigate(prevPostUrl); router.navigate(prevPostUrl);
}); });
} else { } else {
$prevPost.removeClass('enabled'); $prevPost.removeClass('enabled');
$prevPost.removeAttr('href'); $prevPost.removeAttr('href');
keyboard.unbind('d'); keyboard.unbind(['d', 'right']);
} }
}).fail(function() { }).fail(function() {
}); });
@ -117,7 +120,7 @@ App.Presenters.PostPresenter = function(
return promise.make(function(resolve, reject) { return promise.make(function(resolve, reject) {
promise.wait(api.get('/posts/' + postNameOrId)) promise.wait(api.get('/posts/' + postNameOrId))
.then(function(postResponse) { .then(function(postResponse) {
post = postResponse.json; post = postResponse.json.post;
resolve(); resolve();
}).fail(function(response) { }).fail(function(response) {
showGenericError(response); showGenericError(response);
@ -135,7 +138,6 @@ App.Presenters.PostPresenter = function(
}); });
attachSidebarEvents(); attachSidebarEvents();
attachLinksToPostsAround(); attachLinksToPostsAround();
} }
@ -147,6 +149,7 @@ App.Presenters.PostPresenter = function(
function softRender() { function softRender() {
renderSidebar(); renderSidebar();
syncFitModeButtons();
$el.find('video').prop('loop', post.flags.loop); $el.find('video').prop('loop', post.flags.loop);
} }
@ -159,13 +162,12 @@ App.Presenters.PostPresenter = function(
return templates.post({ return templates.post({
query: params.query, query: params.query,
post: post, post: post,
forceHttpInPermalinks: appState.get('config').forceHttpInPermalinks,
ownScore: post.ownScore, ownScore: post.ownScore,
postFavorites: post.favorites, postFavorites: post.favorites,
postHistory: post.history, postHistory: post.history,
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
formatFileSize: util.formatFileSize,
historyTemplate: templates.history, historyTemplate: templates.history,
@ -179,6 +181,7 @@ App.Presenters.PostPresenter = function(
function attachSidebarEvents() { function attachSidebarEvents() {
$el.find('#sidebar .delete').click(deleteButtonClicked); $el.find('#sidebar .delete').click(deleteButtonClicked);
$el.find('#sidebar .feature').click(featureButtonClicked); $el.find('#sidebar .feature').click(featureButtonClicked);
$el.find('#sidebar .fit-mode a').click(fitModeButtonsClicked);
$el.find('#sidebar .edit').click(editButtonClicked); $el.find('#sidebar .edit').click(editButtonClicked);
$el.find('#sidebar .history').click(historyButtonClicked); $el.find('#sidebar .history').click(historyButtonClicked);
$el.find('#sidebar .add-favorite').click(addFavoriteButtonClicked); $el.find('#sidebar .add-favorite').click(addFavoriteButtonClicked);
@ -208,6 +211,14 @@ App.Presenters.PostPresenter = function(
}).fail(showGenericError); }).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) { function featureButtonClicked(e) {
e.preventDefault(); e.preventDefault();
messagePresenter.hideMessages($messages); 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() { function featurePost() {
promise.wait(api.post('/posts/' + post.id + '/feature')) promise.wait(api.post('/posts/' + post.id + '/feature'))
.then(function(response) { .then(function(response) {
@ -324,6 +346,7 @@ App.Presenters.PostPresenter = function(
App.DI.register('postPresenter', [ App.DI.register('postPresenter', [
'_', '_',
'jQuery', 'jQuery',
'appState',
'util', 'util',
'promise', 'promise',
'api', 'api',

View File

@ -64,6 +64,8 @@ App.Presenters.PostUploadPresenter = function(
$el.find('.remove').click(removeButtonClicked); $el.find('.remove').click(removeButtonClicked);
$el.find('.move-up').click(moveUpButtonClicked); $el.find('.move-up').click(moveUpButtonClicked);
$el.find('.move-down').click(moveDownButtonClicked); $el.find('.move-down').click(moveDownButtonClicked);
$el.find('.previous').click(selectPrevPostTableRow);
$el.find('.next').click(selectNextPostTableRow);
$el.find('.upload').click(uploadButtonClicked); $el.find('.upload').click(uploadButtonClicked);
$el.find('.stop').click(stopButtonClicked); $el.find('.stop').click(stopButtonClicked);
} }
@ -78,7 +80,7 @@ App.Presenters.PostUploadPresenter = function(
fileName: null, fileName: null,
content: null, content: null,
url: null, url: null,
thumbnail: null, getThumbnail: function() { return promise.makeSilent(function(resolve, reject) { resolve(null); }); },
$tableRow: null, $tableRow: null,
}; };
} }
@ -111,7 +113,7 @@ App.Presenters.PostUploadPresenter = function(
} }
} }
$input.val(''); $input.val('');
var post = addPostFromUrl(url); var post = addPostFromURL(url);
selectPostTableRow(post); selectPostTableRow(post);
} }
@ -137,20 +139,13 @@ App.Presenters.PostUploadPresenter = function(
allPosts.push(post); allPosts.push(post);
setAllPosts(allPosts); setAllPosts(allPosts);
createPostTableRow(post); createPostTableRow(post);
updatePostThumbnailInTable(post);
} }
function postChanged(post) { function postChanged(post) {
updatePostTableRow(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) { function postTableRowClicked(e) {
e.preventDefault(); e.preventDefault();
if (!interactionEnabled) { if (!interactionEnabled) {
@ -161,6 +156,7 @@ App.Presenters.PostUploadPresenter = function(
$allCheckboxes.prop('checked', false); $allCheckboxes.prop('checked', false);
$myCheckbox.prop('checked', true); $myCheckbox.prop('checked', true);
postTableCheckboxesChanged(e); postTableCheckboxesChanged(e);
tagInput.focus();
} }
function postTableCheckboxClicked(e) { function postTableCheckboxClicked(e) {
@ -209,24 +205,6 @@ App.Presenters.PostUploadPresenter = function(
postTableSelectionChanged(selectedPosts); 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) { function removeButtonClicked(e) {
e.preventDefault(); e.preventDefault();
removePosts(getSelectedPosts()); removePosts(getSelectedPosts());
@ -255,34 +233,75 @@ App.Presenters.PostUploadPresenter = function(
stopUpload(); stopUpload();
} }
function addPostFromFile(file) { function makeThumbnail(thumbnailWidth, thumbnailHeight, file) {
var post = _.extend({}, getDefaultPost(), {fileName: file.name, file: file}); return promise.makeSilent(function(resolve, reject) {
var canvas = document.createElement('canvas');
fileDropper.readAsDataURL(file, function(content) { var img = new Image();
if (file.type.match('image.*')) { canvas.width = thumbnailWidth;
post.thumbnail = content; canvas.height = thumbnailHeight;
postThumbnailLoaded(post); 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); postAdded(post);
return post; return post;
} }
function addPostFromUrl(url) { function addPostFromURL(url) {
var post = _.extend({}, getDefaultPost(), {url: url, fileName: url}); var post = _.extend({}, getDefaultPost(), {
postAdded(post); url: url,
setPostsSource([post], url); fileName: url,
});
var matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/); var matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/);
if (matches) { if (matches) {
var youtubeThumbnailUrl = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg'; var youtubeThumbnailURL = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
post.thumbnail = youtubeThumbnailUrl; post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
postThumbnailLoaded(post); return promise.makeSilent(function(resolve, reject) {
resolve(youtubeThumbnailURL);
});
};
} else if (url.match(/image|img|jpg|png|gif/i)) { } else if (url.match(/image|img|jpg|png|gif/i)) {
post.thumbnail = url; post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
postThumbnailLoaded(post); return promise.makeSilent(function(resolve, reject) {
resolve(url);
});
};
} }
postAdded(post);
setPostsSource([post], url);
return post; return post;
} }
@ -294,9 +313,8 @@ App.Presenters.PostUploadPresenter = function(
$row.removeClass('template'); $row.removeClass('template');
$row.find('td:not(.checkbox)').click(postTableRowClicked); $row.find('td:not(.checkbox)').click(postTableRowClicked);
$row.find('a').click(postTableRowClicked);
$row.find('td.checkbox').click(postTableCheckboxClicked); $row.find('td.checkbox').click(postTableCheckboxClicked);
$row.find('img').mouseenter(postTableRowImageHovered);
$row.find('img').mouseleave(postTableRowImageUnhovered);
$row.data('post', post); $row.data('post', post);
$table.find('tbody').append($row); $table.find('tbody').append($row);
$row.find('td.checkbox input').attr('id', _.uniqueId()); $row.find('td.checkbox input').attr('id', _.uniqueId());
@ -314,21 +332,31 @@ App.Presenters.PostUploadPresenter = function(
} }
function updatePostThumbnailInForm(post) { function updatePostThumbnailInForm(post) {
if (post.thumbnail === null) { post.getThumbnail(null, null).then(function(thumbnailDataURL) {
$el.find('.form-slider .thumbnail img').hide(); 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 { } 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) { function updatePostThumbnailInTable(post) {
post.getThumbnail(30, 30).then(function(thumbnailDataURL) {
var $row = post.$tableRow; var $row = post.$tableRow;
if (post.thumbnail === null) { if (thumbnailDataURL === null) {
$row.find('img')[0].setAttribute('src', util.transparentPixel()); $row.find('img').attr('src', util.transparentPixel());
//huge speedup thanks to this condition } else {
} else if ($row.find('img').attr('src') !== post.thumbnail) { $row.find('img').attr('src', thumbnailDataURL);
$row.find('img')[0].setAttribute('src', post.thumbnail);
} }
});
} }
function getAllPosts() { function getAllPosts() {
@ -360,6 +388,9 @@ App.Presenters.PostUploadPresenter = function(
showPostEditForm(selectedPosts); showPostEditForm(selectedPosts);
} }
$el.find('.post-table-op').prop('disabled', selectedPosts.length === 0); $el.find('.post-table-op').prop('disabled', selectedPosts.length === 0);
if (selectedPosts.length === 1) {
updatePostThumbnailInForm(selectedPosts[0]);
}
} }
function hidePostEditForm() { 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) { function getCombinedPost(posts) {
var combinedPost = _.extend({}, getDefaultPost()); var combinedPost = _.extend({}, getDefaultPost());
if (posts.length === 0) { if (posts.length === 0) {
@ -418,7 +460,7 @@ App.Presenters.PostUploadPresenter = function(
var tagFilter = function(post) { var tagFilter = function(post) {
return function(tag) { 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) { function addTagToPosts(posts, tag) {
jQuery.each(posts, function(i, post) { jQuery.each(posts, function(i, post) {
var index = post.tags.indexOf(tag); if (!hasTag(post, tag)) {
if (index === -1) {
post.tags.push(tag); post.tags.push(tag);
} }
postChanged(post); postChanged(post);
@ -475,9 +516,8 @@ App.Presenters.PostUploadPresenter = function(
function removeTagFromPosts(posts, tag) { function removeTagFromPosts(posts, tag) {
jQuery.each(posts, function(i, post) { jQuery.each(posts, function(i, post) {
var index = post.tags.indexOf(tag); if (hasTag(post, tag)) {
if (index !== -1) { post.tags.splice(getTagIndex(post, tag), 1);
post.tags.splice(index, 1);
} }
postChanged(post); postChanged(post);
}); });
@ -500,10 +540,12 @@ App.Presenters.PostUploadPresenter = function(
function selectPrevPostTableRow() { function selectPrevPostTableRow() {
selectPostTableRow($el.find('tbody tr.selected:eq(0)').prev().data('post')); selectPostTableRow($el.find('tbody tr.selected:eq(0)').prev().data('post'));
return false;
} }
function selectNextPostTableRow() { function selectNextPostTableRow() {
selectPostTableRow($el.find('tbody tr.selected:eq(0)').next().data('post')); selectPostTableRow($el.find('tbody tr.selected:eq(0)').next().data('post'));
return false;
} }
function showOrHidePostsTable() { function showOrHidePostsTable() {

View File

@ -60,7 +60,7 @@ App.Presenters.RegistrationPresenter = function(
function registrationSuccess(apiResponse) { function registrationSuccess(apiResponse) {
$el.find('form').slideUp(function() { $el.find('form').slideUp(function() {
var message = 'Registration complete! '; 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.'; message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
} else { } else {
message += '<a href="#/login">Click here</a> to login.'; message += '<a href="#/login">Click here</a> to login.';

View File

@ -36,8 +36,8 @@ App.Presenters.TagListPresenter = function(
baseUri: '#/tags', baseUri: '#/tags',
backendUri: '/tags', backendUri: '/tags',
$target: $el.find('.pagination-target'), $target: $el.find('.pagination-target'),
updateCallback: function($page, data) { updateCallback: function($page, response) {
renderTags($page, data.entities); renderTags($page, response.json.tags);
}, },
}, },
function() { function() {
@ -108,7 +108,7 @@ App.Presenters.TagListPresenter = function(
_.each(tags, function(tag) { _.each(tags, function(tag) {
var $item = jQuery(templates.listItem({ var $item = jQuery(templates.listItem({
tag: tag, tag: tag,
formatRelativeTime: util.formatRelativeTime, util: util,
})); }));
$target.append($item); $target.append($item);
}); });

View File

@ -66,9 +66,9 @@ App.Presenters.TagPresenter = function(
api.get('tags/' + tagName + '/siblings'), api.get('tags/' + tagName + '/siblings'),
api.get('posts', {query: tagName})) api.get('posts', {query: tagName}))
.then(function(tagResponse, siblingsResponse, postsResponse) { .then(function(tagResponse, siblingsResponse, postsResponse) {
tag = tagResponse.json; tag = tagResponse.json.tag;
siblings = siblingsResponse.json.data; siblings = siblingsResponse.json.tags;
posts = postsResponse.json.data; posts = postsResponse.json.posts;
posts = posts.slice(0, 8); posts = posts.slice(0, 8);
render(); 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() { function render() {
$el.html(templates.tag({ $el.html(templates.tag({
privileges: privileges, privileges: privileges,
tag: tag, tag: tag,
siblings: siblings, siblings: siblings,
tagCategories: JSON.parse(jQuery('head').attr('data-tag-categories')), tagCategories: getTagCategories(),
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
historyTemplate: templates.history, historyTemplate: templates.history,
})); }));
$el.find('.post-list').hide(); $el.find('.post-list').hide();
@ -127,7 +135,7 @@ App.Presenters.TagPresenter = function(
promise.wait(api.put('/tags/' + tag.name, formData)) promise.wait(api.put('/tags/' + tag.name, formData))
.then(function(response) { .then(function(response) {
router.navigateInplace('#/tag/' + response.json.name); router.navigateInplace('#/tag/' + response.json.tag.name);
tagList.refreshTags(); tagList.refreshTags();
}).fail(function(response) { }).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.'); window.alert(response.json && response.json.error || 'An error occured.');

View File

@ -58,7 +58,9 @@ App.Presenters.UserAccountRemovalPresenter = function(
} }
promise.wait(api.delete('/users/' + user.name)) promise.wait(api.delete('/users/' + user.name))
.then(function() { .then(function() {
if (user.name === auth.getCurrentUser().name) {
auth.logout(); auth.logout();
}
var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. <a href="">Back to main page</a>'); var $messageDiv = messagePresenter.showInfo($messages, 'Account deleted. <a href="">Back to main page</a>');
$messageDiv.find('a').click(mainPageLinkClicked); $messageDiv.find('a').click(mainPageLinkClicked);
}).fail(function(response) { }).fail(function(response) {

View File

@ -133,7 +133,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
function editSuccess(apiResponse) { function editSuccess(apiResponse) {
var wasLoggedIn = auth.isLoggedIn(user.name); var wasLoggedIn = auth.isLoggedIn(user.name);
user = apiResponse.json; user = apiResponse.json.user;
if (wasLoggedIn) { if (wasLoggedIn) {
auth.updateCurrentUser(user); auth.updateCurrentUser(user);
} }
@ -142,7 +142,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
var $messages = jQuery(target).find('.messages'); var $messages = jQuery(target).find('.messages');
var message = 'Account settings updated!'; 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.'; 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); messagePresenter.showInfo($messages, message);

View File

@ -51,6 +51,9 @@ App.Presenters.UserBrowsingSettingsPresenter = function(
sketchy: $el.find('[name=listSketchyPosts]').is(':checked'), sketchy: $el.find('[name=listSketchyPosts]').is(':checked'),
unsafe: $el.find('[name=listUnsafePosts]').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)) promise.wait(browsingSettings.setSettings(newSettings))

View File

@ -35,8 +35,8 @@ App.Presenters.UserListPresenter = function(
baseUri: '#/users', baseUri: '#/users',
backendUri: '/users', backendUri: '/users',
$target: $el.find('.pagination-target'), $target: $el.find('.pagination-target'),
updateCallback: function($page, data) { updateCallback: function($page, response) {
renderUsers($page, data.entities); renderUsers($page, response.json.users);
}, },
}, },
function() { function() {
@ -76,8 +76,7 @@ App.Presenters.UserListPresenter = function(
_.each(users, function(user) { _.each(users, function(user) {
var $item = jQuery('<li>' + templates.listItem(_.extend({ var $item = jQuery('<li>' + templates.listItem(_.extend({
user: user, user: user,
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
}, privileges)) + '</li>'); }, privileges)) + '</li>');
$target.append($item); $target.append($item);
}); });

View File

@ -41,7 +41,7 @@ App.Presenters.UserPresenter = function(
promise.wait(api.get('/users/' + userName)) promise.wait(api.get('/users/' + userName))
.then(function(response) { .then(function(response) {
user = response.json; user = response.json.user;
var extendedContext = _.extend(params, {user: user}); var extendedContext = _.extend(params, {user: user});
presenterManager.initPresenters([ presenterManager.initPresenters([
@ -74,8 +74,7 @@ App.Presenters.UserPresenter = function(
$el.html(templates.user({ $el.html(templates.user({
user: user, user: user,
isLoggedIn: auth.isLoggedIn(user.name), isLoggedIn: auth.isLoggedIn(user.name),
formatRelativeTime: util.formatRelativeTime, util: util,
formatAbsoluteTime: util.formatAbsoluteTime,
canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings, canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings,
canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()), canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()),
canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount})); canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount}));

View File

@ -2,21 +2,32 @@ var App = App || {};
App.Promise = function(_, jQuery, progress) { 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 active = [];
var promiseId = 0; var promiseId = 0;
function make(callback) { function make(callback, useProgress) {
var deferred = jQuery.Deferred(); var deferred = jQuery.Deferred();
var promise = deferred.promise(); var promise = deferred.promise();
promise.promiseId = ++ promiseId; promise.promiseId = ++ promiseId;
if (useProgress === true) {
progress.start(); progress.start();
}
callback(function() { callback(function() {
try { try {
deferred.resolve.apply(deferred, arguments); deferred.resolve.apply(deferred, arguments);
active = _.without(active, promise.promiseId); active = _.without(active, promise.promiseId);
progress.done(); progress.done();
} catch (e) { } catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset(); progress.reset();
} }
}, function() { }, function() {
@ -25,6 +36,9 @@ App.Promise = function(_, jQuery, progress) {
active = _.without(active, promise.promiseId); active = _.without(active, promise.promiseId);
progress.done(); progress.done();
} catch (e) { } catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset(); progress.reset();
} }
}); });
@ -33,7 +47,7 @@ App.Promise = function(_, jQuery, progress) {
promise.always(function() { promise.always(function() {
if (!_.contains(active, promise.promiseId)) { 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 { return {
make: make, make: function(callback) { return make(callback, true); },
makeSilent: function(callback) { return make(callback, false); },
wait: wait, wait: wait,
getActive: getActive, getActive: getActive,
abortAll: abortAll, abortAll: abortAll,

View File

@ -93,7 +93,7 @@ App.Router = function(_, jQuery, promise, util, appState, presenterManager) {
} }
function dispatch() { function dispatch() {
var url = document.location.hash; var url = decodeURI(document.location.hash);
for (var i = 0; i < routes.length; i ++) { for (var i = 0; i < routes.length; i ++) {
var route = routes[i]; var route = routes[i];
if (route.match(url)) { if (route.match(url)) {

View File

@ -15,7 +15,7 @@ App.Services.PostsAroundCalculator = function(_, promise, util, pager) {
pager.setPage(query.page); pager.setPage(query.page);
promise.wait(pager.retrieveCached()) promise.wait(pager.retrieveCached())
.then(function(response) { .then(function(response) {
var postIds = _.pluck(response.entities, 'id'); var postIds = _.pluck(response.json.posts, 'id');
var position = _.indexOf(postIds, postId); var position = _.indexOf(postIds, postId);
if (position === -1) { if (position === -1) {
@ -41,20 +41,28 @@ App.Services.PostsAroundCalculator = function(_, promise, util, pager) {
if (position + direction >= 0 && position + direction < postIds.length) { if (position + direction >= 0 && position + direction < postIds.length) {
var url = util.appendComplexRouteParam( var url = util.appendComplexRouteParam(
'#/post/' + postIds[position + direction], '#/post/' + postIds[position + direction],
_.extend({page: page}, pager.getSearchParams())); util.simplifySearchQuery(
_.extend(
{page: page},
pager.getSearchParams())));
resolve(url); resolve(url);
} else if (page + direction >= 1) { } else if (page + direction >= 1) {
pager.setPage(page + direction); pager.setPage(page + direction);
promise.wait(pager.retrieveCached()) promise.wait(pager.retrieveCached())
.then(function(response) { .then(function(response) {
if (response.entities.length) { if (response.json.posts.length) {
var post = direction === - 1 ? var post = direction === - 1 ?
_.last(response.entities) : _.last(response.json.posts) :
_.first(response.entities); _.first(response.json.posts);
var url = util.appendComplexRouteParam( var url = util.appendComplexRouteParam(
'#/post/' + post.id, '#/post/' + post.id,
_.extend({page: page + direction}, pager.getSearchParams())); util.simplifySearchQuery(
_.extend(
{page: page + direction},
pager.getSearchParams())));
resolve(url); resolve(url);
} else { } else {
resolve(null); resolve(null);

View File

@ -2,75 +2,139 @@ var App = App || {};
App.Util = App.Util || {}; App.Util = App.Util || {};
App.Util.Draggable = function(jQuery) { App.Util.Draggable = function(jQuery) {
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
function relativeDragStrategy($element) { function relativeDragStrategy($element) {
var $parent = $element.parent(); var $parent = $element.parent();
var delta; 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 { return {
click: function(e) { mouseClicked: function(e) {
delta = { delta = {
x: $element.offset().left - e.clientX, x: $element.offset().left - e.clientX,
y: $element.offset().top - e.clientY, y: $element.offset().top - e.clientY,
}; };
}, },
update: function(e) { mouseMoved: function(e) {
var x = e.clientX + delta.x - $parent.offset().left; setPosition(
var y = e.clientY + delta.y - $parent.offset().top; e.clientX + delta.x - $parent.offset().left,
x = Math.min(Math.max(x, 0), $parent.outerWidth() - $element.outerWidth()); e.clientY + delta.y - $parent.offset().top);
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 + '%'});
}, },
getPosition: getPosition,
setPosition: setPosition,
}; };
} }
function absoluteDragStrategy($element) { function absoluteDragStrategy($element) {
var delta; 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 { return {
click: function(e) { mouseClicked: function(e) {
delta = { delta = {
x: $element.position().left - e.clientX, x: $element.position().left - e.clientX,
y: $element.position().top - e.clientY, y: $element.position().top - e.clientY,
}; };
}, },
update: function(e) { mouseMoved: function(e) {
var x = e.clientX + delta.x; setPosition(e.clientX + delta.x, e.clientY + delta.y);
var y = e.clientY + delta.y;
$element.css({
left: x + 'px',
top: y + 'px'});
}, },
getPosition: getPosition,
setPosition: setPosition,
}; };
} }
function makeDraggable($element, dragStrategy) { function makeDraggable($element, dragStrategy, enableHotkeys) {
var strategy = dragStrategy($element); var strategy = dragStrategy($element);
$element.data('drag-strategy', strategy);
$element.addClass('draggable'); $element.addClass('draggable');
$element.mousedown(function(e) { $element.mousedown(function(e) {
if (e.target !== $element.get(0)) { if (e.target !== $element.get(0)) {
return; return;
} }
e.preventDefault(); e.preventDefault();
$element.focus();
$element.addClass('dragging'); $element.addClass('dragging');
strategy.click(e); strategy.mouseClicked(e);
jQuery(window).bind('mousemove.elemmove', function(e) { jQuery(window).bind('mousemove.elemmove', function(e) {
strategy.update(e); strategy.mouseMoved(e);
}).bind('mouseup.elemmove', function(e) { }).bind('mouseup.elemmove', function(e) {
e.preventDefault(); e.preventDefault();
strategy.update(e); strategy.mouseMoved(e);
$element.removeClass('dragging'); $element.removeClass('dragging');
jQuery(window).unbind('mousemove.elemmove'); jQuery(window).unbind('mousemove.elemmove');
jQuery(window).unbind('mouseup.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 { return {

View File

@ -193,11 +193,23 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
smartypants: true, smartypants: true,
}; };
var sjis = [];
var preDecorator = function(text) { 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 //prevent ^#... from being treated as headers, due to tag permalinks
text = text.replace(/^#/g, '%%%#'); text = text.replace(/^#/g, '%%%#');
//fix \ before ~ being stripped away //fix \ before ~ being stripped away
text = text.replace(/\\~/g, '%%%T'); 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; return text;
}; };
@ -206,6 +218,8 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
text = text.replace(/%%%T/g, '\\~'); text = text.replace(/%%%T/g, '\\~');
text = text.replace(/%%%#/g, '#'); text = text.replace(/%%%#/g, '#');
text = text.replace(/%%%SJIS(\d+)/, function(match, capture) { return '<div class="sjis">' + sjis[capture] + '</div>'; });
//search permalinks //search permalinks
text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>'); text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>');
//spoilers //spoilers
@ -215,12 +229,6 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
//strike-through //strike-through
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>'); text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
text = text.replace(/\\~/g, '~'); 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; return text;
}; };
@ -237,6 +245,17 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
return result.slice(0, -1); 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 { return {
promiseTemplate: promiseTemplate, promiseTemplate: promiseTemplate,
formatRelativeTime: formatRelativeTime, formatRelativeTime: formatRelativeTime,
@ -249,6 +268,7 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
transparentPixel: transparentPixel, transparentPixel: transparentPixel,
loadImagesNicely: loadImagesNicely, loadImagesNicely: loadImagesNicely,
appendComplexRouteParam: appendComplexRouteParam, appendComplexRouteParam: appendComplexRouteParam,
simplifySearchQuery: simplifySearchQuery,
}; };
}; };

View File

@ -2,41 +2,103 @@ var App = App || {};
App.Util = App.Util || {}; App.Util = App.Util || {};
App.Util.Resizable = function(jQuery) { 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 $resizer = jQuery('<div class="resizer"></div>');
var strategy = relativeResizeStrategy($element);
$element.append($resizer); $element.append($resizer);
$resizer.mousedown(function(e) { $resizer.mousedown(function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
$element.focus();
$element.addClass('resizing'); $element.addClass('resizing');
var $parent = $element.parent(); strategy.mouseClicked(e);
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 + '%'});
};
jQuery(window).bind('mousemove.elemsize', function(e) { jQuery(window).bind('mousemove.elemsize', function(e) {
update(e); strategy.mouseMoved(e);
}).bind('mouseup.elemsize', function(e) { }).bind('mouseup.elemsize', function(e) {
e.preventDefault(); e.preventDefault();
update(e); strategy.mouseMoved(e);
$element.removeClass('resizing'); $element.removeClass('resizing');
jQuery(window).unbind('mousemove.elemsize'); jQuery(window).unbind('mousemove.elemsize');
jQuery(window).unbind('mouseup.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 { return {

View File

@ -41,6 +41,50 @@
</div> </div>
</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"> <div class="form-row">
<label class="form-label"></label> <label class="form-label"></label>
<div class="form-input"> <div class="form-input">
@ -48,5 +92,3 @@
</div> </div>
</div> </div>
</form> </form>

View File

@ -27,8 +27,8 @@
<% } %> <% } %>
</span> </span>
<span class="date" title="<%= formatAbsoluteTime(comment.creationTime) %>"> <span class="date" title="<%= util.formatAbsoluteTime(comment.creationTime) %>">
<%= formatRelativeTime(comment.creationTime) %> <%= util.formatRelativeTime(comment.creationTime) %>
</span> </span>
<span class="score"> <span class="score">
@ -60,7 +60,7 @@
</div> </div>
<div class="content"> <div class="content">
<%= formatMarkdown(comment.text) %> <%= util.formatMarkdown(comment.text) %>
</div> </div>
</div> </div>
</div> </div>

View File

@ -60,10 +60,15 @@
</tr> </tr>
<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> <td>Go to newer/older page or post</td>
</tr> </tr>
<tr>
<td><code>[F]</code></td>
<td>Cycle post fit mode</td>
</tr>
<tr> <tr>
<td><code>[E]</code></td> <td><code>[E]</code></td>
<td>Edit post</td> <td>Edit post</td>
@ -109,6 +114,8 @@
{search: 'comment_count:3', description: 'having exactly three comments'}, {search: 'comment_count:3', description: 'having exactly three comments'},
{search: 'score:4', description: 'having score of 4'}, {search: 'score:4', description: 'having score of 4'},
{search: 'tag_count:7', description: 'tagged with exactly seven tags'}, {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:today', description: 'posted today'},
{search: 'date:yesterday', description: 'posted yesterday'}, {search: 'date:yesterday', description: 'posted yesterday'},
{search: 'date:2000', description: 'posted in year 2000'}, {search: 'date:2000', description: 'posted in year 2000'},
@ -116,6 +123,10 @@
{search: 'date:2000-01-01', description: 'posted on January 1st, 2000'}, {search: 'date:2000-01-01', description: 'posted on January 1st, 2000'},
{search: 'id:1', description: 'having specific post ID'}, {search: 'id:1', description: 'having specific post ID'},
{search: 'name:<em>hash</em>', description: 'having specific post name (hash in full URLs)'}, {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:image', description: 'only image posts'},
{search: 'type:flash', description: 'only Flash posts'}, {search: 'type:flash', description: 'only Flash posts'},
{search: 'type:youtube', description: 'only Youtube 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:liked', description: 'posts liked by currently logged in user'},
{search: 'special:disliked', description: 'posts disliked 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: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) { %> _.each(table, function(row) { %>
<tr> <tr>
@ -159,17 +171,22 @@
var table = [ var table = [
{search: 'order:random', description: 'as random as it can get'}, {search: 'order:random', description: 'as random as it can get'},
{search: 'order:id', description: 'highest to lowest post ID (default browse view)'}, {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:creation_date', description: 'newest to oldest (pretty much same as above)'},
{search: '-order:date', description: 'oldest to newest'}, {search: '-order:creation_date', description: 'oldest to newest'},
{search: 'order:date,asc', description: 'oldest to newest (ascending order, default = descending)'}, {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:score', description: 'highest scored'},
{search: 'order:file_size', description: 'largest files first'}, {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:tag_count', description: 'with most tags'},
{search: 'order:fav_count', description: 'loved by most'}, {search: 'order:fav_count', description: 'loved by most'},
{search: 'order:comment_count', description: 'most commented first'}, {search: 'order:comment_count', description: 'most commented first'},
{search: 'order:fav_date', description: 'recently added to favorites'}, {search: 'order:fav_date', description: 'recently added to favorites'},
{search: 'order:comment_date', description: 'recently commented'}, {search: 'order:comment_date', description: 'recently commented'},
{search: 'order:feature_date', description: 'recently featured'}, {search: 'order:feature_date', description: 'recently featured'},
{search: 'order:feature_count', description: 'most often featured'},
]; ];
_.each(table, function(row) { %> _.each(table, function(row) { %>
<tr> <tr>
@ -181,9 +198,9 @@
</table> </table>
<p>As shown with <a <p>As shown with <a
href="#/posts/query=-order:date"><code>-order:date</code></a>, any of them href="#/posts/query=-order:creation_date"><code>-order:creation_date</code></a>,
can be reversed in the same way as negating other tags: by placing a dash any of them can be reversed in the same way as negating other tags: by
before the tag.</p> placing a dash before the tag.</p>
</div> </div>
<div data-tab="comments"> <div data-tab="comments">
@ -212,6 +229,10 @@
<td><code>[spoiler]Lelouch survives[/spoiler]</td> <td><code>[spoiler]Lelouch survives[/spoiler]</td>
<td>marks text as spoiler and hides it</td> <td>marks text as spoiler and hides it</td>
</tr> </tr>
<tr>
<td><code>[sjis](´・ω・`)[/sjis]</td>
<td>adds SJIS art</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -22,8 +22,8 @@ var showDifference = function(className, difference) {
<tbody> <tbody>
<% _.each(history, function( historyEntry) { %> <% _.each(history, function( historyEntry) { %>
<tr> <tr>
<td class="time" title="<%= formatAbsoluteTime(historyEntry.time) %>"> <td class="time" title="<%= util.formatAbsoluteTime(historyEntry.time) %>">
<%= formatRelativeTime(historyEntry.time) %> <%= util.formatRelativeTime(historyEntry.time) %>
</td> </td>
<td class="user"> <td class="user">

View File

@ -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"> <div id="home">
<h1><%= title %></h1> <h1><%= title %></h1>
<p class="subheader"> <p class="subheader">
Serving <%= globals.postCount || 0 %> posts (<%= formatFileSize(globals.postSize || 0) %>) Serving <%= globals.postCount || 0 %> posts (<%= util.formatFileSize(globals.postSize || 0) %>)
</p> </p>
<% if (post && post.id) { %> <% if (post && post.id) { %>
<div class="post"> <div class="post" style="width: <%= post.imageWidth || 800 %>px">
<div id="post-content-target"> <div id="post-content-target">
</div> </div>
@ -25,29 +43,16 @@
<% } %> <% } %>
uploaded uploaded
<%= formatRelativeTime(post.uploadTime) %> <%= util.formatRelativeTime(post.creationTime) %>
by
<% showUser(post.user.name) %>
</span> </span>
<span class="right"> <span class="right">
featured featured
<%= formatRelativeTime(post.lastFeatureTime) %> <%= util.formatRelativeTime(post.lastFeatureTime) %>
by by
<% showUser(user.name) %>
<% 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>
<% } %>
</span> </span>
</div> </div>
@ -56,7 +61,7 @@
<p> <p>
<small class="version"> <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> <a href="#/history">Recent tag and post edits</a>
</small> </small>

View File

@ -2,4 +2,6 @@
</div> </div>
<ul class="page-list"> <ul class="page-list">
<li class="prev"><a href="#">Prev</a></li>
<li class="next"><a href="#">Next</a></li>
</ul> </ul>

View File

@ -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-content post-type-<%= post.contentType %>">
<div class="post-notes-target"> <div class="post-notes-target">
</div> </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 %>"/> <img alt="<%= post.name %>" src="<%= postContentUrl %>"/>
<div style="padding-top: calc(100% * <%= post.imageHeight %> / <%= post.imageWidth %>)"></div>
</div>
<% } else if (post.contentType === 'youtube') { %> <% } else if (post.contentType === 'youtube') { %>
@ -19,14 +32,15 @@
<object <object
type="<%= post.contentMimeType %>" type="<%= post.contentMimeType %>"
width="<%= post.imageWidth %>" width="<%= width %>"
height="<%= post.imageHeight %>" height="<%= height %>"
data="<%= postContentUrl %>"> data="<%= postContentUrl %>">
<param name="wmode" value="opaque"/> <param name="wmode" value="opaque"/>
<param name="movie" value="<%= postContentUrl %>"/> <param name="movie" value="<%= postContentUrl %>"/>
</object> </object>
<% } else if (post.contentType === 'video') { %> <% } else if (post.contentType === 'video') { %>
<% if (post.flags.loop) { %> <% if (post.flags.loop) { %>
<video id="video" controls loop="loop"> <video id="video" controls loop="loop">
<% } else { %> <% } else { %>
@ -40,4 +54,7 @@
<% } else { console.log(new Error('Unknown post type')) } %> <% } else { console.log(new Error('Unknown post type')) } %>
<div class="padding-fix" style="padding-bottom: calc(100% * <%= height %> / <%= width %>)"></div>
</div>
</div> </div>

View File

@ -30,8 +30,15 @@
</div> </div>
<% } %> <% } %>
<div class="form-row advanced-trigger">
<label></label>
<div class="form-input">
<a href="#">Advanced&hellip;</a>
</div>
</div>
<% if (privileges.canChangeSource) { %> <% if (privileges.canChangeSource) { %>
<div class="form-row"> <div class="form-row advanced">
<label class="form-label" for="post-source">Source:</label> <label class="form-label" for="post-source">Source:</label>
<div class="form-input"> <div class="form-input">
<input maxlength="200" type="text" name="source" id="post-source" placeholder="Where did you get this? (optional)" value="<%= post.source %>"/> <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) { %> <% if (privileges.canChangeRelations) { %>
<div class="form-row"> <div class="form-row advanced">
<label class="form-label" for="post-relations">Relations:</label> <label class="form-label" for="post-relations">Relations:</label>
<div class="form-input"> <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(' ') %>"/> <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') { %> <% if (privileges.canChangeFlags && post.contentType === 'video') { %>
<div class="form-row"> <div class="form-row advanced">
<label class="form-label">Loop:</label> <label class="form-label">Loop:</label>
<div class="form-input"> <div class="form-input">
<input type="checkbox" id="post-loop" name="loop" value="loop" <%= post.flags.loop ? 'checked="checked"' : '' %>/> <input type="checkbox" id="post-loop" name="loop" value="loop" <%= post.flags.loop ? 'checked="checked"' : '' %>/>
@ -61,7 +68,7 @@
<% } %> <% } %>
<% if (privileges.canChangeContent) { %> <% if (privileges.canChangeContent) { %>
<div class="form-row"> <div class="form-row advanced">
<label class="form-label" for="post-content">Content:</label> <label class="form-label" for="post-content">Content:</label>
<div class="form-input"> <div class="form-input">
<input type="file" id="post-content" name="content"/> <input type="file" id="post-content" name="content"/>
@ -70,7 +77,7 @@
<% } %> <% } %>
<% if (privileges.canChangeThumbnail) { %> <% if (privileges.canChangeThumbnail) { %>
<div class="form-row"> <div class="form-row advanced">
<label class="form-label" for="post-thumbnail">Thumbnail:</label> <label class="form-label" for="post-thumbnail">Thumbnail:</label>
<div class="form-input"> <div class="form-input">
<input type="file" id="post-thumbnail" name="thumbnail"/> <input type="file" id="post-thumbnail" name="thumbnail"/>

View File

@ -2,7 +2,7 @@
<% if (canViewPosts) { %> <% if (canViewPosts) { %>
<a class="link" <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(', ') %>"> title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
<% } else { %> <% } else { %>
<span class="link"> <span class="link">

View File

@ -1,6 +1,6 @@
<div class="post-notes"> <div class="post-notes">
<% _.each(notes, function(note) { %> <% _.each(notes, function(note) { %>
<div class="post-note" <div tabindex="0" class="post-note"
style="left: <%= note.left %>%; style="left: <%= note.left %>%;
top: <%= note.top %>%; top: <%= note.top %>%;
width: <%= note.width %>%; width: <%= note.width %>%;
@ -8,7 +8,7 @@
<div class="text-wrapper"> <div class="text-wrapper">
<div class="text"> <div class="text">
<%= formatMarkdown(note.text) %> <%= util.formatMarkdown(note.text) %>
</div> </div>
</div> </div>

View File

@ -39,7 +39,9 @@
<label></label> <label></label>
</td> </td>
<td class="thumbnail"> <td class="thumbnail">
<a href="#"/>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
</a>
</td> </td>
<td class="tags"></td> <td class="tags"></td>
<td class="safety"><div class="safety-template"></div></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> <button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
</li><!-- </li><!--
--><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><!--
--><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><!--
--><li> --><li>
<button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button> <button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
@ -73,6 +81,7 @@
<div class="form-slider"> <div class="form-slider">
<div class="thumbnail"> <div class="thumbnail">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
<a href="#" target="_blank">Open preview in a new tab</a>
</div> </div>
<form class="form-wrapper"> <form class="form-wrapper">
@ -134,8 +143,4 @@
</div> </div>
</div> </div>
<div id="lightbox">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Preview">
</div>
</div> </div>

View File

@ -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-wrapper">
<div id="post-current-search"> <div id="post-current-search">
@ -10,7 +19,7 @@
</div> </div>
<div class="search"> <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 || '-' %> Current search: <%= query.query || '-' %>
</a> </a>
</div> </div>
@ -32,7 +41,7 @@
<a class="download" href="<%= permaLink %>"> <a class="download" href="<%= permaLink %>">
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
<br/> <br/>
<%= post.contentExtension + ', ' + formatFileSize(post.originalFileSize) %> <%= post.contentExtension + ', ' + util.formatFileSize(post.originalFileSize) %>
</a> </a>
</li> </li>
<% } %> <% } %>
@ -72,6 +81,7 @@
<% } %> <% } %>
</ul> </ul>
<div class="box">
<h1>Tags (<%= _.size(post.tags) %>)</h1> <h1>Tags (<%= _.size(post.tags) %>)</h1>
<ul class="tags"> <ul class="tags">
<% _.each(post.tags, function(tag) { %> <% _.each(post.tags, function(tag) { %>
@ -87,9 +97,10 @@
</li> </li>
<% }) %> <% }) %>
</ul> </ul>
</div>
<div class="box">
<h1>Details</h1> <h1>Details</h1>
<div class="author-box"> <div class="author-box">
<% if (post.user.name) { %> <% if (post.user.name) { %>
<a href="#/user/<%= post.user.name %>"> <a href="#/user/<%= post.user.name %>">
@ -109,13 +120,12 @@
<br/> <br/>
<span class="date" title="<%= formatAbsoluteTime(post.uploadTime) %>"> <span class="date" title="<%= util.formatAbsoluteTime(post.creationTime) %>">
<%= formatRelativeTime(post.uploadTime) %> <%= util.formatRelativeTime(post.creationTime) %>
</span> </span>
</div> </div>
<ul class="other-info"> <ul class="other-info">
<li> <li>
Rating: Rating:
<span class="safety-<%= post.safety %>"> <span class="safety-<%= post.safety %>">
@ -126,7 +136,7 @@
<% if (post.originalFileSize) { %> <% if (post.originalFileSize) { %>
<li> <li>
File size: File size:
<%= formatFileSize(post.originalFileSize) %> <%= util.formatFileSize(post.originalFileSize) %>
</li> </li>
<% } %> <% } %>
@ -137,11 +147,11 @@
</li> </li>
<% } %> <% } %>
<% if (post.lastEditTime !== post.uploadTime) { %> <% if (post.lastEditTime !== post.creationTime) { %>
<li> <li>
Edited: Edited:
<span title="<%= formatAbsoluteTime(post.lastEditTime) %>"> <span title="<%= util.formatAbsoluteTime(post.lastEditTime) %>">
<%= formatRelativeTime(post.lastEditTime) %> <%= util.formatRelativeTime(post.lastEditTime) %>
</span> </span>
</li> </li>
<% } %> <% } %>
@ -149,7 +159,7 @@
<% if (post.featureCount > 0) { %> <% if (post.featureCount > 0) { %>
<li> <li>
Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %> Featured: <%= post.featureCount %> <%= post.featureCount < 2 ? 'time' : 'times' %>
<small>(<%= formatRelativeTime(post.lastFeatureTime) %>)</small> <small>(<%= util.formatRelativeTime(post.lastFeatureTime) %>)</small>
</li> </li>
<% } %> <% } %>
@ -187,8 +197,10 @@
<% }) %> <% }) %>
</ul> </ul>
<% } %> <% } %>
</div>
<% if (_.any(post.relations)) { %> <% if (_.any(post.relations)) { %>
<div class="box">
<h1>Related posts</h1> <h1>Related posts</h1>
<ul class="related"> <ul class="related">
<% _.each(post.relations, function(relatedPost) { %> <% _.each(post.relations, function(relatedPost) { %>
@ -199,11 +211,11 @@
</li> </li>
<% }) %> <% }) %>
</ul> </ul>
</div>
<% } %> <% } %>
<% if (_.any(privileges) || _.any(editPrivileges) || post.contentType === 'image') { %> <div class="box">
<h1>Options</h1> <h1>Options</h1>
<ul class="operations"> <ul class="operations">
<% if (_.any(editPrivileges)) { %> <% if (_.any(editPrivileges)) { %>
<li> <li>
@ -213,7 +225,7 @@
</li> </li>
<% } %> <% } %>
<% if (privileges.canAddPostNotes) { %> <% if (privileges.canAddPostNotes && (post.contentType === 'image' || post.contentType === 'animation')) { %>
<li> <li>
<a class="add-note" href="#"> <a class="add-note" href="#">
Add new note Add new note
@ -245,7 +257,7 @@
</li> </li>
<% } %> <% } %>
<% if (post.contentType === 'image') { %> <% if (post.contentType === 'image' || post.contentType === 'animation') { %>
<li> <li>
<a href="http://iqdb.org/?url=<%= permaLink %>"> <a href="http://iqdb.org/?url=<%= permaLink %>">
Search on IQDB Search on IQDB
@ -258,9 +270,16 @@
</a> </a>
</li> </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>
<div id="post-view"> <div id="post-view">
@ -277,8 +296,7 @@
<h1>History</h1> <h1>History</h1>
<%= historyTemplate({ <%= historyTemplate({
history: postHistory, history: postHistory,
formatRelativeTime: formatRelativeTime, util: util,
formatAbsoluteTime: formatAbsoluteTime,
}) %> }) %>
</div> </div>
<% } %> <% } %>

View File

@ -41,7 +41,7 @@
<div class="form-row"> <div class="form-row">
<label class="form-label" for="tag-category">Category:</label> <label class="form-label" for="tag-category">Category:</label>
<div class="form-input"> <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"' : '') %>> <input name="category" type="radio" value="<%= k %>" id="category-<%= k %>" <% print(tag.category === k ? 'checked="checked"' : '') %>>
<label for="category-<%= k %>"> <label for="category-<%= k %>">
<% print(tag.category === k ? v + ' (current)' : v) %> <% print(tag.category === k ? v + ' (current)' : v) %>
@ -103,8 +103,7 @@
<h3>History</h3> <h3>History</h3>
<%= historyTemplate({ <%= historyTemplate({
history: tag.history, history: tag.history,
formatRelativeTime: formatRelativeTime, util: util,
formatAbsoluteTime: formatAbsoluteTime,
}) %> }) %>
</div> </div>
<% } %> <% } %>

View File

@ -19,11 +19,11 @@
<%= user.name %> <%= user.name %>
<% } %> <% } %>
</h1> </h1>
<div class="date-joined" title="<%= formatAbsoluteTime(user.registrationTime) %>"> <div class="date-joined" title="<%= util.formatAbsoluteTime(user.creationTime) %>">
Joined: <%= formatRelativeTime(user.registrationTime) %> Joined: <%= util.formatRelativeTime(user.creationTime) %>
</div> </div>
<div class="date-seen" title="<%= formatAbsoluteTime(user.lastLoginTime) %>"> <div class="date-seen" title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
Last seen: <%= formatRelativeTime(user.lastLoginTime) %> Last seen: <%= util.formatRelativeTime(user.lastLoginTime) %>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,10 +7,10 @@
<a class="big-button" href="#/users/order=name,desc">Sort Z&rarr;A</a> <a class="big-button" href="#/users/order=name,desc">Sort Z&rarr;A</a>
</li> </li>
<li> <li>
<a class="big-button" href="#/users/order=registration_time,asc">Sort old&rarr;new</a> <a class="big-button" href="#/users/order=creation_time,asc">Sort old&rarr;new</a>
</li> </li>
<li> <li>
<a class="big-button" href="#/users/order=registration_time,desc">Sort new&rarr;old</a> <a class="big-button" href="#/users/order=creation_time,desc">Sort new&rarr;old</a>
</li> </li>
</ul> </ul>

View File

@ -51,15 +51,15 @@
<table> <table>
<tr> <tr>
<td>Registered:</td> <td>Registered:</td>
<td title="<%= formatAbsoluteTime(user.registrationTime) %>"> <td title="<%= util.formatAbsoluteTime(user.creationTime) %>">
<%= formatRelativeTime(user.registrationTime) %> <%= util.formatRelativeTime(user.creationTime) %>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Seen:</td> <td>Seen:</td>
<td title="<%= formatAbsoluteTime(user.lastLoginTime) %>"> <td title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
<%= formatRelativeTime(user.lastLoginTime) %> <%= util.formatRelativeTime(user.lastLoginTime) %>
</td> </td>
</tr> </tr>

1
scripts/cron-globals.php → scripts/cron-globals Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/php
<?php <?php
require_once(__DIR__ require_once(__DIR__
. DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..'

43
scripts/cron-stats Executable file
View 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
View 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
View 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
View 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
View File

@ -1,3 +1,4 @@
#!/usr/bin/php
<?php <?php
require_once(__DIR__ require_once(__DIR__
. DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..'

35
scripts/upgrade Executable file
View 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();

View File

@ -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();

View File

@ -64,8 +64,7 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
public function findAll() public function findAll()
{ {
$query = $this->pdo->from($this->tableName); $query = $this->pdo->from($this->tableName);
$arrayEntities = iterator_to_array($query); return $this->arrayToEntities($query);
return $this->arrayToEntities($arrayEntities);
} }
public function findById($entityId) public function findById($entityId)
@ -248,7 +247,7 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
$query->where($sql, $bindings); $query->where($sql, $bindings);
} }
protected function arrayToEntities(array $arrayEntities, $entityConverter = null) protected function arrayToEntities($arrayEntities, $entityConverter = null)
{ {
if ($entityConverter === null) if ($entityConverter === null)
$entityConverter = $this->entityConverter; $entityConverter = $this->entityConverter;

View File

@ -11,7 +11,7 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
[ [
'name' => $entity->getName(), 'name' => $entity->getName(),
'userId' => $entity->getUserId(), 'userId' => $entity->getUserId(),
'uploadTime' => $this->entityTimeToDbTime($entity->getUploadTime()), 'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
'lastEditTime' => $this->entityTimeToDbTime($entity->getLastEditTime()), 'lastEditTime' => $this->entityTimeToDbTime($entity->getLastEditTime()),
'safety' => $entity->getSafety(), 'safety' => $entity->getSafety(),
'contentType' => $entity->getContentType(), 'contentType' => $entity->getContentType(),
@ -33,7 +33,7 @@ class PostEntityConverter extends AbstractEntityConverter implements IEntityConv
$entity = new Post(intval($array['id'])); $entity = new Post(intval($array['id']));
$entity->setName($array['name']); $entity->setName($array['name']);
$entity->setUserId($array['userId']); $entity->setUserId($array['userId']);
$entity->setUploadTime($this->dbTimeToEntityTime($array['uploadTime'])); $entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime'])); $entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
$entity->setSafety(intval($array['safety'])); $entity->setSafety(intval($array['safety']));
$entity->setContentType(intval($array['contentType'])); $entity->setContentType(intval($array['contentType']));

View File

@ -11,6 +11,7 @@ class TagEntityConverter extends AbstractEntityConverter implements IEntityConve
[ [
'name' => $entity->getName(), 'name' => $entity->getName(),
'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()), 'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
'lastEditTime' => $this->entityTimeToDbTime($entity->getLastEditTime()),
'banned' => intval($entity->isBanned()), 'banned' => intval($entity->isBanned()),
'category' => $entity->getCategory(), 'category' => $entity->getCategory(),
]; ];
@ -21,6 +22,7 @@ class TagEntityConverter extends AbstractEntityConverter implements IEntityConve
$entity = new Tag(intval($array['id'])); $entity = new Tag(intval($array['id']));
$entity->setName($array['name']); $entity->setName($array['name']);
$entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime'])); $entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
$entity->setLastEditTime($this->dbTimeToEntityTime($array['lastEditTime']));
$entity->setMeta(Tag::META_USAGES, intval($array['usages'])); $entity->setMeta(Tag::META_USAGES, intval($array['usages']));
$entity->setBanned($array['banned']); $entity->setBanned($array['banned']);
$entity->setCategory($array['category']); $entity->setCategory($array['category']);

View File

@ -15,7 +15,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
'passwordHash' => $entity->getPasswordHash(), 'passwordHash' => $entity->getPasswordHash(),
'passwordSalt' => $entity->getPasswordSalt(), 'passwordSalt' => $entity->getPasswordSalt(),
'accessRank' => $entity->getAccessRank(), 'accessRank' => $entity->getAccessRank(),
'registrationTime' => $this->entityTimeToDbTime($entity->getRegistrationTime()), 'creationTime' => $this->entityTimeToDbTime($entity->getCreationTime()),
'lastLoginTime' => $this->entityTimeToDbTime($entity->getLastLoginTime()), 'lastLoginTime' => $this->entityTimeToDbTime($entity->getLastLoginTime()),
'avatarStyle' => $entity->getAvatarStyle(), 'avatarStyle' => $entity->getAvatarStyle(),
'browsingSettings' => json_encode($entity->getBrowsingSettings()), 'browsingSettings' => json_encode($entity->getBrowsingSettings()),
@ -33,7 +33,7 @@ class UserEntityConverter extends AbstractEntityConverter implements IEntityConv
$entity->setPasswordHash($array['passwordHash']); $entity->setPasswordHash($array['passwordHash']);
$entity->setPasswordSalt($array['passwordSalt']); $entity->setPasswordSalt($array['passwordSalt']);
$entity->setAccessRank(intval($array['accessRank'])); $entity->setAccessRank(intval($array['accessRank']));
$entity->setRegistrationTime($this->dbTimeToEntityTime($array['registrationTime'])); $entity->setCreationTime($this->dbTimeToEntityTime($array['creationTime']));
$entity->setLastLoginTime($this->dbTimeToEntityTime($array['lastLoginTime'])); $entity->setLastLoginTime($this->dbTimeToEntityTime($array['lastLoginTime']));
$entity->setAvatarStyle(intval($array['avatarStyle'])); $entity->setAvatarStyle(intval($array['avatarStyle']));
$entity->setBrowsingSettings(json_decode($array['browsingSettings'])); $entity->setBrowsingSettings(json_decode($array['browsingSettings']));

View File

@ -1,6 +1,8 @@
<?php <?php
namespace Szurubooru\Dao; namespace Szurubooru\Dao;
use Szurubooru\Dao\EntityConverters\FavoriteEntityConverter; use Szurubooru\Dao\EntityConverters\FavoriteEntityConverter;
use Szurubooru\Dao\PostDao;
use Szurubooru\Dao\UserDao;
use Szurubooru\DatabaseConnection; use Szurubooru\DatabaseConnection;
use Szurubooru\Entities\Entity; use Szurubooru\Entities\Entity;
use Szurubooru\Entities\Favorite; use Szurubooru\Entities\Favorite;
@ -10,10 +12,14 @@ use Szurubooru\Services\TimeService;
class FavoritesDao extends AbstractDao implements ICrudDao class FavoritesDao extends AbstractDao implements ICrudDao
{ {
private $userDao;
private $postDao;
private $timeService; private $timeService;
public function __construct( public function __construct(
DatabaseConnection $databaseConnection, DatabaseConnection $databaseConnection,
UserDao $userDao,
PostDao $postDao,
TimeService $timeService) TimeService $timeService)
{ {
parent::__construct( parent::__construct(
@ -21,6 +27,8 @@ class FavoritesDao extends AbstractDao implements ICrudDao
'favorites', 'favorites',
new FavoriteEntityConverter()); new FavoriteEntityConverter());
$this->userDao = $userDao;
$this->postDao = $postDao;
$this->timeService = $timeService; $this->timeService = $timeService;
} }
@ -58,6 +66,23 @@ class FavoritesDao extends AbstractDao implements ICrudDao
$this->deleteById($favorite->getId()); $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) private function get(User $user, Entity $entity)
{ {
$query = $this->pdo->from($this->tableName)->where('userId', $user->getId()); $query = $this->pdo->from($this->tableName)->where('userId', $user->getId());

View File

@ -44,10 +44,53 @@ class FileDao implements IFileDao
return $this->directory . DIRECTORY_SEPARATOR . $fileName; 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) private function createFolders($fileName)
{ {
$fullPath = dirname($this->getFullPath($fileName)); $fullPath = dirname($this->getFullPath($fileName));
if (!file_exists($fullPath)) if (!file_exists($fullPath))
mkdir($fullPath, 0777, true); 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);
}
} }

View File

@ -149,7 +149,7 @@ class PostDao extends AbstractDao implements ICrudDao
return; return;
} }
elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT) elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT_AUTHOR)
{ {
foreach ($requirement->getValue()->getValues() as $userName) foreach ($requirement->getValue()->getValues() as $userName)
{ {
@ -194,6 +194,17 @@ class PostDao extends AbstractDao implements ICrudDao
return; 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); parent::decorateQueryFromRequirement($query, $requirement);
} }

View File

@ -10,7 +10,7 @@ use Szurubooru\Services\ThumbnailService;
class UserDao extends AbstractDao implements ICrudDao class UserDao extends AbstractDao implements ICrudDao
{ {
const ORDER_NAME = 'name'; const ORDER_NAME = 'name';
const ORDER_REGISTRATION_TIME = 'registrationTime'; const ORDER_CREATION_TIME = 'creationTime';
private $fileDao; private $fileDao;
private $thumbnailService; private $thumbnailService;

View File

@ -65,9 +65,12 @@ final class Dispatcher
$json['__statements'] = $this->databaseConnection->getPDO()->getStatements(); $json['__statements'] = $this->databaseConnection->getPDO()->getStatements();
} }
if (!$this->httpHelper->isRedirecting())
{
$this->httpHelper->setResponseCode($code); $this->httpHelper->setResponseCode($code);
$this->httpHelper->setHeader('Content-Type', 'application/json'); $this->httpHelper->setHeader('Content-Type', 'application/json');
$this->httpHelper->outputJSON($json); $this->httpHelper->outputJSON($json);
}
return $json; return $json;
} }

View File

@ -12,6 +12,7 @@ final class Post extends Entity
const POST_TYPE_FLASH = 2; const POST_TYPE_FLASH = 2;
const POST_TYPE_VIDEO = 3; const POST_TYPE_VIDEO = 3;
const POST_TYPE_YOUTUBE = 4; const POST_TYPE_YOUTUBE = 4;
const POST_TYPE_ANIMATED_IMAGE = 5;
const FLAG_LOOP = 1; const FLAG_LOOP = 1;
@ -28,7 +29,7 @@ final class Post extends Entity
private $name; private $name;
private $userId; private $userId;
private $uploadTime; private $creationTime;
private $lastEditTime; private $lastEditTime;
private $safety; private $safety;
private $contentType; private $contentType;
@ -78,14 +79,14 @@ final class Post extends Entity
$this->safety = $safety; $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() public function getLastEditTime()

View File

@ -5,6 +5,7 @@ final class Tag extends Entity
{ {
private $name; private $name;
private $creationTime; private $creationTime;
private $lastEditTime;
private $banned = false; private $banned = false;
private $category = 'default'; private $category = 'default';
@ -33,6 +34,16 @@ final class Tag extends Entity
$this->creationTime = $creationTime; $this->creationTime = $creationTime;
} }
public function getLastEditTime()
{
return $this->lastEditTime;
}
public function setLastEditTime($lastEditTime)
{
$this->lastEditTime = $lastEditTime;
}
public function isBanned() public function isBanned()
{ {
return $this->banned; return $this->banned;

View File

@ -23,7 +23,7 @@ final class User extends Entity
private $passwordHash; private $passwordHash;
private $passwordSalt; private $passwordSalt;
private $accessRank; private $accessRank;
private $registrationTime; private $creationTime;
private $lastLoginTime; private $lastLoginTime;
private $avatarStyle; private $avatarStyle;
private $browsingSettings; private $browsingSettings;
@ -110,14 +110,14 @@ final class User extends Entity
$this->accessRank = $accessRank; $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() public function getLastLoginTime()

View File

@ -14,7 +14,7 @@ class PostEditFormData implements IValidatable
public $relations; public $relations;
public $flags; public $flags;
public $seenEditTime; public $lastEditTime;
public function __construct($inputReader = null) public function __construct($inputReader = null)
{ {
@ -29,7 +29,7 @@ class PostEditFormData implements IValidatable
$this->tags = preg_split('/[\s+]/', $inputReader->tags); $this->tags = preg_split('/[\s+]/', $inputReader->tags);
if ($inputReader->relations !== null) if ($inputReader->relations !== null)
$this->relations = array_filter(preg_split('/[\s+]/', $inputReader->relations)); $this->relations = array_filter(preg_split('/[\s+]/', $inputReader->relations));
$this->seenEditTime = $inputReader->seenEditTime; $this->lastEditTime = $inputReader->lastEditTime;
$this->flags = new \StdClass; $this->flags = new \StdClass;
$this->flags->loop = !empty($inputReader->loop); $this->flags->loop = !empty($inputReader->loop);
} }

View File

@ -41,4 +41,3 @@ class TagEditFormData implements IValidatable
$validator->validatePostTags($this->suggestions); $validator->validatePostTags($this->suggestions);
} }
} }

View File

@ -39,4 +39,3 @@ class UploadFormData implements IValidatable
$validator->validatePostSource($this->source); $validator->validatePostSource($this->source);
} }
} }

View File

@ -37,6 +37,7 @@ class EnumHelper
'video' => Post::POST_TYPE_VIDEO, 'video' => Post::POST_TYPE_VIDEO,
'flash' => Post::POST_TYPE_FLASH, 'flash' => Post::POST_TYPE_FLASH,
'youtube' => Post::POST_TYPE_YOUTUBE, 'youtube' => Post::POST_TYPE_YOUTUBE,
'animation' => Post::POST_TYPE_ANIMATED_IMAGE,
]; ];
private static $snapshotTypeMap = private static $snapshotTypeMap =
@ -103,7 +104,12 @@ class EnumHelper
$key = trim(strtolower($enumString)); $key = trim(strtolower($enumString));
$lowerEnumMap = array_change_key_case($enumMap, \CASE_LOWER); $lowerEnumMap = array_change_key_case($enumMap, \CASE_LOWER);
if (!isset($lowerEnumMap[$key])) 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]; return $lowerEnumMap[$key];
} }

View File

@ -3,6 +3,8 @@ namespace Szurubooru\Helpers;
class HttpHelper class HttpHelper
{ {
private $redirected = false;
public function setResponseCode($code) public function setResponseCode($code)
{ {
http_response_code($code); http_response_code($code);
@ -29,9 +31,26 @@ class HttpHelper
} }
public function getRequestHeaders() public function getRequestHeaders()
{
if (function_exists('getallheaders'))
{ {
return 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) public function getRequestHeader($key)
{ {
@ -50,4 +69,23 @@ class HttpHelper
$requestUri = preg_replace('/\?.*$/', '', $requestUri); $requestUri = preg_replace('/\?.*$/', '', $requestUri);
return $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;
}
} }

View File

@ -30,6 +30,9 @@ final class InputReader extends \ArrayObject
if (!isset($_FILES[$fileName])) if (!isset($_FILES[$fileName]))
return null; return null;
if (!$_FILES[$fileName]['tmp_name'])
throw new \Exception('File is probably too big.');
return file_get_contents($_FILES[$fileName]['tmp_name']); return file_get_contents($_FILES[$fileName]['tmp_name']);
} }
} }

View File

@ -18,6 +18,12 @@ class MimeHelper
return self::getMimeTypeFrom16Bytes(substr($buffer, 0, 16)); 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) public static function isFlash($mime)
{ {
return strtolower($mime) === 'application/x-shockwave-flash'; return strtolower($mime) === 'application/x-shockwave-flash';

View File

@ -3,8 +3,8 @@ namespace Szurubooru;
class NotSupportedException extends \BadMethodCallException class NotSupportedException extends \BadMethodCallException
{ {
public function __construct() public function __construct($message = null)
{ {
parent::__construct('Not supported'); parent::__construct($message === null ? 'Not supported' : $message);
} }
} }

View File

@ -49,6 +49,6 @@ class AddComment extends AbstractCommentRoute
$post = $this->postService->getByNameOrId($args['postNameOrId']); $post = $this->postService->getByNameOrId($args['postNameOrId']);
$comment = $this->commentService->createComment($post, $this->inputReader->text); $comment = $this->commentService->createComment($post, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig()); return ['comment' => $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig())];
} }
} }

View File

@ -52,6 +52,6 @@ class DeleteComment extends AbstractCommentRoute
? Privilege::DELETE_OWN_COMMENTS ? Privilege::DELETE_OWN_COMMENTS
: Privilege::DELETE_ALL_COMMENTS); : Privilege::DELETE_ALL_COMMENTS);
return $this->commentService->deleteComment($comment); $this->commentService->deleteComment($comment);
} }
} }

View File

@ -53,6 +53,6 @@ class EditComment extends AbstractCommentRoute
: Privilege::EDIT_ALL_COMMENTS); : Privilege::EDIT_ALL_COMMENTS);
$comment = $this->commentService->updateComment($comment, $this->inputReader->text); $comment = $this->commentService->updateComment($comment, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig()); return ['comment' => $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig())];
} }
} }

View File

@ -80,7 +80,7 @@ class GetComments extends AbstractCommentRoute
} }
return [ return [
'data' => $data, 'comments' => $data,
'pageSize' => $result->getPageSize(), 'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()]; 'totalRecords' => $result->getTotalRecords()];
} }

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