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
-------------
In order to run szurubooru, you need to have installed following software:
In order to run `szurubooru`, you need to have installed following software:
- Apache2
- mod_rewrite
- mod_mime_magic (recommended)
- PHP 5.6.0
- pdo_sqlite
- imagick or gd
- composer (PHP package manager)
- npm (node.js package manager)
- `Apache` 2.4+
- `mod_rewrite`
- `mod_mime_magic` (recommended)
- `PHP` 5.6.0+
- `pdo_mysql`
- `imagick` or `gd`
- `MySQL` or `MariaDB`
- `composer` (`PHP` package manager)
- `npm` (`node.js` package manager)
Optional modules:
Optional software:
- dump-gnash or swfrender for flash thumbnails
- ffmpegthumbnailer or ffmpeg for video thumbnails
- `dump-gnash`, `swfrender` or `ffmpeg` for Flash thumbnails
- `ffmpegthumbnailer` or `ffmpeg` for video thumbnails
Cloning the repository
----------------------
Download the repository somewhere you will it run from, or better yet, clone it
with `git`:
cd /srv/www/
git clone https://github.com/rr-/szurubooru booru-test
Fetching dependencies
---------------------
To fetch dependencies that szurubooru needs in order to run, enter following
To fetch dependencies that `szurubooru` needs in order to run, enter following
commands in the terminal:
composer update
@ -30,11 +42,11 @@ commands in the terminal:
Running grunt tasks
-------------------
Running `grunt` tasks
---------------------
Szurubooru uses grunt to run tasks like database ugprades and tests. In order
to use grunt from the terminal, you can use:
`szurubooru` uses `grunt` to run tasks like database upgrades and tests. In
order to use `grunt` from the terminal, you can use:
node_modules/grunt-cli/bin/grunt [TASK]
@ -43,25 +55,25 @@ administrator:
npm install -g grunt-cli
This will add "grunt" to your PATH, making things much more human-friendly.
This will add `grunt` to your PATH, making things much more human-friendly.
grunt [TASK]
Enabling required modules in PHP
--------------------------------
Enabling required modules in `PHP`
----------------------------------
Enable required modules in php.ini (or other configuration file, depending on
Enable required modules in `php.ini` (or other configuration file, depending on
your setup):
;Linux
extension=pdo_sqlite.so
extension=pdo_mysql.so
;Windows
extension=php_pdo_sqlite.dll
extension=php_pdo_mysql.dll
In order to draw thumbnails, szurubooru needs either imagick or gd2:
In order to draw thumbnails, `szurubooru` needs either `Imagick` or `gd2`:
;Linux
extension=imagick.so
@ -73,25 +85,16 @@ In order to draw thumbnails, szurubooru needs either imagick or gd2:
Upgrading the database
----------------------
Every time database schema changes, you should upgrade the database by running
following grunt task in the terminal:
grunt upgrade
Creating virtual server in Apache
---------------------------------
In order to make Szurubooru visible in your browser, you need to create a
virtual server. This guide focuses on Apache2 web server. Note that although it
should be also possible to host szurubooru with nginx, you'd need to manually
translate the rules inside public_html/.htaccess into nginx configuration.
In order to make `szurubooru` visible in your browser, you need to create a
virtual server. This guide focuses on `Apache` web server. Note that although
it should be also possible to host `szurubooru` with `nginx`, you'd need to
manually translate the rules inside `public_html/.htaccess` into `nginx`
configuration.
Creating virtual server for Apache comes with no surprises, basically all you
Creating virtual server for `Apache` comes with no surprises, basically all you
need is the most basic configuration:
<VirtualHost *:80>
@ -99,30 +102,34 @@ need is the most basic configuration:
DocumentRoot /path/to/szurubooru/public_html/
</VirtualHost>
ServerName specifies the domain under which szurubooru will be hosted.
DocumentRoot should point to the public_html/ directory.
`ServerName` specifies the domain under which `szurubooru` will be hosted.
`DocumentRoot` should point to the `public_html/` directory.
Some environments / configurations require extra steps to make things work - in
case you experience any problems, please consult the troubleshooting section
later in this file.
Enabling required modules in Apache
-----------------------------------
Enable required modules in httpd.conf (or other configuration file, depending
Enable required modules in `httpd.conf` (or other configuration file, depending
on your setup):
LoadModule rewrite_module mod_rewrite.so ;Linux
LoadModule rewrite_module modules/mod_rewrite.so ;Windows
Enable PHP support:
Enable `PHP` support:
LoadModule php5_module /usr/lib/apache2/modules/libphp5.so ;Linux
LoadModule php5_module /path/to/php/php5apache2_4.dll ;Windows
AddType application/x-httpd-php .php
PHPIniDir /path/to/php/
Enable MIME auto-detection (not required, but recommended - szurubooru doesn't
use file extensions, and reporting correct Content-Type to browser is always a
good thing):
Enable MIME auto-detection (not required, but recommended - `szurubooru`
doesn't use file extensions, and reporting correct `Content-Type` to browser is
always a good thing):
;Linux
LoadModule mime_magic_module mod_mime_magic.so
@ -137,21 +144,40 @@ good thing):
</IfModule>
Creating administrator account
------------------------------
By now, you should be able to view szurubooru in the browser. Registering
administrator account is simple - the first user to create an account
automatically becomes administrator and doesn't need e-mail activation.
Overwriting configuration
-------------------------
Everything that can be configured is stored in data/config.ini file. In order
to make changes there, copy the file and name it local.ini. Make sure you don't
edit the file itself, especially if you want to contribute.
Everything that can be configured is stored in `data/config.ini` file. In order
to make changes there, copy the file and name it `local.ini` and place it in
`data/` directory as well. Make sure you don't edit the `data/config.ini` file
itself, especially if you want to contribute.
Setting up the database
-----------------------
Before running `szurubooru` for first time, you need to set up the database.
`szurubooru` uses MySQL, so let's fire `mysql` and type following:
create user 'maria' identified by 'arkadia';
create database booru_test;
grant all privileges on *.* to 'maria'@'%' with grant option;
Then you need to provide the above credentials in the configuration files as
described in the previous section. Example `local.ini` file:
[database]
dsn = mysql:dbname=booru_test
user = maria
password = arkadia
After that, upgrade the database using following command:
grunt upgrade
This should be also executed every time database schema changes.
@ -165,21 +191,36 @@ smallest possible packages, run following command:
grunt build
This should create public_html/app.min.js, public_html/app.min.css and
public_html/app.min.html. .htaccess is configured so that if these files exist,
it will load them instead of development environment. To delete these
This should create `public_html/app.min.js`, `public_html/app.min.css` and
`public_html/app.min.html`. `.htaccess` is configured so that if these files
exist, it will load them instead of development environment. To delete these
conveniently, you can run:
grunt clean
If, for any reason, you do not wish to minify the resources, you should at
least copy the dependencies fetched before to the `public_html/` directory with
following:
grunt copy
Creating administrator account
------------------------------
By now, you should be able to view `szurubooru` in the browser. Registering
administrator account is simple - the first user to create an account
automatically becomes administrator and doesn't need e-mail activation.
Troubleshooting
---------------
1. Problems with Apache virtual servers
1. Problems with `Apache` virtual servers
After reloading Apache configuration, if you find yourself unable to
After reloading `Apache` configuration, if you find yourself unable to
connect to the server, make sure that connections are open, for example,
like this:
@ -187,40 +228,28 @@ Troubleshooting
Require all granted
</Directory>
(Note that Apache versions prior to 2.4 used "Allow from all" directive.)
(Note that `Apache` versions prior to 2.4 used `Allow from all` directive.)
Additionally, in order to access virtual host from your machine, make sure
the domain name "example.com" supplied in <VirtualHost/> section is
included in your hosts file (usually /etc/hosts on Linux and
C:/windows/system32/drivers/etc/hosts in Windows).
Additionally, in order to access the virtual host from your machine, make
sure the domain name `example.com` supplied in `<VirtualHost/>` section is
included in your `hosts` file (usually `/etc/hosts` on Linux and
`C:/windows/system32/drivers/etc/hosts` on Windows).
If the site doesn't work for you, make sure Apache can parse .htaccess
files. If it can't, you need to set AllowOverride option to "yes", for
example by putting following snippet inside <VirtualHost/> section:
If the site doesn't work for you, make sure `Apache` can parse `.htaccess`
files. If it can't, you need to set `AllowOverride` option to `yes`, for
example by putting following snippet inside the `<VirtualHost/>` section:
<Directory /path/to/szurubooru/public_html/>
AllowOverride All
</Directory>
2. Problems with PHP modules or registration
2. Problems with `PHP` modules or registration
Make sure your php.ini path is correct. Make sure all the modules are
actually loaded by inspecting phpinfo - create small file containing:
Make sure your `php.ini` path is correct. Make sure all the modules are
actually loaded by inspecting results of `phpinfo()` call - create small
file containing:
<?php phpinfo(); ?>
Then, run it in your browser and inspect the output, looking for missing
modules that were supposed to be loaded.
3. "Attempt to write to read-only database"
Make sure Apache has permission to access the database file AND directory
it's stored in. (SQLite writes temporary journal files to the parent
database directory). If you're the only user of the system, you can run
these commands without worrying too much:
chmod 0777 data/
chmod 0777 data/db.sqlite
Otherwise, if you're feeling fancy, you can experiment with setfacl on
Linux or group policies on Windows.

View File

@ -5,19 +5,21 @@ szurubooru
## What is it?
Szurubooru is a Danbooru-style board, a gallery where users can upload, browse,
tag and comment images, video clips and flash animations.
`szurubooru` is a Danbooru-style board, a gallery where users can upload,
browse, tag and comment images, video clips and flash animations.
Its name have its roots in Polish language and has onomatopoeic meaning of
scraping or scrubbing. It is pronounced *"shoorubooru"* [ˌʃuruˈburu].
## Licensing
Please see the file named [`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
Please see the file named
[`LICENSE`](https://github.com/rr-/szurubooru/blob/master/LICENSE).
## Installation
Please see the file named [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
Please see the file named
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
## Bugs and feature requests
@ -29,8 +31,8 @@ please do following:
to your problem, comment on that issue instead of opening a new one.
2. If you found an issue and the issue is closed, feel free to reopen it.
3. If you're reporting a bug, create an isolated and reproducible scenario.
4. If you're filing a feature request, provide examples - what might be obvious
to you, might not be so obvious to the developers.
4. If you're filing a feature request, provide examples - what might be
obvious to you, might not be so obvious to the developers.
## Contributing the code
@ -40,13 +42,14 @@ Here are some guidelines on how to contribute:
- Respect coding standards - be consistent with existing code base.
- Watch your whitespace - don't leave any characters at the end of the lines.
- Always run tests before pushing.
- Before starting, see [`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
- Before starting, see
[`INSTALL.md`](https://github.com/rr-/szurubooru/blob/master/INSTALL.md).
- Use `grunt` to do automatic tasks like minifying Javascript files or running
tests. Run `grunt --help` to see full list of available tasks.
## API
Szurubooru from version 0.9+ uses REST API. Currently there is no formal
`szurubooru` from version 0.9+ uses REST API. Currently there is no formal
documentation; source code behind REST layer lies in `src/Controllers/`
directory. In order to use the API, bear in mind that you need to:

View File

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

View File

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

View File

@ -81,12 +81,13 @@ module.exports = function(grunt) {
files: [
{ src: 'node_modules/jquery/dist/jquery.min.js', dest: 'public_html/lib/jquery.min.js' },
{ src: 'node_modules/jquery.cookie/jquery.cookie.js', dest: 'public_html/lib/jquery.cookie.js' },
{ src: 'node_modules/Mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
{ src: 'node_modules/mousetrap/mousetrap.min.js', dest: 'public_html/lib/mousetrap.min.js' },
{ src: 'node_modules/pathjs/path.js', dest: 'public_html/lib/path.js' },
{ src: 'node_modules/underscore/underscore-min.js', dest: 'public_html/lib/underscore.min.js' },
{ src: 'node_modules/marked/lib/marked.js', dest: 'public_html/lib/marked.js' },
{ src: 'node_modules/nprogress/nprogress.js', dest: 'public_html/lib/nprogress.js' },
{ src: 'node_modules/nprogress/nprogress.css', dest: 'public_html/lib/nprogress.css' },
{ cwd: 'node_modules', src: 'font-awesome/**/*', dest: 'public_html/lib/', expand: true },
]
}
},
@ -136,7 +137,7 @@ module.exports = function(grunt) {
templates: readTemplates(grunt),
timestamp: grunt.template.today('isoDateTime'),
maxPostSize: config.database.maxPostSize,
tagCategories: config.tags.categories,
tagCategories: config.tags.categories.map(function(s) { return s.split(/,\s*/); }),
}
},
dist: {
@ -162,7 +163,7 @@ module.exports = function(grunt) {
});
grunt.registerTask('update', 'Upgrade database to newest version.', function() {
exec('php scripts/upgrade.php');
exec('php scripts/upgrade');
});
grunt.registerTask('upgrade', ['update']);

View File

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

View File

@ -145,12 +145,6 @@
<!-- Indentation -->
<!-- **************** -->
<!-- Tests to make sure that a line does not contain the tab character. -->
<test name="indentation"> <!-- noTabs -->
<property name="type" value="tabs"/> <!-- tabs or spaces -->
<property name="number" value="4"/> <!-- number of spaces if type = spaces -->
</test>
<!-- Check the position of the open curly brace in a control structure (if) -->
<!-- sl = same line -->
<!-- nl = new line -->

View File

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

View File

@ -10,27 +10,33 @@
display: none;
}
.comments ul {
ul.comments {
list-style-type: none;
margin: 1em 0;
padding: 0;
}
.comment ul {
list-style-position: inside;
margin: 1em 0;
padding: 0;
}
.comment {
margin: 0 0 1em 0;
padding: 0;
display: -webkit-flex;
display: flex;
}
.comment .avatar {
margin-right: 0.5em;
margin-top: 0.2em;
margin-right: 0.75em;
-webkit-flex-shrink: 0;
flex-shrink: 0;
vertical-align: top;
}
.comment .content {
margin-top: 0.25em;
}
.comment .content p:first-child {
margin-top: 0;
}
@ -83,14 +89,18 @@
margin-bottom: 2em;
}
#global-comment-list .post-comment {
display: -webkit-flex;
display: flex;
}
@media all and (max-width: 40em) {
#global-comment-list .post-comment {
-webkit-flex-direction: column;
flex-direction: column;
}
}
#global-comment-list .post {
-webkit-flex-shrink: 0;
-webkit-flex-grow: 0;
flex-shrink: 0;
flex-grow: 0;
margin-right: 1em;
@ -100,6 +110,19 @@
#global-comment-list .comments>h1 {
display: none;
}
#global-comment-list .post-small a {
#global-comment-list .post-small .link {
margin: 0;
}
.sjis {
font-family: 'MS PGothic', ' 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;
color: #555;
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
font-size: 15px;
overflow-y: scroll;
}
@media all and (max-width: 40em) {
body {
font-size: 13px;
}
}
h1 {
font-weight: normal;
font-size: 30px;
font-size: 160%;
}
h2 {
@ -21,11 +27,11 @@ h2 {
h3 {
font-weight: normal;
font-size: 20px;
font-size: 120%;
}
small {
font-size: 13px;
font-size: 87%;
}
#middle {

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@
position: relative;
}
#post-upload-step1 .url-handler .input-wrapper {
margin-right: 8.5em;
margin-right: 9.5em;
}
#post-upload-step1 .url-handler button {
position: absolute;
@ -118,14 +118,14 @@
text-align: left;
}
#post-upload-step2 .messages {
margin-bottom: 1em;
margin: 1em 0;
}
#post-upload-step2 .form-slider {
text-align: center;
}
#post-upload-step2 .form-slider .thumbnail img {
max-width: 100%;
max-height: 300px;
max-height: 450px;
margin: 0 auto 1em auto;
}
@ -140,35 +140,6 @@
display: none;
}
#lightbox {
display: none;
position: absolute;
pointer-events: none;
position: absolute;
margin-left: 10px;
}
#lightbox img {
max-width: 400px;
max-height: 400px;
background: white;
border: 0.5em solid white;
box-shadow: 0 0 0 1px #eee;
position: relative;
}
#lightbox:after {
content: '';
position: absolute;
left: -8px;
top: 50%;
margin-top: -8px;
width: 12px;
height: 12px;
background: white;
border-left: 1px solid #eee;
border-bottom: 1px solid #eee;
transform: rotate(45deg);
}
#uploading-alert {
display: none;
text-align: left;

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

View File

@ -78,19 +78,6 @@
word-break: break-all;
}
.tag-category-character,
.tag-category-character a {
color: #0a0;
}
.tag-category-copyright,
.tag-category-copyright a {
color: #a0a;
}
.tag-category-artist,
.tag-category-artist a {
color: #a00;
}
.tag-category-meta,
.tag-category-meta a {
color: #aaa;
*[class*='tag-category-']:not(.tag-category-default) a {
color: inherit;
}

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@
data-version="dev"
data-build-time=""
data-max-post-size="10485760"
data-tag-categories='["meta","character","artist","copyright"]'>
data-tag-categories='[["meta","meta","#aaa"],["character","character","#0a0"],["artist","artist","#a00"],["copyright","copyright","#a0a"]]'>
<!-- /build -->
<!-- build:template
<head
@ -15,6 +15,7 @@
data-tag-categories='<%= JSON.stringify(tagCategories).replace(/'/g, '&#039;') %>'>
/build -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<!-- build:remove -->
<title>szurubooru</title>
@ -23,11 +24,26 @@
<title><%= serviceName %></title>
/build -->
<!-- build:remove -->
<style type="text/css">
.tag-category-character { color: #0a0; }
.tag-category-copyright { color: #a0a; }
.tag-category-artist { color: #a00; }
.tag-category-meta { color: #aaa; }
</style>
<!-- /build -->
<!-- build:template
<link rel="stylesheet" type="text/css" href="app.min.css?<%= timestamp %>"/>
<style type="text/css">
<% _.each(tagCategories, function(item) {
var type = item[0];
var color = item[2];
%>.tag-category-<%= type %>{color:<%=color%>;}<%
}); %>
</style>
/build -->
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="/lib/font-awesome/css/font-awesome.min.css"/>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Droid+Sans:400,700"/>
<link rel="stylesheet" type="text/css" href="//fonts.googleapis.com/css?family=Inconsolata">

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('loggedIn', response.json.user && !!response.json.user.id);
appState.set('loggedInUser', response.json.user);
appState.set('config', response.json.config);
}
function isLoggedIn(userName) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,12 +5,15 @@ App.Presenters.PostContentPresenter = function(
jQuery,
util,
promise,
keyboard,
presenterManager,
postNotesPresenter) {
postNotesPresenter,
browsingSettings) {
var post;
var templates = {};
var $target;
var $wrapper;
function init(params, loaded) {
$target = params.$target;
@ -27,14 +30,95 @@ App.Presenters.PostContentPresenter = function(
});
}
function getFitters() {
var originalWidth = $wrapper.attr('data-width');
var originalHeight = $wrapper.attr('data-height');
var ratio = originalWidth / originalHeight;
var containerHeight = jQuery(window).height() - $wrapper.offset().top - 10;
var containerWidth = $wrapper.parent().outerWidth() - 10;
return {
'fit-both': function(allowUpscale) {
var width = containerWidth;
var height = containerWidth / ratio;
if (height > containerHeight) {
width = containerHeight * ratio;
height = containerHeight;
}
if (!allowUpscale) {
if (width > originalWidth) {
width = originalWidth;
height = originalWidth / ratio;
}
if (height > originalHeight) {
width = originalHeight * ratio;
height = originalHeight;
}
}
$wrapper.css({maxWidth: width + 'px'});
},
'fit-height': function(allowUpscale) {
var width = containerHeight * ratio;
if (width > originalWidth && !allowUpscale) {
width = originalWidth;
}
$wrapper.css({maxWidth: width + 'px'});
},
'fit-width': function(allowUpscale) {
if (allowUpscale) {
$wrapper.css({maxWidth: containerWidth + 'px'});
} else {
$wrapper.css({maxWidth: originalWidth + 'px'});
}
},
'original': function(allowUpscale) {
$wrapper.css({
minWidth: originalWidth + 'px',
width: originalWidth + 'px'});
}
};
}
function getFitMode() {
return $wrapper.data('fit-mode');
}
function changeFitMode(fitMode) {
$wrapper.data('fit-mode', fitMode);
$wrapper.css({
width: '', height: '',
minWidth: '', minHeight: '',
maxWidth: '', maxHeight: '',
});
getFitters()[fitMode.style](fitMode.upscale);
updatePostNotesSize();
}
function cycleFitMode() {
var oldMode = getFitMode();
var fitterNames = Object.keys(getFitters());
var newMode = {
style: fitterNames[(fitterNames.indexOf(oldMode.style) + 1) % fitterNames.length],
upscale: oldMode.upscale,
};
changeFitMode(newMode);
}
function render() {
$target.html(templates.postContent({post: post}));
$wrapper = $target.find('.object-wrapper');
if (post.contentType === 'image') {
if (post.contentType === 'image' || post.contentType === 'animation') {
loadPostNotes();
updatePostNotesSize();
}
changeFitMode({
style: browsingSettings.getSettings().fitMode,
upscale: browsingSettings.getSettings().upscale,
});
keyboard.keyup('f', cycleFitMode);
jQuery(window).resize(updatePostNotesSize);
}
@ -45,8 +129,14 @@ App.Presenters.PostContentPresenter = function(
}
function updatePostNotesSize() {
$target.find('.post-notes-target').width($target.find('.image-wrapper').outerWidth());
$target.find('.post-notes-target').height($target.find('.image-wrapper').outerHeight());
var $postNotes = $target.find('.post-notes-target');
var $wrapper = $target.find('.object-wrapper');
$postNotes.css({
width: $wrapper.outerWidth() + 'px',
height: $wrapper.outerHeight() + 'px',
left: ($wrapper.offset().left - $wrapper.parent().offset().left) + 'px',
top: ($wrapper.offset().top - $wrapper.parent().offset().top) + 'px',
});
}
function addNewPostNote() {
@ -57,14 +147,19 @@ App.Presenters.PostContentPresenter = function(
init: init,
render: render,
addNewPostNote: addNewPostNote,
updatePostNotesSize: updatePostNotesSize,
getFitMode: getFitMode,
changeFitMode: changeFitMode,
cycleFitMode: cycleFitMode,
};
};
App.DI.register('postContentPresenter', [
'jQuery',
'util',
'promise',
'keyboard',
'presenterManager',
'postNotesPresenter'],
'postNotesPresenter',
'browsingSettings'],
App.Presenters.PostContentPresenter);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -133,7 +133,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
function editSuccess(apiResponse) {
var wasLoggedIn = auth.isLoggedIn(user.name);
user = apiResponse.json;
user = apiResponse.json.user;
if (wasLoggedIn) {
auth.updateCurrentUser(user);
}
@ -142,7 +142,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
var $messages = jQuery(target).find('.messages');
var message = 'Account settings updated!';
if (!apiResponse.json.confirmed) {
if (!apiResponse.json.user.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
}
messagePresenter.showInfo($messages, message);

View File

@ -51,6 +51,9 @@ App.Presenters.UserBrowsingSettingsPresenter = function(
sketchy: $el.find('[name=listSketchyPosts]').is(':checked'),
unsafe: $el.find('[name=listUnsafePosts]').is(':checked'),
},
keyboardShortcuts: $el.find('[name=keyboardShortcuts]').is(':checked'),
fitMode: $el.find('[name=fitMode]:checked').val(),
upscale: $el.find('[name=upscale]').is(':checked'),
};
promise.wait(browsingSettings.setSettings(newSettings))

View File

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

View File

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

View File

@ -2,21 +2,32 @@ var App = App || {};
App.Promise = function(_, jQuery, progress) {
function BrokenPromiseError(promiseId) {
this.name = 'BrokenPromiseError';
this.message = 'Broken promise (promise ID: ' + promiseId + ')';
}
BrokenPromiseError.prototype = new Error();
var active = [];
var promiseId = 0;
function make(callback) {
function make(callback, useProgress) {
var deferred = jQuery.Deferred();
var promise = deferred.promise();
promise.promiseId = ++ promiseId;
if (useProgress === true) {
progress.start();
}
callback(function() {
try {
deferred.resolve.apply(deferred, arguments);
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset();
}
}, function() {
@ -25,6 +36,9 @@ App.Promise = function(_, jQuery, progress) {
active = _.without(active, promise.promiseId);
progress.done();
} catch (e) {
if (!(e instanceof BrokenPromiseError)) {
console.log(e);
}
progress.reset();
}
});
@ -33,7 +47,7 @@ App.Promise = function(_, jQuery, progress) {
promise.always(function() {
if (!_.contains(active, promise.promiseId)) {
throw new Error('Broken promise (promise ID: ' + promise.promiseId + ')');
throw new BrokenPromiseError(promise.promiseId);
}
});
@ -60,7 +74,8 @@ App.Promise = function(_, jQuery, progress) {
}
return {
make: make,
make: function(callback) { return make(callback, true); },
makeSilent: function(callback) { return make(callback, false); },
wait: wait,
getActive: getActive,
abortAll: abortAll,

View File

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

View File

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

View File

@ -2,75 +2,139 @@ var App = App || {};
App.Util = App.Util || {};
App.Util.Draggable = function(jQuery) {
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
function relativeDragStrategy($element) {
var $parent = $element.parent();
var delta;
var x = $element.offset().left - $parent.offset().left;
var y = $element.offset().top - $parent.offset().top;
var getPosition = function() {
return {x: x, y: y};
};
var setPosition = function(newX, newY) {
x = newX;
y = newY;
var screenX = Math.min(Math.max(newX, 0), $parent.outerWidth() - $element.outerWidth());
var screenY = Math.min(Math.max(newY, 0), $parent.outerHeight() - $element.outerHeight());
screenX *= 100.0 / $parent.outerWidth();
screenY *= 100.0 / $parent.outerHeight();
$element.css({
left: screenX + '%',
top: screenY + '%'});
};
return {
click: function(e) {
mouseClicked: function(e) {
delta = {
x: $element.offset().left - e.clientX,
y: $element.offset().top - e.clientY,
};
},
update: function(e) {
var x = e.clientX + delta.x - $parent.offset().left;
var y = e.clientY + delta.y - $parent.offset().top;
x = Math.min(Math.max(x, 0), $parent.outerWidth() - $element.outerWidth());
y = Math.min(Math.max(y, 0), $parent.outerHeight() - $element.outerHeight());
x *= 100.0 / $parent.outerWidth();
y *= 100.0 / $parent.outerHeight();
$element.css({
left: x + '%',
top: y + '%'});
mouseMoved: function(e) {
setPosition(
e.clientX + delta.x - $parent.offset().left,
e.clientY + delta.y - $parent.offset().top);
},
getPosition: getPosition,
setPosition: setPosition,
};
}
function absoluteDragStrategy($element) {
var delta;
var x = $element.offset().left;
var y = $element.offset().top;
var getPosition = function() {
return {x: x, y: y};
};
var setPosition = function(newX, newY) {
x = newX;
y = newY;
$element.css({
left: x + 'px',
top: y + 'px'});
};
return {
click: function(e) {
mouseClicked: function(e) {
delta = {
x: $element.position().left - e.clientX,
y: $element.position().top - e.clientY,
};
},
update: function(e) {
var x = e.clientX + delta.x;
var y = e.clientY + delta.y;
$element.css({
left: x + 'px',
top: y + 'px'});
mouseMoved: function(e) {
setPosition(e.clientX + delta.x, e.clientY + delta.y);
},
getPosition: getPosition,
setPosition: setPosition,
};
}
function makeDraggable($element, dragStrategy) {
function makeDraggable($element, dragStrategy, enableHotkeys) {
var strategy = dragStrategy($element);
$element.data('drag-strategy', strategy);
$element.addClass('draggable');
$element.mousedown(function(e) {
if (e.target !== $element.get(0)) {
return;
}
e.preventDefault();
$element.focus();
$element.addClass('dragging');
strategy.click(e);
strategy.mouseClicked(e);
jQuery(window).bind('mousemove.elemmove', function(e) {
strategy.update(e);
strategy.mouseMoved(e);
}).bind('mouseup.elemmove', function(e) {
e.preventDefault();
strategy.update(e);
strategy.mouseMoved(e);
$element.removeClass('dragging');
jQuery(window).unbind('mousemove.elemmove');
jQuery(window).unbind('mouseup.elemmove');
});
});
if (enableHotkeys) {
$element.keydown(function(e) {
var position = strategy.getPosition();
var oldPosition = {x: position.x, y: position.y};
if (e.shiftKey) {
return;
}
var delta = e.ctrlKey ? 10 : 1;
if (e.which === KEY_LEFT) {
position.x -= delta;
} else if (e.which === KEY_RIGHT) {
position.x += delta;
} else if (e.which === KEY_UP) {
position.y -= delta;
} else if (e.which === KEY_DOWN) {
position.y += delta;
}
if (position.x !== oldPosition.x || position.y !== oldPosition.y) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
strategy.setPosition(position.x, position.y);
}
});
}
}
return {

View File

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

View File

@ -2,41 +2,103 @@ var App = App || {};
App.Util = App.Util || {};
App.Util.Resizable = function(jQuery) {
function makeResizable($element) {
var KEY_LEFT = 37;
var KEY_UP = 38;
var KEY_RIGHT = 39;
var KEY_DOWN = 40;
function relativeResizeStrategy($element) {
var $parent = $element.parent();
var delta;
var width = $element.width();
var height = $element.height();
var getSize = function() {
return {width: width, height: height};
};
var setSize = function(newWidth, newHeight) {
width = newWidth;
height = newHeight;
var screenWidth = Math.min(Math.max(width, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
var screenHeight = Math.min(Math.max(height, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
screenWidth *= 100.0 / $parent.outerWidth();
screenHeight *= 100.0 / $parent.outerHeight();
$element.css({
width: screenWidth + '%',
height: screenHeight + '%'});
};
return {
mouseClicked: function(e) {
delta = {
x: $element.width() - e.clientX,
y: $element.height() - e.clientY,
};
},
mouseMoved: function(e) {
setSize(
e.clientX + delta.x,
e.clientY + delta.y);
},
getSize: getSize,
setSize: setSize,
};
}
function makeResizable($element, enableHotkeys) {
var $resizer = jQuery('<div class="resizer"></div>');
var strategy = relativeResizeStrategy($element);
$element.append($resizer);
$resizer.mousedown(function(e) {
e.preventDefault();
e.stopPropagation();
$element.focus();
$element.addClass('resizing');
var $parent = $element.parent();
var deltaX = $element.width() - e.clientX;
var deltaY = $element.height() - e.clientY;
var update = function(e) {
var w = e.clientX + deltaX;
var h = e.clientY + deltaY;
w = Math.min(Math.max(w, 20), $parent.outerWidth() + $parent.offset().left - $element.offset().left);
h = Math.min(Math.max(h, 20), $parent.outerHeight() + $parent.offset().top - $element.offset().top);
w *= 100.0 / $parent.outerWidth();
h *= 100.0 / $parent.outerHeight();
$element.css({
width: w + '%',
height: h + '%'});
};
strategy.mouseClicked(e);
jQuery(window).bind('mousemove.elemsize', function(e) {
update(e);
strategy.mouseMoved(e);
}).bind('mouseup.elemsize', function(e) {
e.preventDefault();
update(e);
strategy.mouseMoved(e);
$element.removeClass('resizing');
jQuery(window).unbind('mousemove.elemsize');
jQuery(window).unbind('mouseup.elemsize');
});
});
if (enableHotkeys) {
$element.keydown(function(e) {
var size = strategy.getSize();
var oldSize = {width: size.width, height: size.height};
if (!e.shiftKey) {
return;
}
var delta = e.ctrlKey ? 10 : 1;
if (e.which === KEY_LEFT) {
size.width -= delta;
} else if (e.which === KEY_RIGHT) {
size.width += delta;
} else if (e.which === KEY_UP) {
size.height -= delta;
} else if (e.which === KEY_DOWN) {
size.height += delta;
}
if (size.width !== oldSize.width || size.height !== oldSize.height) {
e.stopPropagation();
e.stopImmediatePropagation();
e.preventDefault();
strategy.setSize(size.width, size.height);
}
});
}
}
return {

View File

@ -41,6 +41,50 @@
</div>
</div>
<div class="form-row">
<label class="form-label" for="browsing-settings-keyboard-shortcuts">Keyboard shortcuts:</label>
<div class="form-input">
<input <% print(settings.keyboardShortcuts ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-keyboard-shortcuts" name="keyboardShortcuts"/>
<label for="browsing-settings-keyboard-shortcuts">
Enabled
</label>
</div>
</div>
<div class="form-row">
<label class="form-label">Default fit mode:</label>
<div class="form-input">
<input <% print(settings.fitMode === 'fit-width' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-width" name="fitMode" value="fit-width"/>
<label for="browsing-settings-fit-width">
Fit to window width
</label>
<br/>
<input <% print(settings.fitMode === 'fit-height' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-height" name="fitMode" value="fit-height"/>
<label for="browsing-settings-fit-height">
Fit to window height
</label>
<br/>
<input <% print(settings.fitMode === 'fit-both' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-both" name="fitMode" value="fit-both"/>
<label for="browsing-settings-fit-both">
Fit to both width and height
</label>
<br/>
<input <% print(settings.fitMode === 'original' ? 'checked="checked"' : '') %> type="radio" id="browsing-settings-fit-original" name="fitMode" value="original"/>
<label for="browsing-settings-fit-original">
Show at original size
</label>
<br/>
<input <% print(settings.upscale ? 'checked="checked"' : '') %> type="checkbox" id="browsing-settings-upscale" name="upscale" value="upscale"/>
<label for="browsing-settings-upscale">
Upscale small posts
</label>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
@ -48,5 +92,3 @@
</div>
</div>
</form>

View File

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

View File

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

View File

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

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">
<h1><%= title %></h1>
<p class="subheader">
Serving <%= globals.postCount || 0 %> posts (<%= formatFileSize(globals.postSize || 0) %>)
Serving <%= globals.postCount || 0 %> posts (<%= util.formatFileSize(globals.postSize || 0) %>)
</p>
<% if (post && post.id) { %>
<div class="post">
<div class="post" style="width: <%= post.imageWidth || 800 %>px">
<div id="post-content-target">
</div>
@ -25,29 +43,16 @@
<% } %>
uploaded
<%= formatRelativeTime(post.uploadTime) %>
<%= util.formatRelativeTime(post.creationTime) %>
by
<% showUser(post.user.name) %>
</span>
<span class="right">
featured
<%= formatRelativeTime(post.lastFeatureTime) %>
<%= util.formatRelativeTime(post.lastFeatureTime) %>
by
<% var showLink = canViewUsers && user.name %>
<% if (showLink) { %>
<a href="#/user/<%= user.name %>">
<% } %>
<img width="25" height="25" class="author-avatar"
src="/data/thumbnails/25x25/avatars/<%= user.name || '!' %>"
alt="<%= user.name || 'Anonymous user' %>"/>
<%= user.name || 'Anonymous user' %>
<% if (showLink) { %>
</a>
<% } %>
<% showUser(user.name) %>
</span>
</div>
@ -56,7 +61,7 @@
<p>
<small class="version">
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= formatRelativeTime(buildTime) %>)
Version: <a href="//github.com/rr-/szurubooru/commits/master"><%= version %></a> (built <%= util.formatRelativeTime(buildTime) %>)
|
<a href="#/history">Recent tag and post edits</a>
</small>

View File

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

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

View File

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

View File

@ -2,7 +2,7 @@
<% if (canViewPosts) { %>
<a class="link"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, typeof(query) !== 'undefined' ? query : {}) %>"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, util.simplifySearchQuery(typeof(query) !== 'undefined' ? query : {})) %>"
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
<% } else { %>
<span class="link">

View File

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

View File

@ -39,7 +39,9 @@
<label></label>
</td>
<td class="thumbnail">
<a href="#"/>
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
</a>
</td>
<td class="tags"></td>
<td class="safety"><div class="safety-template"></div></td>
@ -52,10 +54,16 @@
<button class="post-table-op remove"><i class="fa fa-remove"></i> Remove</button>
</li><!--
--><li>
<button class="post-table-op move-up"><i class="fa fa-chevron-up"></i> Move up</button>
<button class="post-table-op previous"><i class="fa fa-chevron-up"></i> Previous</button>
</li><!--
--><li>
<button class="post-table-op move-down"><i class="fa fa-chevron-down"></i> Move down</button>
<button class="post-table-op next"><i class="fa fa-chevron-down"></i> Next</button>
</li><!--
--><li>
<button class="post-table-op move-up"><i class="fa fa-arrow-up"></i> Move up</button>
</li><!--
--><li>
<button class="post-table-op move-down"><i class="fa fa-arrow-down"></i> Move down</button>
</li><!--
--><li>
<button class="upload highlight" type="submit"><i class="fa fa-upload"></i> Submit</button>
@ -73,6 +81,7 @@
<div class="form-slider">
<div class="thumbnail">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Thumbnail"/>
<a href="#" target="_blank">Open preview in a new tab</a>
</div>
<form class="form-wrapper">
@ -134,8 +143,4 @@
</div>
</div>
<div id="lightbox">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" alt="Preview">
</div>
</div>

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

View File

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

View File

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

View File

@ -7,10 +7,10 @@
<a class="big-button" href="#/users/order=name,desc">Sort Z&rarr;A</a>
</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>
<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>
</ul>

View File

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

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

@ -1,3 +1,4 @@
#!/usr/bin/php
<?php
require_once(__DIR__
. 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
require_once(__DIR__
. 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()
{
$query = $this->pdo->from($this->tableName);
$arrayEntities = iterator_to_array($query);
return $this->arrayToEntities($arrayEntities);
return $this->arrayToEntities($query);
}
public function findById($entityId)
@ -248,7 +247,7 @@ abstract class AbstractDao implements ICrudDao, IBatchDao
$query->where($sql, $bindings);
}
protected function arrayToEntities(array $arrayEntities, $entityConverter = null)
protected function arrayToEntities($arrayEntities, $entityConverter = null)
{
if ($entityConverter === null)
$entityConverter = $this->entityConverter;

View File

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

View File

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

View File

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

View File

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

View File

@ -44,10 +44,53 @@ class FileDao implements IFileDao
return $this->directory . DIRECTORY_SEPARATOR . $fileName;
}
public function listAll()
{
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory));
$files = [];
foreach ($iterator as $path)
{
if (!$path->isDir())
$files[] = $this->getRelativePath($this->directory, $path->getPathName());
}
return $files;
}
private function createFolders($fileName)
{
$fullPath = dirname($this->getFullPath($fileName));
if (!file_exists($fullPath))
mkdir($fullPath, 0777, true);
}
private function getRelativePath($from, $to)
{
$from = is_dir($from) ? rtrim($from, '\/') . '/' : $from;
$to = is_dir($to) ? rtrim($to, '\/') . '/' : $to;
$from = explode('/', str_replace('\\', '/', $from));
$to = explode('/', str_replace('\\', '/', $to));
$relPath = $to;
foreach ($from as $depth => $dir)
{
if ($dir === $to[$depth])
{
array_shift($relPath);
}
else
{
$remaining = count($from) - $depth;
if ($remaining > 1)
{
$padLength = (count($relPath) + $remaining - 1) * -1;
$relPath = array_pad($relPath, $padLength, '..');
break;
}
else
{
$relPath[0] = $relPath[0];
}
}
}
return implode('/', $relPath);
}
}

View File

@ -149,7 +149,7 @@ class PostDao extends AbstractDao implements ICrudDao
return;
}
elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT)
elseif ($requirement->getType() === PostFilter::REQUIREMENT_COMMENT_AUTHOR)
{
foreach ($requirement->getValue()->getValues() as $userName)
{
@ -194,6 +194,17 @@ class PostDao extends AbstractDao implements ICrudDao
return;
}
elseif ($requirement->getType() === PostFilter::REQUIREMENT_TUMBLEWEED)
{
$sql = 'posts.score = 0
AND posts.commentCount = 0
AND posts.favCount = 0';
if ($requirement->isNegated())
$sql = 'NOT (' . $sql . ')';
$query->where($sql, true);
return;
}
parent::decorateQueryFromRequirement($query, $requirement);
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ final class Post extends Entity
const POST_TYPE_FLASH = 2;
const POST_TYPE_VIDEO = 3;
const POST_TYPE_YOUTUBE = 4;
const POST_TYPE_ANIMATED_IMAGE = 5;
const FLAG_LOOP = 1;
@ -28,7 +29,7 @@ final class Post extends Entity
private $name;
private $userId;
private $uploadTime;
private $creationTime;
private $lastEditTime;
private $safety;
private $contentType;
@ -78,14 +79,14 @@ final class Post extends Entity
$this->safety = $safety;
}
public function getUploadTime()
public function getCreationTime()
{
return $this->uploadTime;
return $this->creationTime;
}
public function setUploadTime($uploadTime)
public function setCreationTime($creationTime)
{
$this->uploadTime = $uploadTime;
$this->creationTime = $creationTime;
}
public function getLastEditTime()

View File

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

View File

@ -23,7 +23,7 @@ final class User extends Entity
private $passwordHash;
private $passwordSalt;
private $accessRank;
private $registrationTime;
private $creationTime;
private $lastLoginTime;
private $avatarStyle;
private $browsingSettings;
@ -110,14 +110,14 @@ final class User extends Entity
$this->accessRank = $accessRank;
}
public function getRegistrationTime()
public function getCreationTime()
{
return $this->registrationTime;
return $this->creationTime;
}
public function setRegistrationTime($registrationTime)
public function setCreationTime($creationTime)
{
$this->registrationTime = $registrationTime;
$this->creationTime = $creationTime;
}
public function getLastLoginTime()

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ class EnumHelper
'video' => Post::POST_TYPE_VIDEO,
'flash' => Post::POST_TYPE_FLASH,
'youtube' => Post::POST_TYPE_YOUTUBE,
'animation' => Post::POST_TYPE_ANIMATED_IMAGE,
];
private static $snapshotTypeMap =
@ -103,7 +104,12 @@ class EnumHelper
$key = trim(strtolower($enumString));
$lowerEnumMap = array_change_key_case($enumMap, \CASE_LOWER);
if (!isset($lowerEnumMap[$key]))
throw new \DomainException('Unrecognized value: ' . $enumString);
{
throw new \DomainException(sprintf(
'Unrecognized value: %s.' . PHP_EOL . 'Possible values: %s',
$enumString,
implode(', ', array_keys($lowerEnumMap))));
}
return $lowerEnumMap[$key];
}

View File

@ -3,6 +3,8 @@ namespace Szurubooru\Helpers;
class HttpHelper
{
private $redirected = false;
public function setResponseCode($code)
{
http_response_code($code);
@ -29,9 +31,26 @@ class HttpHelper
}
public function getRequestHeaders()
{
if (function_exists('getallheaders'))
{
return getallheaders();
}
$result = [];
foreach ($_SERVER as $key => $value)
{
if (substr($key, 0, 5) === "HTTP_")
{
$key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
$result[$key] = $value;
}
else
{
$result[$key] = $value;
}
}
return $result;
}
public function getRequestHeader($key)
{
@ -50,4 +69,23 @@ class HttpHelper
$requestUri = preg_replace('/\?.*$/', '', $requestUri);
return $requestUri;
}
public function redirect($destination)
{
$this->setResponseCode(307);
$this->setHeader('Location', $destination);
$this->redirected = true;
}
public function nonCachedRedirect($destination)
{
$this->setResponseCode(303);
$this->setHeader('Location', $destination);
$this->redirected = true;
}
public function isRedirecting()
{
return $this->redirected;
}
}

View File

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

View File

@ -18,6 +18,12 @@ class MimeHelper
return self::getMimeTypeFrom16Bytes(substr($buffer, 0, 16));
}
public static function isBufferAnimatedGif($buffer)
{
return strtolower(self::getMimeTypeFromBuffer($buffer)) === 'image/gif'
and preg_match_all('#\x21\xf9\x04.{4}\x00[\x2c\x21]#s', $buffer) > 1;
}
public static function isFlash($mime)
{
return strtolower($mime) === 'application/x-shockwave-flash';

View File

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

View File

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

View File

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

View File

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

View File

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

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