232 Commits
1.0.0 ... 1.1.0

Author SHA1 Message Date
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
43334b33e1 Fixed suggestions not hiding in the upload 2014-12-14 20:49:42 +01:00
839e97b1e2 Fixed history for posts uploaded anonymously
Until now posts uploaded anonymously were not so anonymous - the author
was clearly visible in post history (and tag history if the upload has
led to creation of any new tags). Both of these are fixed now.
2014-12-14 09:30:22 +01:00
8a33b9581d Tweaked font size for <small/> tag 2014-12-07 13:43:15 +01:00
7067d8e13d Added support for [small]text[/small] 2014-12-07 13:43:04 +01:00
5769034223 Fixed tag input behavior for initial text 2014-11-30 20:30:06 +01:00
c0a5c800e0 Fixed autocomplete staying even after hiding input 2014-11-30 13:05:31 +01:00
b70231bd7e Changed used tags in autocomplete to be grayed-out 2014-11-30 12:49:48 +01:00
bc757dd883 Fixed reused implied tags marked as duplicates 2014-11-30 12:49:48 +01:00
924592675c Fixed tag suggestions for implied tags 2014-11-30 12:40:22 +01:00
a3aea27a13 Fixed tag edits not triggering tag list updates 2014-11-30 12:40:22 +01:00
3c54671aeb Widened post edit form 2014-11-30 11:58:39 +01:00
d8df51f0c0 Moved suggestions before siblings 2014-11-30 11:55:24 +01:00
b693a5f4b3 Added tag siblings suggestion synchronization 2014-11-30 11:54:38 +01:00
303f91e15c Refactored tag suggestions 2014-11-30 11:45:49 +01:00
c350c47195 Added showing tag suggestions on click 2014-11-30 11:31:40 +01:00
24ce67b4ff Added auto completion to tag list presenter 2014-11-30 00:22:50 +01:00
06d7c19556 Added POST to tag merging API 2014-11-28 23:18:26 +01:00
728d1d65de Fixed mass tag 2014-11-28 23:18:23 +01:00
4b27b8a85d Fixed updating user settings 2014-11-28 23:18:14 +01:00
bfe31d87a1 Fixed history presenter 2014-11-27 18:22:48 +01:00
2fd371b10a Added absolute timestamp hints where necessary 2014-11-27 10:34:45 +01:00
f1647a5f7b Fixed precision loss in post disk space usage 2014-11-22 18:08:34 +01:00
cd688b25a3 Fixed example tag usages not showing up 2014-11-22 17:54:37 +01:00
a7c6e9f043 Fixed putting "null" in post sources 2014-11-22 17:49:35 +01:00
997c2a10ec Renamed SearchServices directory to Search 2014-11-22 14:56:30 +01:00
95cf0ca37b Moved Controllers/ViewProxies to ViewProxies 2014-11-22 14:53:40 +01:00
116522498d Removed obsolete method in InputReader 2014-11-22 14:47:25 +01:00
01a84ee4e2 Changed account settings editing
Account settings editing no longer encapsulates file content in base64.
2014-11-22 14:45:48 +01:00
2458935fdf Changed post editing
Post editing no longer encapsulates file content in base64.
2014-11-22 14:45:45 +01:00
f2b1e3bedb Changed post uploads
Post uploads no longer encapsulates file content in base64.
This means dramatic speed up for sending on local networks.
2014-11-22 14:37:00 +01:00
77c51d9a8a Added support for FormData in JS API facade 2014-11-22 14:31:53 +01:00
d8d65ed24c Changed PUT requests to POST
HTTP spec disallows multipart/form-data requests using PUT method.
2014-11-22 14:30:29 +01:00
58d3129548 Removed dangling console.log 2014-11-22 14:04:40 +01:00
0b032e7f94 Merge branch 'api' 2014-11-22 13:10:24 +01:00
4e6fe634e1 Fixed post list disregarding viewPosts privilege 2014-11-22 13:10:00 +01:00
b14f02810e Fixed everyone could view every post 2014-11-22 13:00:36 +01:00
796c2d1b1f Fixed everyone could feature posts 2014-11-22 12:58:02 +01:00
40197d6c39 Fixed everyone could delete posts 2014-11-22 12:57:52 +01:00
736c0a66ff Fixed comments disregarding viewUsers privilege 2014-11-22 12:56:44 +01:00
48230a64ad Simplified routing 2014-11-22 12:44:45 +01:00
da6b37b14c Merge branch 'master' into api 2014-11-21 22:34:04 +01:00
4b4ccf365a Fixed entity IDs being strings
This coincidentally fixes editing newly added comments.
2014-11-21 22:32:10 +01:00
2b0a4d1f76 Removed AJAX external caching
jQuery's caching caused problems in some scenarios, for example with
scoring/faving posts.
2014-11-21 22:24:06 +01:00
2d1b5308f3 Removed unneeded dependencies 2014-11-21 22:16:56 +01:00
8fb1b87ae5 Fixed post editing 2014-11-21 22:16:56 +01:00
9621810332 Fixed syntax error 2014-11-21 22:16:56 +01:00
bd33b09f7b Fixed private methods 2014-11-21 22:16:31 +01:00
3245c75187 Removed controller layer 2014-11-21 22:16:31 +01:00
e38152b921 Moved user avatar controller to routes 2014-11-21 22:16:31 +01:00
2195b2c9a1 Moved scores controller to routes 2014-11-21 22:16:31 +01:00
969f70318b Moved user controller to routes 2014-11-21 22:16:31 +01:00
06cc776438 Removed .swp files... 2014-11-21 22:05:53 +01:00
76edbfeddb Moved tag controller to routes 2014-11-21 15:49:53 +01:00
9de6e7e739 Moved post notes controller to routes 2014-11-21 12:54:33 +01:00
d8a4e1ec4e Moved post controller to routes 2014-11-21 12:45:47 +01:00
193d1c5f7a Moved post content controller to routes 2014-11-21 11:27:51 +01:00
7ff961fc21 Moved history controller to routes 2014-11-21 10:33:15 +01:00
a11436aa8c Added more tests to route repository test 2014-11-21 10:30:53 +01:00
a3b02adb7f Moved global param controller to routes 2014-11-21 10:30:52 +01:00
40b16f586b Fixed RouteRepositoryTest using real DB 2014-11-21 10:30:51 +01:00
602d7a1f45 Split favorites controller to routes 2014-11-21 10:23:50 +01:00
333c538f1e Split comment controller to routes 2014-11-21 10:23:49 +01:00
7c182f57a0 Added support for arguments in new Routes 2014-11-21 10:23:49 +01:00
2a7ca79b2d Added Route layer proof of concept 2014-11-21 10:23:49 +01:00
9e894bc41c Fixed ControllerRepositoryTest using real DB 2014-11-21 10:21:26 +01:00
cdd2726f30 Improved MySQL integration 2014-11-21 10:20:04 +01:00
029f0f00a4 Improved test database upgrade script 2014-11-21 10:03:08 +01:00
c6fe7a4320 Changed downloading YouTube posts in backend
Trying to download YouTube posts by visiting backend directly results in
a redirect to YouTube instead of showing an ugly error.
2014-11-19 19:47:54 +01:00
8bd4ae27c2 Fixed bad message when serving non-existing files 2014-11-19 19:47:54 +01:00
2a215ef51b Removed download link for YouTube posts 2014-11-19 19:47:50 +01:00
6473ed74d3 Fixed post note removal didn't generate snapshot 2014-11-19 19:40:06 +01:00
44a4184eb8 Changed snapshot merging to work for deletions
Creating and deleting stuff will remove history snapshots if it occurs
within five minute gap. This is to prevent spamming history with
tag names that are introduced by an accident and removed shortly after.
2014-11-19 19:37:10 +01:00
847f248829 Added snapshot compression 2014-11-19 10:22:59 +01:00
a0133ea632 Added snapshot merging 2014-11-18 21:22:46 +01:00
0b15ca1b05 Fixed current search not showing arrow 2014-11-18 16:06:12 +01:00
2996f27671 Fixed scoremin and scoremax 2014-11-17 23:04:23 +01:00
6a751ed0b2 Fixed user list presenter disrespecting privileges
If user had no right to view user accounts, the list presenter ignored
that and linked to pages that shown privilege errors. Now it shows the
links only if user has right to view user accounts.
2014-11-17 22:16:30 +01:00
437 changed files with 23389 additions and 20390 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,7 +27,11 @@ h2 {
h3 {
font-weight: normal;
font-size: 20px;
font-size: 120%;
}
small {
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,18 +10,21 @@
}
#home .post {
text-align: left;
text-align: center;
margin: 0 auto;
display: inline-block;
max-width: 60%;
min-width: 40em;
}
#home .post .left {
display: inline-block;
float: left;
margin-right: 0.5em;
}
#home .post .right {
display: inline-block;
float: right;
margin-left: 0.5em;
}
#home .post-footer,
@ -35,5 +38,7 @@
#home .version {
opacity: .4;
font-size: 12px;
}
#home .subheader, #home .post-footer {
font-size: 85%;
}

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;
@ -68,9 +74,9 @@
.post-small {
position: relative;
}
.post-small a {
display: inline-block;
margin: 0.2em;
.post-small .link {
display: block;
margin: 0.3em;
border: 1px solid #999;
z-index: 1;
position: relative;
@ -82,20 +88,20 @@
}
.post-small a:focus,
.post-small a:hover {
.post-small .link:focus,
.post-small .link:hover {
background: #64C2ED;
border-color: #64C2ED;
box-shadow: 0 0 0 2px #64C2ED;
outline: 0;
}
.post-small a:focus img:not(.loading),
.post-small a:hover img:not(.loading) {
.post-small .link:focus img:not(.loading),
.post-small .link:hover img:not(.loading) {
opacity: .8 !important;
}
.post-small a .info {
.post-small .link .info {
display: none;
text-align: center;
position: absolute;
@ -105,22 +111,22 @@
background: #64C2ED;
color: black;
}
.post-small a .info ul {
.post-small .link .info ul {
list-style-type: none;
padding: 0;
margin: 0;
}
.post-small a .info li {
.post-small .link .info li {
display: inline-block;
margin: 0.1em 0.5em;
padding: 0;
}
.post-small a:focus .info,
.post-small a:hover .info {
.post-small .link:focus .info,
.post-small .link:hover .info {
display: block;
}
.post-small:not(.post-type-image) a::before {
.post-small:not(.post-type-image) .link::before {
display: block;
content: '';
z-index: 2;
@ -133,7 +139,8 @@
border-left: 50px solid transparent;
}
.post-small:not(.post-type-image) a::after {
.post-small:not(.post-type-image) .link::after {
pointer-events: none;
display: block;
content: '...';
z-index: 3;
@ -148,16 +155,19 @@
color: white;
font-size: 15px;
}
.post-small.post-type-youtube a::after {
.post-small.post-type-youtube .link::after {
font-size: 13px;
content: 'youtube';
}
.post-small.post-type-video a::after {
.post-small.post-type-video .link::after {
content: 'video';
}
.post-small.post-type-flash a::after {
.post-small.post-type-flash .link::after {
content: 'flash';
}
.post-small.post-type-animation .link::after {
content: 'anim';
}
.post-small .action {
display: none;

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,26 +141,34 @@
vertical-align: top;
}
#sidebar .essential li i.fa {
font-size: 30px;
font-size: 200%;
}
#sidebar .essential li a {
display: block;
text-align: center;
font-size: 12px;
font-size: 87%;
}
#post-view #post-edit-target {
padding: 1em;
width: 50%;
min-width: 30em;
position: absolute;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 1em 0.5em rgba(255, 255, 255, 0.8);
z-index: 2;
display: none;
}
#post-edit-target .form-wrapper {
min-width: 100%;
}
#post-view>* {
z-index: -1;
}
#post-edit-target .advanced-trigger .form-input {
overflow: auto; /* fix browser's outline around the link being cut due to overflow: hidden; */
}
#post-edit-target .file-handler {
margin: 0.5em 0;
}
@ -181,6 +183,25 @@
position: relative;
margin-bottom: 0.5em;
}
.post-content .object-wrapper {
max-width: 100%;
position: relative;
}
.post-content .object-wrapper img,
.post-content .object-wrapper object,
.post-content .object-wrapper iframe,
.post-content .object-wrapper video {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
border: 0;
}
.post-content .object-wrapper video {
background: black;
}
.post-notes-target {
position: absolute;
pointer-events: none;
@ -220,10 +241,16 @@
}
.post-note {
outline: 0;
pointer-events: auto;
position: absolute;
background: rgba(255, 255, 255, 0.3);
border: 1px solid rgba(0, 0, 0, 0.3);
font-size: 12pt;
}
.post-note:focus {
border-color: rgba(255, 0, 0, 0.3);
background-color: rgba(255, 225, 225, 0.3);
}
.post-note .text-wrapper {
position: absolute;
@ -236,15 +263,13 @@
width: -webkit-max-content;
width: -moz-max-content;
width: max-content;
max-width: 22.5em;
}
.post-note .text {
padding: 0.5em;
background: lemonchiffon;
border: 1px solid black;
}
.post-note:hover .text-wrapper {
display: block;
}
.post-note .text p:first-of-type {
margin-top: 0;

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

@ -39,12 +39,14 @@
#user-list .user img {
vertical-align: top;
margin-right: 1em;
display: block;
}
#user-list .user>a {
display: block;
#user-list .user .avatar {
float: left;
margin-right: 1em;
}
#user-list .user .avatar a {
display: block;
}
#user-list .user .details {
float: left;
@ -54,5 +56,5 @@
#user-list .user h1 {
margin-top: 0;
font-weight: normal;
font-size: 16pt;
font-size: 1.25em;
}

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">
@ -138,7 +154,7 @@
<script type="text/javascript" src="/js/Presenters/PostPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/GlobalCommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/PostCommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/CommentListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/TagListPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/TagPresenter.js"></script>

View File

@ -73,7 +73,7 @@ App.API = function(_, jQuery, promise, appState) {
var xhr = null;
var apiPromise = promise.make(function(resolve, reject) {
xhr = jQuery.ajax({
var options = {
headers: {
'X-Authorization-Token': appState.get('loginToken') || '',
},
@ -92,7 +92,13 @@ App.API = function(_, jQuery, promise, appState) {
type: method,
url: fullUrl,
data: data,
});
cache: false,
};
if (data instanceof FormData) {
options.processData = false;
options.contentType = false;
}
xhr = jQuery.ajax(options);
});
apiPromise.xhr = xhr;
return apiPromise;

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,
};
}
@ -70,7 +73,7 @@ App.BrowsingSettings = function(
var formData = {
browsingSettings: JSON.stringify(settings),
};
return api.put('/users/' + user.name, formData);
return api.post('/users/' + user.name, formData);
}
function save() {
@ -90,7 +93,6 @@ App.BrowsingSettings = function(
getSettings: getSettings,
setSettings: setSettings,
};
};
App.DI.registerSingleton('browsingSettings', ['promise', 'auth', 'api'], App.BrowsingSettings);

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,12 +19,15 @@ App.Controls.AutoCompleteInput = function($input) {
maxResults: 15,
minLengthToArbitrarySearch: 3,
onApply: null,
onDelete: null,
onRender: null,
additionalFilter: null,
};
var showTimeout = null;
var cachedSource = null;
var results = [];
var activeResult = -1;
var monitorInputHidingInterval = null;
if ($input.length === 0) {
throw new Error('Input element was not found');
@ -61,27 +66,30 @@ App.Controls.AutoCompleteInput = function($input) {
}
$input.bind('keydown', function(e) {
var func = null;
if (isShown() && e.which === KEY_ESCAPE) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
hide();
func = hide;
} else if (isShown() && e.which === KEY_TAB) {
if (e.shiftKey) {
func = selectPrevious;
} else {
func = selectNext;
}
} else if (isShown() && e.which === KEY_DOWN) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectNext();
func = selectNext;
} else if (isShown() && e.which === KEY_UP) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
selectPrevious();
func = selectPrevious;
} else if (isShown() && e.which === KEY_RETURN && activeResult >= 0) {
func = function() { applyAutocomplete(); hide(); };
} else if (isShown() && e.which === KEY_DELETE && activeResult >= 0) {
func = function() { applyDelete(); hide(); };
}
if (func !== null) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
applyAutocomplete();
hide();
func();
} else {
window.clearTimeout(showTimeout);
showTimeout = window.setTimeout(showOrHide, 250);
@ -133,6 +141,7 @@ App.Controls.AutoCompleteInput = function($input) {
function hide() {
$div.hide();
window.clearInterval(monitorInputHidingInterval);
}
function selectPrevious() {
@ -179,6 +188,12 @@ App.Controls.AutoCompleteInput = function($input) {
}
}
function applyDelete() {
if (options.onDelete) {
options.onDelete(results[activeResult].tag);
}
}
function applyAutocomplete() {
if (options.onApply) {
options.onApply(results[activeResult].tag);
@ -222,12 +237,22 @@ App.Controls.AutoCompleteInput = function($input) {
});
$list.append($listItem);
});
if (options.onRender) {
options.onRender($list);
}
refreshActiveResult();
var x = $input.offset().left;
var y = $input.offset().top + $input.outerHeight() - 2;
if (y + $div.height() > window.innerHeight) {
y = $input.offset().top - $div.height();
}
$div.css({
left: ($input.offset().left) + 'px',
top: ($input.offset().top + $input.outerHeight() - 2) + 'px',
left: x + 'px',
top: y + 'px',
});
$div.show();
monitorInputHiding();
}
function refreshActiveResult() {
@ -237,5 +262,13 @@ App.Controls.AutoCompleteInput = function($input) {
}
}
function monitorInputHiding() {
monitorInputHidingInterval = window.setInterval(function() {
if (!$input.is(':visible')) {
hide();
}
}, 100);
}
return options;
};

View File

@ -14,6 +14,14 @@ App.Controls.TagInput = function($underlyingInput) {
var tagConfirmKeys = [KEY_RETURN, KEY_SPACE];
var inputConfirmKeys = [KEY_RETURN];
var SOURCE_INITIAL_TEXT = 1;
var SOURCE_AUTOCOMPLETION = 2;
var SOURCE_PASTE = 3;
var SOURCE_IMPLICATIONS = 4;
var SOURCE_INPUT_BLUR = 5;
var SOURCE_INPUT_ENTER = 6;
var SOURCE_SUGGESTIONS = 7;
var tags = [];
var options = {
beforeTagAdded: null,
@ -23,7 +31,9 @@ App.Controls.TagInput = function($underlyingInput) {
var $wrapper = jQuery('<div class="tag-input">');
var $tagList = jQuery('<ul class="tags">');
var $input = jQuery('<input class="tag-real-input" type="text"/>');
var tagInputId = 'tags' + Math.random();
var $label = jQuery('<label for="' + tagInputId + '" style="display: none">Tags:</label>');
var $input = jQuery('<input class="tag-real-input" type="text" id="' + tagInputId + '"/>');
var $siblings = jQuery('<div class="related-tags"><span>Sibling tags:</span><ul>');
var $suggestions = jQuery('<div class="related-tags"><span>Suggested tags:</span><ul>');
init();
@ -46,6 +56,7 @@ App.Controls.TagInput = function($underlyingInput) {
function render() {
$underlyingInput.hide();
$wrapper.append($tagList);
$wrapper.append($label);
$wrapper.append($input);
$wrapper.insertAfter($underlyingInput);
$wrapper.click(function(e) {
@ -56,18 +67,22 @@ App.Controls.TagInput = function($underlyingInput) {
$input.focus();
});
$input.attr('placeholder', $underlyingInput.attr('placeholder'));
$suggestions.insertAfter($wrapper);
$siblings.insertAfter($wrapper);
$suggestions.insertAfter($wrapper);
processText($underlyingInput.val(), addTagDirectly);
processText($underlyingInput.val(), SOURCE_INITIAL_TEXT);
$underlyingInput.val('');
}
function initAutoComplete() {
var autoComplete = new App.Controls.AutoCompleteInput($input);
autoComplete.onDelete = function(text) {
removeTag(text);
$input.val('');
};
autoComplete.onApply = function(text) {
processText(text, addTag);
processText(text, SOURCE_AUTOCOMPLETION);
$input.val('');
};
autoComplete.additionalFilter = function(results) {
@ -75,6 +90,14 @@ App.Controls.TagInput = function($underlyingInput) {
return !_.contains(getTags(), resultItem[0]);
});
};
autoComplete.onRender = function($list) {
$list.find('li').each(function() {
var $li = jQuery(this);
if (isTaggedWith($li.attr('data-key'))) {
$li.css('opacity', '0.5');
}
});
};
}
$input.bind('focus', function(e) {
@ -83,7 +106,7 @@ App.Controls.TagInput = function($underlyingInput) {
$input.bind('blur', function(e) {
$wrapper.removeClass('focused');
var tagName = $input.val();
addTag(tagName);
addTag(tagName, SOURCE_INPUT_BLUR);
$input.val('');
});
@ -96,12 +119,12 @@ App.Controls.TagInput = function($underlyingInput) {
pastedText = (e.originalEvent || e).clipboardData.getData('text/plain');
}
if (pastedText.length > 200) {
if (pastedText.length > 2000) {
window.alert('Pasted text is too long.');
return;
}
processTextWithoutLast(pastedText, addTag);
processTextWithoutLast(pastedText, SOURCE_PASTE);
});
$input.bind('keydown', function(e) {
@ -114,7 +137,7 @@ App.Controls.TagInput = function($underlyingInput) {
var tagName = $input.val();
e.preventDefault();
$input.val('');
addTag(tagName);
addTag(tagName, SOURCE_INPUT_ENTER);
} else if (e.which === KEY_BACKSPACE && jQuery(this).val().length === 0) {
e.preventDefault();
removeLastTag();
@ -127,19 +150,19 @@ App.Controls.TagInput = function($underlyingInput) {
});
}
function processText(text, callback) {
function processText(text, source) {
var tagNamesToAdd = explodeText(text);
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
}
function processTextWithoutLast(text, callback) {
function processTextWithoutLast(text, source) {
var tagNamesToAdd = explodeText(text);
var lastTagName = tagNamesToAdd.pop();
_.map(tagNamesToAdd, function(tagName) { callback(tagName); });
_.map(tagNamesToAdd, function(tagName) { addTag(tagName, source); });
$input.val(lastTagName);
}
function addTag(tagName) {
function addTag(tagName, source) {
tagName = tagName.trim();
if (tagName.length === 0) {
return;
@ -157,41 +180,55 @@ App.Controls.TagInput = function($underlyingInput) {
if (isTaggedWith(tagName)) {
flashTagRed(tagName);
} else {
beforeTagAdded(tagName);
beforeTagAdded(tagName, source);
var exportedTag = getExportedTag(tagName);
if (!exportedTag || !exportedTag.banned) {
addTagDirectly(tagName);
}
afterTagAdded(tagName);
}
}
function addTagDirectly(tagName) {
tags.push(tagName);
var $elem = createListElement(tagName);
$tagList.append($elem);
}
function beforeTagAdded(tagName) {
afterTagAdded(tagName, source);
}
}
function beforeTagRemoved(tagName) {
if (typeof(options.beforeTagRemoved) === 'function') {
options.beforeTagRemoved(tagName);
}
}
function afterTagRemoved(tagName) {
refreshShownSiblings();
}
function beforeTagAdded(tagName, source) {
if (typeof(options.beforeTagAdded) === 'function') {
options.beforeTagAdded(tagName);
}
}
function afterTagAdded(tagName) {
function afterTagAdded(tagName, source) {
if (source === SOURCE_IMPLICATIONS) {
flashTagYellow(tagName);
} else if (source !== SOURCE_INITIAL_TEXT) {
var tag = getExportedTag(tagName);
if (tag) {
_.each(tag.implications, function(impliedTagName) {
addTag(impliedTagName);
flashTagYellow(impliedTagName);
if (!isTaggedWith(impliedTagName)) {
addTag(impliedTagName, SOURCE_IMPLICATIONS);
}
});
showOrHideSuggestions(tag.suggestions);
if (source !== SOURCE_IMPLICATIONS && source !== SOURCE_SUGGESTIONS) {
showOrHideSuggestions(tagName);
refreshShownSiblings();
}
} else {
flashTagGreen(tagName);
}
}
}
function getExportedTag(tagName) {
return _.first(_.filter(
@ -205,10 +242,9 @@ App.Controls.TagInput = function($underlyingInput) {
var oldTagNames = getTags();
var newTagNames = _.without(oldTagNames, tagName);
if (newTagNames.length !== oldTagNames.length) {
if (typeof(options.beforeTagRemoved) === 'function') {
options.beforeTagRemoved(tagName);
}
beforeTagRemoved(tagName);
setTags(newTagNames);
afterTagRemoved(tagName);
}
}
@ -259,10 +295,11 @@ App.Controls.TagInput = function($underlyingInput) {
$elem.attr('data-tag', tagName.toLowerCase());
var $tagLink = jQuery('<a class="tag">');
$tagLink.text(tagName);
$tagLink.text(tagName + ' ' /* for easy copying */);
$tagLink.click(function(e) {
e.preventDefault();
showOrHideTagSiblings(tagName);
showOrHideSiblings(tagName);
showOrHideSuggestions(tagName);
});
$elem.append($tagLink);
@ -276,19 +313,13 @@ App.Controls.TagInput = function($underlyingInput) {
return $elem;
}
function showOrHideSuggestions(suggestedTagNames) {
if (_.size(suggestedTagNames) === 0) {
return;
function showOrHideSuggestions(tagName) {
var tag = getExportedTag(tagName);
var suggestions = tag ? tag.suggestions : [];
updateSuggestions($suggestions, suggestions);
}
var suggestions = filterSuggestions(suggestedTagNames);
if (suggestions.length > 0) {
attachTagsToSuggestionList($suggestions.find('ul'), suggestions);
$suggestions.slideDown('fast');
}
}
function showOrHideTagSiblings(tagName) {
function showOrHideSiblings(tagName) {
if ($siblings.data('lastTag') === tagName && $siblings.is(':visible')) {
$siblings.slideUp('fast');
$siblings.data('lastTag', null);
@ -298,22 +329,23 @@ App.Controls.TagInput = function($underlyingInput) {
promise.wait(getSiblings(tagName), promise.make(function(resolve, reject) {
$siblings.slideUp('fast', resolve);
})).then(function(siblings) {
siblings = _.pluck(siblings, 'name');
$siblings.data('lastTag', tagName);
if (!_.size(siblings)) {
return;
}
var suggestions = filterSuggestions(_.pluck(siblings, 'name'));
if (suggestions.length > 0) {
attachTagsToSuggestionList($siblings.find('ul'), suggestions);
$siblings.slideDown('fast');
}
$siblings.data('siblings', siblings);
updateSuggestions($siblings, siblings);
}).fail(function() {
});
}
function refreshShownSiblings() {
updateSuggestions($siblings, $siblings.data('siblings'));
}
function updateSuggestions($target, suggestedTagNames) {
function filterSuggestions(sourceTagNames) {
if (!sourceTagNames) {
return [];
}
var tagNames = _.filter(sourceTagNames.slice(), function(tagName) {
return !isTaggedWith(tagName);
});
@ -329,7 +361,7 @@ App.Controls.TagInput = function($underlyingInput) {
$a.text(tagName);
$a.click(function(e) {
e.preventDefault();
addTag(tagName);
addTag(tagName, SOURCE_SUGGESTIONS);
$li.fadeOut('fast', function() {
$li.remove();
if ($list.children().length === 0) {
@ -342,11 +374,20 @@ App.Controls.TagInput = function($underlyingInput) {
});
}
var suggestions = filterSuggestions(suggestedTagNames);
if (suggestions.length > 0) {
attachTagsToSuggestionList($target.find('ul'), suggestions);
$target.slideDown('fast');
} else {
$target.slideUp('fast');
}
}
function getSiblings(tagName) {
return promise.make(function(resolve, reject) {
promise.wait(api.get('/tags/' + tagName + '/siblings'))
.then(function(response) {
resolve(response.json.data);
resolve(response.json.tags);
}).fail(function() {
reject();
});
@ -364,6 +405,7 @@ App.Controls.TagInput = function($underlyingInput) {
function hideSuggestions() {
$siblings.hide();
$suggestions.hide();
$siblings.data('siblings', []);
}
_.extend(options, {

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

@ -0,0 +1,232 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.CommentListPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
messagePresenter) {
var $el;
var privileges;
var templates = {};
var post;
var comments = [];
function init(params, loaded) {
$el = params.$target;
post = params.post;
comments = params.comments || [];
privileges = {
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
canEditOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
canEditAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
canDeleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
canDeleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
canViewUsers: auth.hasPrivilege(auth.privileges.viewUsers),
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
};
promise.wait(
util.promiseTemplate('comment-list'),
util.promiseTemplate('comment-list-item'),
util.promiseTemplate('comment-form'))
.then(function(
commentListTemplate,
commentListItemTemplate,
commentFormTemplate)
{
templates.commentList = commentListTemplate;
templates.commentListItem = commentListItemTemplate;
templates.commentForm = commentFormTemplate;
render();
loaded();
if (comments.length === 0) {
promise.wait(api.get('/comments/' + params.post.id))
.then(function(response) {
comments = response.json.comments;
render();
}).fail(function() {
console.log(arguments);
});
}
})
.fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.commentList(
_.extend(
{
commentListItemTemplate: templates.commentListItem,
commentFormTemplate: templates.commentForm,
util: util,
comments: comments,
post: post,
},
privileges)));
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
renderComments(comments);
}
function renderComments(comments) {
var $target = $el.find('.comments');
var $targetList = $el.find('ul');
if (comments.length > 0) {
$target.show();
} else {
$target.hide();
}
$targetList.empty();
_.each(comments, function(comment) {
renderComment($targetList, comment);
});
}
function renderComment($targetList, comment) {
var $item = jQuery('<li>' + templates.commentListItem({
comment: comment,
util: util,
canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.canEditOwnComments : privileges.canEditAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.canDeleteOwnComments : privileges.canDeleteAllComments,
canViewUsers: privileges.canViewUsers,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
util.loadImagesNicely($item.find('img'));
$targetList.append($item);
$item.find('a.edit').click(function(e) {
e.preventDefault();
editCommentStart($item, comment);
});
$item.find('a.delete').click(function(e) {
e.preventDefault();
deleteComment(comment);
});
$item.find('a.score-up').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
});
$item.find('a.score-down').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
});
}
function commentFormSubmitted(e, comment) {
e.preventDefault();
var $button = jQuery(e.target);
var $form = $button.parents('form');
var sender = $button.val();
if (sender === 'preview') {
previewComment($form);
} else {
submitComment($form, comment);
}
}
function previewComment($form) {
var $preview = $form.find('.preview');
$preview.slideUp('fast', function() {
$preview.html(util.formatMarkdown($form.find('textarea').val()));
$preview.slideDown('fast');
});
}
function updateComment(comment) {
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
render();
}
function addComment(comment) {
comments.push(comment);
render();
}
function submitComment($form, commentToEdit) {
$form.find('.preview').slideUp();
var $textarea = $form.find('textarea');
var data = {text: $textarea.val()};
var p;
if (commentToEdit) {
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
} else {
p = promise.wait(api.post('/comments/' + post.id, data));
}
p.then(function(response) {
$textarea.val('');
var comment = response.json.comment;
if (commentToEdit) {
$form.slideUp(function() {
$form.remove();
});
updateComment(comment);
} else {
addComment(comment);
}
}).fail(showGenericError);
}
function editCommentStart($item, comment) {
if ($item.find('.comment-form').length > 0) {
return;
}
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
$item.find('.body').append($form);
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
}
function deleteComment(comment) {
if (!window.confirm('Are you sure you want to delete this comment?')) {
return;
}
promise.wait(api.delete('/comments/' + comment.id))
.then(function(response) {
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
renderComments(comments);
}).fail(showGenericError);
}
function score(comment, scoreValue) {
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
.then(function(response) {
comment.score = parseInt(response.json.score);
comment.ownScore = parseInt(response.json.ownScore);
updateComment(comment);
}).fail(showGenericError);
}
function showGenericError(response) {
window.alert(response.json && response.json.error || response);
}
return {
init: init,
render: render,
};
};
App.DI.register('commentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.CommentListPresenter);

View File

@ -5,17 +5,23 @@ App.Presenters.GlobalCommentListPresenter = function(
_,
jQuery,
util,
auth,
promise,
pagerPresenter,
topNavigationPresenter) {
var $el;
var privileges;
var templates = {};
function init(params, loaded) {
$el = jQuery('#content');
topNavigationPresenter.select('comments');
privileges = {
canViewPosts: auth.hasPrivilege(auth.privileges.viewPosts),
};
promise.wait(
util.promiseTemplate('global-comment-list'),
util.promiseTemplate('global-comment-list-item'),
@ -32,8 +38,8 @@ App.Presenters.GlobalCommentListPresenter = function(
baseUri: '#/comments',
backendUri: '/comments',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderComments($page, data.entities);
updateCallback: function($page, response) {
renderComments($page, response.json.comments);
},
},
function() {
@ -47,7 +53,7 @@ App.Presenters.GlobalCommentListPresenter = function(
function reinit(params, loaded) {
pagerPresenter.reinit({query: params.query});
pagerPresenter.reinit({query: params.query || {}});
loaded();
}
@ -59,20 +65,20 @@ App.Presenters.GlobalCommentListPresenter = function(
$el.html(templates.list());
}
function renderComments($page, data) {
function renderComments($page, postComments) {
var $target = $page.find('.posts');
_.each(data, function(data) {
var post = data.post;
var comments = data.comments;
_.each(postComments, function(postComments) {
var post = postComments.post;
var comments = postComments.comments;
var $post = jQuery('<li>' + templates.listItem({
util: util,
post: post,
postTemplate: templates.post,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
util.loadImagesNicely($post.find('img'));
var presenter = App.DI.get('postCommentListPresenter');
var presenter = App.DI.get('commentListPresenter');
presenter.init({
post: post,
@ -95,4 +101,4 @@ App.Presenters.GlobalCommentListPresenter = function(
};
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);
App.DI.register('globalCommentListPresenter', ['_', 'jQuery', 'util', 'auth', 'promise', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.GlobalCommentListPresenter);

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,7 +62,7 @@ App.Presenters.HistoryPresenter = function(
function renderHistory($page, historyItems) {
$page.append(templates.history({
formatRelativeTime: util.formatRelativeTime,
util: util,
history: historyItems}));
}
@ -72,7 +72,6 @@ App.Presenters.HistoryPresenter = function(
deinit: deinit,
render: render,
};
};
App.DI.register('historyPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'pagerPresenter', 'topNavigationPresenter'], App.Presenters.HistoryPresenter);

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

@ -1,230 +0,0 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PostCommentListPresenter = function(
_,
jQuery,
util,
promise,
api,
auth,
topNavigationPresenter,
messagePresenter) {
var $el;
var privileges;
var templates = {};
var post;
var comments = [];
function init(params, loaded) {
$el = params.$target;
post = params.post;
comments = params.comments || [];
privileges = {
canListComments: auth.hasPrivilege(auth.privileges.listComments),
canAddComments: auth.hasPrivilege(auth.privileges.addComments),
editOwnComments: auth.hasPrivilege(auth.privileges.editOwnComments),
editAllComments: auth.hasPrivilege(auth.privileges.editAllComments),
deleteOwnComments: auth.hasPrivilege(auth.privileges.deleteOwnComments),
deleteAllComments: auth.hasPrivilege(auth.privileges.deleteAllComments),
};
promise.wait(
util.promiseTemplate('post-comment-list'),
util.promiseTemplate('comment-list-item'),
util.promiseTemplate('comment-form'))
.then(function(
commentListTemplate,
commentListItemTemplate,
commentFormTemplate)
{
templates.commentList = commentListTemplate;
templates.commentListItem = commentListItemTemplate;
templates.commentForm = commentFormTemplate;
render();
loaded();
if (comments.length === 0) {
promise.wait(api.get('/comments/' + params.post.id))
.then(function(response) {
comments = response.json.data;
render();
}).fail(function() {
console.log(arguments);
});
}
})
.fail(function() {
console.log(arguments);
loaded();
});
}
function render() {
$el.html(templates.commentList(
_.extend(
{
commentListItemTemplate: templates.commentListItem,
commentFormTemplate: templates.commentForm,
formatRelativeTime: util.formatRelativeTime,
formatMarkdown: util.formatMarkdown,
comments: comments,
post: post,
},
privileges)));
$el.find('.comment-add form button[type=submit]').click(function(e) { commentFormSubmitted(e, null); });
renderComments(comments);
}
function renderComments(comments) {
var $target = $el.find('.comments');
var $targetList = $el.find('ul');
if (comments.length > 0) {
$target.show();
} else {
$target.hide();
}
$targetList.empty();
_.each(comments, function(comment) {
renderComment($targetList, comment);
});
}
function renderComment($targetList, comment) {
var $item = jQuery('<li>' + templates.commentListItem({
comment: comment,
formatRelativeTime: util.formatRelativeTime,
formatMarkdown: util.formatMarkdown,
canVote: auth.isLoggedIn(),
canEditComment: auth.isLoggedIn(comment.user.name) ? privileges.editOwnComments : privileges.editAllComments,
canDeleteComment: auth.isLoggedIn(comment.user.name) ? privileges.deleteOwnComments : privileges.deleteAllComments,
}) + '</li>');
util.loadImagesNicely($item.find('img'));
$targetList.append($item);
$item.find('a.edit').click(function(e) {
e.preventDefault();
editCommentStart($item, comment);
});
$item.find('a.delete').click(function(e) {
e.preventDefault();
deleteComment(comment);
});
$item.find('a.score-up').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : 1);
});
$item.find('a.score-down').click(function(e) {
e.preventDefault();
score(comment, jQuery(this).hasClass('active') ? 0 : -1);
});
}
function commentFormSubmitted(e, comment) {
e.preventDefault();
var $button = jQuery(e.target);
var $form = $button.parents('form');
var sender = $button.val();
if (sender === 'preview') {
previewComment($form);
} else {
submitComment($form, comment);
}
}
function previewComment($form) {
var $preview = $form.find('.preview');
$preview.slideUp('fast', function() {
$preview.html(util.formatMarkdown($form.find('textarea').val()));
$preview.slideDown('fast');
});
}
function updateComment(comment) {
comments = _.map(comments, function(c) { return c.id === comment.id ? comment : c; });
render();
}
function addComment(comment) {
comments.push(comment);
render();
}
function submitComment($form, commentToEdit) {
$form.find('.preview').slideUp();
var $textarea = $form.find('textarea');
var data = {text: $textarea.val()};
var p;
if (commentToEdit) {
p = promise.wait(api.put('/comments/' + commentToEdit.id, data));
} else {
p = promise.wait(api.post('/comments/' + post.id, data));
}
p.then(function(response) {
$textarea.val('');
var comment = response.json;
if (commentToEdit) {
$form.slideUp(function() {
$form.remove();
});
updateComment(comment);
} else {
addComment(comment);
}
}).fail(showGenericError);
}
function editCommentStart($item, comment) {
if ($item.find('.comment-form').length > 0) {
return;
}
var $form = jQuery(templates.commentForm({title: 'Edit comment', text: comment.text}));
$item.find('.body').append($form);
$item.find('form button[type=submit]').click(function(e) { commentFormSubmitted(e, comment); });
}
function deleteComment(comment) {
if (!window.confirm('Are you sure you want to delete this comment?')) {
return;
}
promise.wait(api.delete('/comments/' + comment.id))
.then(function(response) {
comments = _.filter(comments, function(c) { return c.id !== comment.id; });
renderComments(comments);
}).fail(showGenericError);
}
function score(comment, scoreValue) {
promise.wait(api.post('/comments/' + comment.id + '/score', {score: scoreValue}))
.then(function(response) {
comment.score = parseInt(response.json.score);
comment.ownScore = parseInt(response.json.ownScore);
updateComment(comment);
}).fail(showGenericError);
}
function showGenericError(response) {
window.alert(response.json && response.json.error || response);
}
return {
init: init,
render: render,
};
};
App.DI.register('postCommentListPresenter', ['_', 'jQuery', 'util', 'promise', 'api', 'auth', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.PostCommentListPresenter);

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();
@ -75,15 +95,11 @@ App.Presenters.PostEditPresenter = function(
}
function postContentChanged(files) {
postContentFileDropper.readAsDataURL(files[0], function(content) {
postContent = content;
});
postContent = files[0];
}
function postThumbnailChanged(files) {
postThumbnailFileDropper.readAsDataURL(files[0], function(content) {
postThumbnail = content;
});
postThumbnail = files[0];
}
function getPrivileges() {
@ -92,37 +108,36 @@ App.Presenters.PostEditPresenter = function(
function editPost() {
var $form = $target.find('form');
var formData = {};
formData.seenEditTime = post.lastEditTime;
formData.flags = {};
var formData = new FormData();
formData.append('lastEditTime', post.lastEditTime);
if (privileges.canChangeContent && postContent) {
formData.content = postContent;
formData.append('content', postContent);
}
if (privileges.canChangeThumbnail && postThumbnail) {
formData.thumbnail = postThumbnail;
formData.append('thumbnail', postThumbnail);
}
if (privileges.canChangeSource) {
formData.source = $form.find('[name=source]').val();
formData.append('source', $form.find('[name=source]').val());
}
if (privileges.canChangeSafety) {
formData.safety = $form.find('[name=safety]:checked').val();
formData.append('safety', $form.find('[name=safety]:checked').val());
}
if (privileges.canChangeTags) {
formData.tags = tagInput.getTags().join(' ');
formData.append('tags', tagInput.getTags().join(' '));
}
if (privileges.canChangeRelations) {
formData.relations = $form.find('[name=relations]').val();
formData.append('relations', $form.find('[name=relations]').val());
}
if (privileges.canChangeFlags) {
if (post.contentType === 'video') {
formData.flags.loop = $form.find('[name=loop]').is(':checked') ? 1 : 0;
formData.append('loop', $form.find('[name=loop]').is(':checked') ? 1 : 0);
}
}
@ -131,11 +146,14 @@ App.Presenters.PostEditPresenter = function(
return;
}
promise.wait(api.put('/posts/' + post.id, formData))
jQuery(document.activeElement).blur();
promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) {
tagList.refreshTags();
post = response.json.post;
if (typeof(updateCallback) !== 'undefined') {
updateCallback(post = response.json);
updateCallback(post);
}
}).fail(function(response) {
showEditError(response);
@ -155,4 +173,4 @@ App.Presenters.PostEditPresenter = function(
};
App.DI.register('postEditPresenter', ['util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);
App.DI.register('postEditPresenter', ['jQuery', 'util', 'promise', 'api', 'auth', 'tagList'], App.Presenters.PostEditPresenter);

View File

@ -29,6 +29,7 @@ App.Presenters.PostListPresenter = function(
params.query = params.query || {};
privileges.canMassTag = auth.hasPrivilege(auth.privileges.massTag);
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
promise.wait(
util.promiseTemplate('post-list'),
@ -44,8 +45,8 @@ App.Presenters.PostListPresenter = function(
baseUri: '#/posts',
backendUri: '/posts',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderPosts($page, data.entities);
updateCallback: function($page, response) {
renderPosts($page, response.json.posts);
},
},
function() {
@ -165,6 +166,7 @@ App.Presenters.PostListPresenter = function(
util: util,
query: params.query,
post: post,
canViewPosts: privileges.canViewPosts,
}) + '</li>');
$post.data('post', post);
util.loadImagesNicely($post.find('img'));
@ -215,11 +217,11 @@ App.Presenters.PostListPresenter = function(
tags.push(params.query.massTag);
}
var formData = {};
formData.seenEditTime = post.lastEditTime;
formData.lastEditTime = post.lastEditTime;
formData.tags = tags.join(' ');
promise.wait(api.put('/posts/' + post.id, formData))
promise.wait(api.post('/posts/' + post.id, formData))
.then(function(response) {
post = response.json;
post = response.json.post;
$post.data('post', post);
softRenderPost($post);
}).fail(function(response) {

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

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,35 +233,75 @@ App.Presenters.PostUploadPresenter = function(
stopUpload();
}
function addPostFromFile(file) {
var post = _.extend({}, getDefaultPost(), {fileName: file.name});
fileDropper.readAsDataURL(file, function(content) {
post.content = content;
if (file.type.match('image.*')) {
post.thumbnail = content;
postThumbnailLoaded(post);
function makeThumbnail(thumbnailWidth, thumbnailHeight, file) {
return promise.makeSilent(function(resolve, reject) {
var canvas = document.createElement('canvas');
var img = new Image();
canvas.width = thumbnailWidth;
canvas.height = thumbnailHeight;
var context = canvas.getContext('2d');
img.onload = function() {
//memory still leaks...
img.onload = null;
context.drawImage(img, 0, 0, thumbnailWidth, thumbnailHeight);
URL.revokeObjectURL(img.src);
img.src = null;
resolve(canvas.toDataURL());
};
img.src = URL.createObjectURL(file);
});
}
function addPostFromFile(file) {
var post = _.extend({}, getDefaultPost(), {
fileName: file.name,
file: file,
getThumbnail: function(thumbnailWidth, thumbnailHeight) {
return promise.makeSilent(function(resolve, reject) {
if (!file.type.match('image.*')) {
resolve(null);
return;
}
if (thumbnailWidth === null || thumbnailHeight === null) {
resolve(URL.createObjectURL(post.file));
return;
}
makeThumbnail(thumbnailWidth, thumbnailHeight, post.file)
.then(function(thumbnailDataURL) {
resolve(thumbnailDataURL);
});
});
},
});
postAdded(post);
return post;
}
function addPostFromUrl(url) {
var post = _.extend({}, getDefaultPost(), {url: url, fileName: url});
postAdded(post);
setPostsSource([post], url);
function addPostFromURL(url) {
var post = _.extend({}, getDefaultPost(), {
url: url,
fileName: url,
});
var matches = url.match(/watch.*?=([a-zA-Z0-9_-]+)/);
if (matches) {
var youtubeThumbnailUrl = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
post.thumbnail = youtubeThumbnailUrl;
postThumbnailLoaded(post);
var youtubeThumbnailURL = 'http://img.youtube.com/vi/' + matches[1] + '/mqdefault.jpg';
post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
return promise.makeSilent(function(resolve, reject) {
resolve(youtubeThumbnailURL);
});
};
} else if (url.match(/image|img|jpg|png|gif/i)) {
post.thumbnail = url;
postThumbnailLoaded(post);
post.getThumbnail = function(thumbnailWidth, thumbnailHeight) {
return promise.makeSilent(function(resolve, reject) {
resolve(url);
});
};
}
postAdded(post);
setPostsSource([post], url);
return post;
}
@ -295,9 +313,8 @@ App.Presenters.PostUploadPresenter = function(
$row.removeClass('template');
$row.find('td:not(.checkbox)').click(postTableRowClicked);
$row.find('a').click(postTableRowClicked);
$row.find('td.checkbox').click(postTableCheckboxClicked);
$row.find('img').mouseenter(postTableRowImageHovered);
$row.find('img').mouseleave(postTableRowImageUnhovered);
$row.data('post', post);
$table.find('tbody').append($row);
$row.find('td.checkbox input').attr('id', _.uniqueId());
@ -315,21 +332,31 @@ App.Presenters.PostUploadPresenter = function(
}
function updatePostThumbnailInForm(post) {
if (post.thumbnail === null) {
$el.find('.form-slider .thumbnail img').hide();
post.getThumbnail(null, null).then(function(thumbnailDataURL) {
var $thumbnail = $el.find('.form-slider .thumbnail');
var $img = $thumbnail.find('img');
var $link = $thumbnail.find('a');
if (thumbnailDataURL === null) {
$img.hide();
$link.hide();
} else {
$el.find('.form-slider .thumbnail img').show()[0].setAttribute('src', post.thumbnail);
$img.show();
$img.attr('src', thumbnailDataURL);
$link.show();
$link.attr('href', thumbnailDataURL);
}
});
}
function updatePostThumbnailInTable(post) {
post.getThumbnail(30, 30).then(function(thumbnailDataURL) {
var $row = post.$tableRow;
if (post.thumbnail === null) {
$row.find('img')[0].setAttribute('src', util.transparentPixel());
//huge speedup thanks to this condition
} else if ($row.find('img').attr('src') !== post.thumbnail) {
$row.find('img')[0].setAttribute('src', post.thumbnail);
if (thumbnailDataURL === null) {
$row.find('img').attr('src', util.transparentPixel());
} else {
$row.find('img').attr('src', thumbnailDataURL);
}
});
}
function getAllPosts() {
@ -361,6 +388,9 @@ App.Presenters.PostUploadPresenter = function(
showPostEditForm(selectedPosts);
}
$el.find('.post-table-op').prop('disabled', selectedPosts.length === 0);
if (selectedPosts.length === 1) {
updatePostThumbnailInForm(selectedPosts[0]);
}
}
function hidePostEditForm() {
@ -410,6 +440,17 @@ App.Presenters.PostUploadPresenter = function(
};
}
function getTagIndex(post, tag) {
var tags = jQuery.map(post.tags, function(tag) {
return tag.toLowerCase();
});
return tags.indexOf(tag.toLowerCase());
}
function hasTag(post, tag) {
return getTagIndex(post, tag) !== -1;
}
function getCombinedPost(posts) {
var combinedPost = _.extend({}, getDefaultPost());
if (posts.length === 0) {
@ -419,7 +460,7 @@ App.Presenters.PostUploadPresenter = function(
var tagFilter = function(post) {
return function(tag) {
return post.tags.indexOf(tag) !== -1;
return hasTag(post, tag);
};
};
@ -442,7 +483,6 @@ App.Presenters.PostUploadPresenter = function(
function setPostsSource(posts, newSource) {
_.each(posts, function(post) {
var maxSourceLength = 200;
console.log(newSource);
if (newSource.length > maxSourceLength) {
newSource = newSource.substring(0, maxSourceLength - 5) + '(...)';
}
@ -467,8 +507,7 @@ App.Presenters.PostUploadPresenter = function(
function addTagToPosts(posts, tag) {
jQuery.each(posts, function(i, post) {
var index = post.tags.indexOf(tag);
if (index === -1) {
if (!hasTag(post, tag)) {
post.tags.push(tag);
}
postChanged(post);
@ -477,9 +516,8 @@ App.Presenters.PostUploadPresenter = function(
function removeTagFromPosts(posts, tag) {
jQuery.each(posts, function(i, post) {
var index = post.tags.indexOf(tag);
if (index !== -1) {
post.tags.splice(index, 1);
if (hasTag(post, tag)) {
post.tags.splice(getTagIndex(post, tag), 1);
}
postChanged(post);
});
@ -502,10 +540,12 @@ App.Presenters.PostUploadPresenter = function(
function selectPrevPostTableRow() {
selectPostTableRow($el.find('tbody tr.selected:eq(0)').prev().data('post'));
return false;
}
function selectNextPostTableRow() {
selectPostTableRow($el.find('tbody tr.selected:eq(0)').next().data('post'));
return false;
}
function showOrHidePostsTable() {
@ -576,17 +616,17 @@ App.Presenters.PostUploadPresenter = function(
var post = posts[0];
var $row = post.$tableRow;
var formData = {};
var formData = new FormData();
if (post.url) {
formData.url = post.url;
formData.append('url', post.url);
} else {
formData.content = post.content;
formData.contentFileName = post.fileName;
formData.append('content', post.file);
formData.append('contentFileName', post.fileName);
}
formData.source = post.source;
formData.safety = post.safety;
formData.anonymous = (post.anonymous | 0);
formData.tags = post.tags.join(' ');
formData.append('source', post.source || '');
formData.append('safety', post.safety);
formData.append('anonymous', (post.anonymous | 0));
formData.append('tags', post.tags.join(' '));
if (post.tags.length === 0) {
showUploadError('No tags set.');

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

@ -10,8 +10,6 @@ App.Presenters.TagListPresenter = function(
pagerPresenter,
topNavigationPresenter) {
var KEY_RETURN = 13;
var $el = jQuery('#content');
var $searchInput;
var templates = {};
@ -38,8 +36,8 @@ App.Presenters.TagListPresenter = function(
baseUri: '#/tags',
backendUri: '/tags',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderTags($page, data.entities);
updateCallback: function($page, response) {
renderTags($page, response.json.tags);
},
},
function() {
@ -78,8 +76,8 @@ App.Presenters.TagListPresenter = function(
function render() {
$el.html(templates.list());
$searchInput = $el.find('input[name=query]');
$searchInput.keydown(searchInputKeyPressed);
$el.find('form').submit(searchFormSubmitted);
App.Controls.AutoCompleteInput($searchInput);
softRender();
}
@ -88,13 +86,6 @@ App.Presenters.TagListPresenter = function(
}
function searchInputKeyPressed(e) {
if (e.which !== KEY_RETURN) {
return;
}
updateSearch();
}
function searchFormSubmitted(e) {
e.preventDefault();
updateSearch();
@ -117,7 +108,7 @@ App.Presenters.TagListPresenter = function(
_.each(tags, function(tag) {
var $item = jQuery(templates.listItem({
tag: tag,
formatRelativeTime: util.formatRelativeTime,
util: util,
}));
$target.append($item);
});

View File

@ -38,6 +38,7 @@ App.Presenters.TagPresenter = function(
privileges.canViewHistory = auth.hasPrivilege(auth.privileges.viewHistory);
privileges.canDelete = auth.hasPrivilege(auth.privileges.deleteTags);
privileges.canMerge = auth.hasPrivilege(auth.privileges.mergeTags);
privileges.canViewPosts = auth.hasPrivilege(auth.privileges.viewPosts);
promise.wait(
util.promiseTemplate('tag'),
@ -65,9 +66,9 @@ App.Presenters.TagPresenter = function(
api.get('tags/' + tagName + '/siblings'),
api.get('posts', {query: tagName}))
.then(function(tagResponse, siblingsResponse, postsResponse) {
tag = tagResponse.json;
siblings = siblingsResponse.json.data;
posts = postsResponse.json.data;
tag = tagResponse.json.tag;
siblings = siblingsResponse.json.tags;
posts = postsResponse.json.posts;
posts = posts.slice(0, 8);
render();
@ -80,13 +81,22 @@ App.Presenters.TagPresenter = function(
});
}
function getTagCategories() {
var tagCategories = JSON.parse(jQuery('head').attr('data-tag-categories'));
var result = {};
jQuery.each(tagCategories, function(i, item) {
result[item[0]] = item[1];
});
return result;
}
function render() {
$el.html(templates.tag({
privileges: privileges,
tag: tag,
siblings: siblings,
tagCategories: JSON.parse(jQuery('head').attr('data-tag-categories')),
formatRelativeTime: util.formatRelativeTime,
tagCategories: getTagCategories(),
util: util,
historyTemplate: templates.history,
}));
$el.find('.post-list').hide();
@ -125,7 +135,8 @@ App.Presenters.TagPresenter = function(
promise.wait(api.put('/tags/' + tag.name, formData))
.then(function(response) {
router.navigateInplace('#/tag/' + response.json.name);
router.navigateInplace('#/tag/' + response.json.tag.name);
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
@ -138,6 +149,7 @@ App.Presenters.TagPresenter = function(
promise.wait(api.delete('/tags/' + tag.name))
.then(function(response) {
router.navigate('#/tags');
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
@ -149,6 +161,7 @@ App.Presenters.TagPresenter = function(
promise.wait(api.put('/tags/' + tag.name + '/merge', {targetTag: targetTag}))
.then(function(response) {
router.navigate('#/tags');
tagList.refreshTags();
}).fail(function(response) {
window.alert(response.json && response.json.error || 'An error occured.');
});
@ -162,6 +175,7 @@ App.Presenters.TagPresenter = function(
util: util,
post: post,
query: {query: tag.name},
canViewPosts: privileges.canViewPosts,
}) + '</li>');
$target.append($post);
});
@ -180,4 +194,16 @@ App.Presenters.TagPresenter = function(
};
App.DI.register('tagPresenter', ['_', 'jQuery', 'util', 'promise', 'auth', 'api', 'tagList', 'router', 'keyboard', 'topNavigationPresenter', 'messagePresenter'], App.Presenters.TagPresenter);
App.DI.register('tagPresenter', [
'_',
'jQuery',
'util',
'promise',
'auth',
'api',
'tagList',
'router',
'keyboard',
'topNavigationPresenter',
'messagePresenter'],
App.Presenters.TagPresenter);

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

@ -77,11 +77,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
}
function avatarContentChanged(files) {
if (files.length === 1) {
fileDropper.readAsDataURL(files[0], function(content) {
avatarContent = content;
});
}
avatarContent = files[0];
}
function accountSettingsFormSubmitted(e) {
@ -89,41 +85,45 @@ App.Presenters.UserAccountSettingsPresenter = function(
var $el = jQuery(target);
var $messages = jQuery(target).find('.messages');
messagePresenter.hideMessages($messages);
var formData = {};
var formData = new FormData();
if (privileges.canChangeAvatarStyle) {
formData.avatarStyle = $el.find('[name=avatar-style]:checked').val();
formData.append('avatarStyle', $el.find('[name=avatar-style]:checked').val());
if (avatarContent) {
formData.avatarContent = avatarContent;
formData.append('avatarContent', avatarContent);
}
}
if (privileges.canChangeName) {
formData.userName = $el.find('[name=userName]').val();
}
if (privileges.canChangeEmailAddress) {
formData.email = $el.find('[name=email]').val();
}
if (privileges.canChangePassword) {
formData.password = $el.find('[name=password]').val();
formData.passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
}
if (privileges.canChangeAccessRank) {
formData.accessRank = $el.find('[name=access-rank]:checked').val();
}
if (privileges.canBan) {
formData.banned = $el.find('[name=ban]').is(':checked') ? 1 : 0;
formData.append('userName', $el.find('[name=userName]').val());
}
if (!validateAccountSettingsFormData(formData)) {
if (privileges.canChangeEmailAddress) {
formData.append('email', $el.find('[name=email]').val());
}
if (privileges.canChangePassword) {
var password = $el.find('[name=password]').val();
var passwordConfirmation = $el.find('[name=passwordConfirmation]').val();
if (password) {
if (password !== passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return;
}
if (!formData.password) {
delete formData.password;
delete formData.passwordConfirmation;
formData.append('password', password);
}
}
promise.wait(api.put('/users/' + user.name, formData))
if (privileges.canChangeAccessRank) {
formData.append('accessRank', $el.find('[name=access-rank]:checked').val());
}
if (privileges.canBan) {
formData.append('banned', $el.find('[name=ban]').is(':checked') ? 1 : 0);
}
promise.wait(api.post('/users/' + user.name, formData))
.then(function(response) {
editSuccess(response);
}).fail(function(response) {
@ -133,7 +133,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
function editSuccess(apiResponse) {
var wasLoggedIn = auth.isLoggedIn(user.name);
user = apiResponse.json;
user = apiResponse.json.user;
if (wasLoggedIn) {
auth.updateCurrentUser(user);
}
@ -142,7 +142,7 @@ App.Presenters.UserAccountSettingsPresenter = function(
var $messages = jQuery(target).find('.messages');
var message = 'Account settings updated!';
if (!apiResponse.json.confirmed) {
if (!apiResponse.json.user.confirmed) {
message += '<br/>Check your inbox for activation e-mail.<br/>If e-mail doesn\'t show up, check your spam folder.';
}
messagePresenter.showInfo($messages, message);
@ -153,16 +153,6 @@ App.Presenters.UserAccountSettingsPresenter = function(
messagePresenter.showError($messages, apiResponse.json && apiResponse.json.error || apiResponse);
}
function validateAccountSettingsFormData(formData) {
var $messages = jQuery(target).find('.messages');
if (formData.password !== formData.passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return false;
}
return true;
}
return {
init: init,
render: render,

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

@ -13,11 +13,14 @@ App.Presenters.UserListPresenter = function(
var $el = jQuery('#content');
var templates = {};
var params;
var privileges = {};
function init(params, loaded) {
topNavigationPresenter.select('users');
topNavigationPresenter.changeTitle('Users');
privileges.canViewUsers = auth.hasPrivilege(auth.privileges.viewUsers);
promise.wait(
util.promiseTemplate('user-list'),
util.promiseTemplate('user-list-item'))
@ -32,8 +35,8 @@ App.Presenters.UserListPresenter = function(
baseUri: '#/users',
backendUri: '/users',
$target: $el.find('.pagination-target'),
updateCallback: function($page, data) {
renderUsers($page, data.entities);
updateCallback: function($page, response) {
renderUsers($page, response.json.users);
},
},
function() {
@ -60,7 +63,7 @@ App.Presenters.UserListPresenter = function(
}
function render() {
$el.html(templates.list());
$el.html(templates.list(privileges));
}
function updateActiveOrder(activeOrder) {
@ -71,10 +74,10 @@ App.Presenters.UserListPresenter = function(
function renderUsers($page, users) {
var $target = $page.find('.users');
_.each(users, function(user) {
var $item = jQuery('<li>' + templates.listItem({
var $item = jQuery('<li>' + templates.listItem(_.extend({
user: user,
formatRelativeTime: util.formatRelativeTime,
}) + '</li>');
util: util,
}, privileges)) + '</li>');
$target.append($item);
});
_.map(_.map($target.find('img'), jQuery), util.loadImagesNicely);

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,7 +74,7 @@ App.Presenters.UserPresenter = function(
$el.html(templates.user({
user: user,
isLoggedIn: auth.isLoggedIn(user.name),
formatRelativeTime: util.formatRelativeTime,
util: util,
canChangeBrowsingSettings: userBrowsingSettingsPresenter.getPrivileges().canChangeBrowsingSettings,
canChangeAccountSettings: _.any(userAccountSettingsPresenter.getPrivileges()),
canDeleteAccount: userAccountRemovalPresenter.getPrivileges().canDeleteAccount}));

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

@ -146,6 +146,11 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
return future ? 'in ' + text : text + ' ago';
}
function formatAbsoluteTime(timeString) {
var time = new Date(Date.parse(timeString));
return time.toString();
}
function formatUnits(number, base, suffixes, callback) {
if (!number && number !== 0) {
return NaN;
@ -188,11 +193,23 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
smartypants: true,
};
var sjis = [];
var preDecorator = function(text) {
text = text.replace(/\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig, function(match, capture) {
var ret = '%%%SJIS' + sjis.length;
sjis.push(capture);
return ret;
});
//prevent ^#... from being treated as headers, due to tag permalinks
text = text.replace(/^#/g, '%%%#');
//fix \ before ~ being stripped away
text = text.replace(/\\~/g, '%%%T');
//post, user and tags premalinks
text = text.replace(/(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g, '$1[$2]($2)');
text = text.replace(/\]\(@(\d+)\)/g, '](#/post/$1)');
text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](#/user/$1)');
text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](#/posts/query=$1)');
return text;
};
@ -201,19 +218,17 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
text = text.replace(/%%%T/g, '\\~');
text = text.replace(/%%%#/g, '#');
text = text.replace(/%%%SJIS(\d+)/, function(match, capture) { return '<div class="sjis">' + sjis[capture] + '</div>'; });
//search permalinks
text = text.replace(/\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig, '<a href="#/posts/query=$1"><code>$1</code></a>');
//spoilers
text = text.replace(/\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig, '<span class="spoiler">$1</span>');
//[small]
text = text.replace(/\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig, '<small>$1</small>');
//strike-through
text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1<del>$3</del>');
text = text.replace(/\\~/g, '~');
//post premalinks
text = text.replace(/(^|[\s<>\(\)\[\]])@(\d+)/g, '$1<a href="#/post/$2"><code>@$2</code></a>');
//user permalinks
text = text.replace(/(^|[\s<>\(\)\[\]])\+([a-zA-Z0-9_-]+)/g, '$1<a href="#/user/$2"><code>+$2</code></a>');
//tag permalinks
text = text.replace(/(^|[\s<>\(\)\[\]])\#([^\s<>/\\]+)/g, '$1<a href="#/posts/query=$2"><code>#$2</code></a>');
return text;
};
@ -230,9 +245,21 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
return result.slice(0, -1);
}
function simplifySearchQuery(query) {
if (typeof(query) === 'undefined') {
return {};
}
if (query.page === 1) {
delete query.page;
}
query = _.pick(query, _.identity); //remove falsy values
return query;
}
return {
promiseTemplate: promiseTemplate,
formatRelativeTime: formatRelativeTime,
formatAbsoluteTime: formatAbsoluteTime,
formatFileSize: formatFileSize,
formatMarkdown: formatMarkdown,
enableExitConfirmation: enableExitConfirmation,
@ -241,6 +268,7 @@ App.Util.Misc = function(_, jQuery, marked, promise) {
transparentPixel: transparentPixel,
loadImagesNicely: loadImagesNicely,
appendComplexRouteParam: appendComplexRouteParam,
simplifySearchQuery: simplifySearchQuery,
};
};

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

@ -1,6 +1,6 @@
<div class="comment">
<div class="avatar">
<% if (comment.user.name) { %>
<% if (comment.user.name && canViewUsers) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
@ -8,7 +8,7 @@
src="/data/thumbnails/40x40/avatars/<%= comment.user.name || '!' %>"
alt="<%= comment.user.name || 'Anonymous user' %>"/>
<% if (comment.user.name) { %>
<% if (comment.user.name && canViewUsers) { %>
</a>
<% } %>
</div>
@ -16,19 +16,19 @@
<div class="body">
<div class="header">
<span class="nickname">
<% if (comment.user.name) { %>
<% if (comment.user.name && canViewUsers) { %>
<a href="#/user/<%= comment.user.name %>">
<% } %>
<%= comment.user.name || 'Anonymous user' %>
<% if (comment.user.name) { %>
<% if (comment.user.name && canViewUsers) { %>
</a>
<% } %>
</span>
<span class="date" title="<%= comment.creationTime %>">
<%= formatRelativeTime(comment.creationTime) %>
<span class="date" title="<%= util.formatAbsoluteTime(comment.creationTime) %>">
<%= util.formatRelativeTime(comment.creationTime) %>
</span>
<span class="score">
@ -60,7 +60,7 @@
</div>
<div class="content">
<%= formatMarkdown(comment.text) %>
<%= util.formatMarkdown(comment.text) %>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
<% if (canListComments && comments.length) { %>
<div class="comments">
<h1>Comments</h1>
<ul class="comments">
</ul>
</div>
<% } %>
<% if (canAddComments) { %>
<div class="comment-add">
<%= commentFormTemplate({title: 'Add comment'}) %>
</div>
<% } %>

View File

@ -1,6 +1,6 @@
<div class="post-comment">
<div class="post">
<%= postTemplate({post: post, util: util}) %>
<%= postTemplate({post: post, util: util, canViewPosts: canViewPosts}) %>
</div>
<div class="post-comments-target">

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

@ -5,14 +5,25 @@ var reprValue = function(value) {
}
return JSON.stringify(value);
};
var showDifference = function(className, difference) {
_.each(difference, function(value, key) {
if (!Array.isArray(value)) {
value = [value];
}
_.each(value, function(v) {
%><li class="<%= className %> difference-<%= key %>"><%= key + ':' + reprValue(v) %></li><%
});
});
};
%>
<table class="history">
<tbody>
<% _.each(history, function( historyEntry) { %>
<tr>
<td class="time">
<%= formatRelativeTime(historyEntry.time) %>
<td class="time" title="<%= util.formatAbsoluteTime(historyEntry.time) %>">
<%= util.formatRelativeTime(historyEntry.time) %>
</td>
<td class="user">
@ -59,17 +70,8 @@ var reprValue = function(value) {
<% if (historyEntry.dataDifference) { %>
<ul><!--
--><% _.each(historyEntry.dataDifference['+'], function (difference) { %><!--
--><li class="addition difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
--></li><!--
--><% }) %><!--
--><% _.each(historyEntry.dataDifference['-'], function (difference) { %><!--
--><li class="removal difference-<%= difference[0] %>"><!--
--><%= difference[0] + ':' + reprValue(difference[1]) %><!--
--></li><!--
--><% }) %><!--
--><% showDifference('addition', historyEntry.dataDifference['+']) %><!--
--><% showDifference('removal', historyEntry.dataDifference['-']) %><!--
--></ul>
<% } %>
<% } %>

View File

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

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,13 +0,0 @@
<% if (canListComments && comments.length) { %>
<div class="comments">
<h1>Comments</h1>
<ul class="comments">
</ul>
</div>
<% } %>
<% if (canAddComments) { %>
<div class="comment-add">
<%= commentFormTemplate({title: 'Add comment'}) %>
</div>
<% } %>

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

@ -1,7 +1,12 @@
<div class="post-small post-type-<%= post.contentType %> ">
<% if (canViewPosts) { %>
<a class="link"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, typeof(query) !== 'undefined' ? query : {}) %>"
href="<%= util.appendComplexRouteParam('#/post/' + post.id, util.simplifySearchQuery(typeof(query) !== 'undefined' ? query : {})) %>"
title="<%= _.map(post.tags, function(tag) { return '#' + tag.name; }).join(', ') %>">
<% } else { %>
<span class="link">
<% } %>
<img width="160" height="160" class="thumb" src="/data/thumbnails/160x160/posts/<%= post.name %>" alt="<%= post.idMarkdown %>"/>
@ -31,7 +36,12 @@
</ul>
</div>
<% } %>
<% if (canViewPosts) { %>
</a>
<% } else { %>
</span>
<% } %>
<div class="action">
<button>Action</button>

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

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,7 +103,7 @@
<h3>History</h3>
<%= historyTemplate({
history: tag.history,
formatRelativeTime: formatRelativeTime
util: util,
}) %>
</div>
<% } %>

View File

@ -1,18 +1,29 @@
<div class="user">
<div class="avatar">
<% if (canViewUsers) { %>
<a href="#/user/<%= user.name %>">
<% } %>
<img width="80" height="80" src="/data/thumbnails/80x80/avatars/<%= user.name %>" alt="<%= user.name %>"/>
<% if (canViewUsers) { %>
</a>
<% } %>
</div>
<div class="details">
<h1>
<% if (canViewUsers) { %>
<a href="#/user/<%= user.name %>">
<%= user.name %>
</a>
<% } else { %>
<%= user.name %>
<% } %>
</h1>
<div class="date-joined" title="<%= user.registrationTime %>">
Joined: <%= formatRelativeTime(user.registrationTime) %>
<div class="date-joined" title="<%= util.formatAbsoluteTime(user.creationTime) %>">
Joined: <%= util.formatRelativeTime(user.creationTime) %>
</div>
<div class="date-seen">
Last seen: <%= formatRelativeTime(user.lastLoginTime) %>
<div class="date-seen" title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
Last seen: <%= util.formatRelativeTime(user.lastLoginTime) %>
</div>
</div>
</div>

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,12 +51,16 @@
<table>
<tr>
<td>Registered:</td>
<td><%= formatRelativeTime(user.registrationTime) %></td>
<td title="<%= util.formatAbsoluteTime(user.creationTime) %>">
<%= util.formatRelativeTime(user.creationTime) %>
</td>
</tr>
<tr>
<td>Seen:</td>
<td><%= formatRelativeTime(user.lastLoginTime) %></td>
<td title="<%= util.formatAbsoluteTime(user.lastLoginTime) %>">
<%= util.formatRelativeTime(user.lastLoginTime) %>
</td>
</tr>
<% if (user.accessRank) { %>

1
scripts/cron-globals.php → scripts/cron-globals Normal file → Executable file
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();
}
}

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 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,17 +0,0 @@
<?php
namespace Szurubooru;
class ControllerRepository
{
private $controllers = [];
public function __construct(array $controllers)
{
$this->controllers = $controllers;
}
public function getControllers()
{
return $this->controllers;
}
}

View File

@ -1,8 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Router;
abstract class AbstractController
{
abstract function registerRoutes(Router $router);
}

View File

@ -1,80 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\TokenViewProxy;
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
use Szurubooru\FormData\LoginFormData;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Router;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\TokenService;
use Szurubooru\Services\UserService;
final class AuthController extends AbstractController
{
private $authService;
private $userService;
private $tokenService;
private $privilegeService;
private $inputReader;
private $userViewProxy;
private $tokenViewProxy;
public function __construct(
AuthService $authService,
UserService $userService,
TokenService $tokenService,
PrivilegeService $privilegeService,
InputReader $inputReader,
UserViewProxy $userViewProxy,
TokenViewProxy $tokenViewProxy)
{
$this->authService = $authService;
$this->userService = $userService;
$this->tokenService = $tokenService;
$this->privilegeService = $privilegeService;
$this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy;
$this->tokenViewProxy = $tokenViewProxy;
}
public function registerRoutes(Router $router)
{
$router->post('/api/login', [$this, 'login']);
$router->put('/api/login', [$this, 'login']);
}
public function login()
{
if (isset($this->inputReader->userNameOrEmail) && isset($this->inputReader->password))
{
$formData = new LoginFormData($this->inputReader);
$this->authService->loginFromCredentials($formData);
$user = $this->authService->getLoggedInUser();
$this->userService->updateUserLastLoginTime($user);
}
elseif (isset($this->inputReader->token))
{
$token = $this->tokenService->getByName($this->inputReader->token);
$this->authService->loginFromToken($token);
$user = $this->authService->getLoggedInUser();
$isFromCookie = boolval($this->inputReader->isFromCookie);
if ($isFromCookie)
$this->userService->updateUserLastLoginTime($user);
}
else
{
$this->authService->loginAnonymous();
$user = $this->authService->getLoggedInUser();
}
return
[
'token' => $this->tokenViewProxy->fromEntity($this->authService->getLoginToken()),
'user' => $this->userViewProxy->fromEntity($user),
'privileges' => $this->privilegeService->getCurrentPrivileges(),
];
}
}

View File

@ -1,155 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\CommentViewProxy;
use Szurubooru\Controllers\ViewProxies\PostViewProxy;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\SearchServices\Filters\CommentFilter;
use Szurubooru\SearchServices\Filters\PostFilter;
use Szurubooru\SearchServices\Requirements\Requirement;
use Szurubooru\SearchServices\Requirements\RequirementRangedValue;
use Szurubooru\SearchServices\Requirements\RequirementSingleValue;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\CommentService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService;
final class CommentController extends AbstractController
{
private $privilegeService;
private $authService;
private $postService;
private $commentService;
private $commentViewProxy;
private $postViewProxy;
private $inputReader;
public function __construct(
PrivilegeService $privilegeService,
AuthService $authService,
PostService $postService,
CommentService $commentService,
CommentViewProxy $commentViewProxy,
PostViewProxy $postViewProxy,
InputReader $inputReader)
{
$this->privilegeService = $privilegeService;
$this->authService = $authService;
$this->postService = $postService;
$this->commentService = $commentService;
$this->commentViewProxy = $commentViewProxy;
$this->postViewProxy = $postViewProxy;
$this->inputReader = $inputReader;
}
public function registerRoutes(Router $router)
{
$router->get('/api/comments', [$this, 'getComments']);
$router->get('/api/comments/:postNameOrId', [$this, 'getPostComments']);
$router->post('/api/comments/:postNameOrId', [$this, 'addComment']);
$router->put('/api/comments/:commentId', [$this, 'editComment']);
$router->delete('/api/comments/:commentId', [$this, 'deleteComment']);
}
public function getComments()
{
$this->privilegeService->assertPrivilege(Privilege::LIST_COMMENTS);
$filter = new PostFilter();
$filter->setPageSize(10);
$filter->setPageNumber($this->inputReader->page);
$filter->setOrder([
PostFilter::ORDER_LAST_COMMENT_TIME =>
PostFilter::ORDER_DESC]);
$this->postService->decorateFilterFromBrowsingSettings($filter);
$requirement = new Requirement();
$requirement->setValue(new RequirementRangedValue());
$requirement->getValue()->setMinValue(1);
$requirement->setType(PostFilter::REQUIREMENT_COMMENT_COUNT);
$filter->addRequirement($requirement);
$result = $this->postService->getFiltered($filter);
$posts = $result->getEntities();
$data = [];
foreach ($posts as $post)
{
$data[] = [
'post' => $this->postViewProxy->fromEntity($post),
'comments' => $this->commentViewProxy->fromArray(
array_reverse($this->commentService->getByPost($post)),
$this->getCommentsFetchConfig()),
];
}
return [
'data' => $data,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
public function getPostComments($postNameOrId)
{
$this->privilegeService->assertPrivilege(Privilege::LIST_COMMENTS);
$post = $this->postService->getByNameOrId($postNameOrId);
$filter = new CommentFilter();
$filter->setOrder([
CommentFilter::ORDER_ID =>
CommentFilter::ORDER_ASC]);
$requirement = new Requirement();
$requirement->setValue(new RequirementSingleValue($post->getId()));
$requirement->setType(CommentFilter::REQUIREMENT_POST_ID);
$filter->addRequirement($requirement);
$result = $this->commentService->getFiltered($filter);
$entities = $this->commentViewProxy->fromArray($result->getEntities(), $this->getCommentsFetchConfig());
return ['data' => $entities];
}
public function addComment($postNameOrId)
{
$this->privilegeService->assertPrivilege(Privilege::ADD_COMMENTS);
$post = $this->postService->getByNameOrId($postNameOrId);
$comment = $this->commentService->createComment($post, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
}
public function editComment($commentId)
{
$comment = $this->commentService->getById($commentId);
$this->privilegeService->assertPrivilege(
($comment->getUser() && $this->privilegeService->isLoggedIn($comment->getUser()))
? Privilege::EDIT_OWN_COMMENTS
: Privilege::EDIT_ALL_COMMENTS);
$comment = $this->commentService->updateComment($comment, $this->inputReader->text);
return $this->commentViewProxy->fromEntity($comment, $this->getCommentsFetchConfig());
}
public function deleteComment($commentId)
{
$comment = $this->commentService->getById($commentId);
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($comment->getUser())
? Privilege::DELETE_OWN_COMMENTS
: Privilege::DELETE_ALL_COMMENTS);
return $this->commentService->deleteComment($comment);
}
private function getCommentsFetchConfig()
{
return
[
CommentViewProxy::FETCH_OWN_SCORE => true,
];
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
use Szurubooru\Router;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\FavoritesService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService;
final class FavoritesController extends AbstractController
{
private $privilegeService;
private $authService;
private $postService;
private $favoritesService;
private $userViewProxy;
public function __construct(
PrivilegeService $privilegeService,
AuthService $authService,
PostService $postService,
FavoritesService $favoritesService,
UserViewProxy $userViewProxy)
{
$this->privilegeService = $privilegeService;
$this->authService = $authService;
$this->postService = $postService;
$this->favoritesService = $favoritesService;
$this->userViewProxy = $userViewProxy;
}
public function registerRoutes(Router $router)
{
$router->get('/api/posts/:postNameOrId/favorites', [$this, 'getFavoriteUsers']);
$router->post('/api/posts/:postNameOrId/favorites', [$this, 'addFavorite']);
$router->delete('/api/posts/:postNameOrId/favorites', [$this, 'deleteFavorite']);
}
public function getFavoriteUsers($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$users = $this->favoritesService->getFavoriteUsers($post);
return ['data' => $this->userViewProxy->fromArray($users)];
}
public function addFavorite($postNameOrId)
{
$this->privilegeService->assertLoggedIn();
$user = $this->authService->getLoggedInUser();
$post = $this->postService->getByNameOrId($postNameOrId);
$this->favoritesService->addFavorite($user, $post);
return $this->getFavoriteUsers($postNameOrId);
}
public function deleteFavorite($postNameOrId)
{
$this->privilegeService->assertLoggedIn();
$user = $this->authService->getLoggedInUser();
$post = $this->postService->getByNameOrId($postNameOrId);
$this->favoritesService->deleteFavorite($user, $post);
return $this->getFavoriteUsers($postNameOrId);
}
}

View File

@ -1,30 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Dao\GlobalParamDao;
use Szurubooru\Router;
final class GlobalParamController extends AbstractController
{
private $globalParamDao;
public function __construct(GlobalParamDao $globalParamDao)
{
$this->globalParamDao = $globalParamDao;
}
public function registerRoutes(Router $router)
{
$router->get('/api/globals', [$this, 'getGlobals']);
}
public function getGlobals()
{
$globals = $this->globalParamDao->findAll();
$return = [];
foreach ($globals as $global)
{
$return[$global->getKey()] = $global->getValue();
}
return $return;
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\SnapshotViewProxy;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\SearchServices\Parsers\SnapshotSearchParser;
use Szurubooru\Services\HistoryService;
use Szurubooru\Services\PrivilegeService;
final class HistoryController extends AbstractController
{
private $historyService;
private $privilegeService;
private $snapshotSearchParser;
private $inputReader;
private $snapshotViewProxy;
public function __construct(
HistoryService $historyService,
PrivilegeService $privilegeService,
SnapshotSearchParser $snapshotSearchParser,
InputReader $inputReader,
SnapshotViewProxy $snapshotViewProxy)
{
$this->historyService = $historyService;
$this->privilegeService = $privilegeService;
$this->snapshotSearchParser = $snapshotSearchParser;
$this->inputReader = $inputReader;
$this->snapshotViewProxy = $snapshotViewProxy;
}
public function registerRoutes(Router $router)
{
$router->get('/api/history', [$this, 'getFiltered']);
}
public function getFiltered()
{
$this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY);
$filter = $this->snapshotSearchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize(50);
$result = $this->historyService->getFiltered($filter);
$entities = $this->snapshotViewProxy->fromArray($result->getEntities());
return [
'data' => $entities,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
}

View File

@ -1,57 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Config;
use Szurubooru\Dao\PublicFileDao;
use Szurubooru\Helpers\MimeHelper;
use Szurubooru\Router;
use Szurubooru\Services\NetworkingService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PostThumbnailService;
final class PostContentController extends AbstractController
{
private $config;
private $fileDao;
private $postService;
private $networkingService;
private $postThumbnailService;
public function __construct(
Config $config,
PublicFileDao $fileDao,
PostService $postService,
NetworkingService $networkingService,
PostThumbnailService $postThumbnailService)
{
$this->config = $config;
$this->fileDao = $fileDao;
$this->postService = $postService;
$this->networkingService = $networkingService;
$this->postThumbnailService = $postThumbnailService;
}
public function registerRoutes(Router $router)
{
$router->get('/api/posts/:postName/content', [$this, 'getPostContent']);
$router->get('/api/posts/:postName/thumbnail/:size', [$this, 'getPostThumbnail']);
}
public function getPostContent($postName)
{
$post = $this->postService->getByName($postName);
$customFileName = sprintf('%s_%s.%s',
$this->config->basic->serviceName,
$post->getName(),
strtolower(MimeHelper::getExtension($post->getContentMimeType())));
$this->networkingService->serveFile($this->fileDao->getFullPath($post->getContentPath()), $customFileName);
}
public function getPostThumbnail($postName, $size)
{
$post = $this->postService->getByName($postName);
$thumbnailName = $this->postThumbnailService->generateIfNeeded($post, $size, $size);
$this->networkingService->serveFile($this->fileDao->getFullPath($thumbnailName));
}
}

View File

@ -1,179 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Config;
use Szurubooru\Controllers\ViewProxies\PostViewProxy;
use Szurubooru\Controllers\ViewProxies\SnapshotViewProxy;
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
use Szurubooru\Entities\Post;
use Szurubooru\FormData\PostEditFormData;
use Szurubooru\FormData\UploadFormData;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\SearchServices\Parsers\PostSearchParser;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\PostFeatureService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService;
final class PostController extends AbstractController
{
private $config;
private $authService;
private $privilegeService;
private $postService;
private $postFeatureService;
private $postSearchParser;
private $inputReader;
private $postViewProxy;
private $snapshotViewProxy;
public function __construct(
Config $config,
AuthService $authService,
PrivilegeService $privilegeService,
PostService $postService,
PostFeatureService $postFeatureService,
PostSearchParser $postSearchParser,
InputReader $inputReader,
UserViewProxy $userViewProxy,
PostViewProxy $postViewProxy,
SnapshotViewProxy $snapshotViewProxy)
{
$this->config = $config;
$this->authService = $authService;
$this->privilegeService = $privilegeService;
$this->postService = $postService;
$this->postFeatureService = $postFeatureService;
$this->postSearchParser = $postSearchParser;
$this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy;
$this->postViewProxy = $postViewProxy;
$this->snapshotViewProxy = $snapshotViewProxy;
}
public function registerRoutes(Router $router)
{
$router->post('/api/posts', [$this, 'createPost']);
$router->get('/api/posts', [$this, 'getFiltered']);
$router->get('/api/posts/featured', [$this, 'getFeatured']);
$router->get('/api/posts/:postNameOrId', [$this, 'getByNameOrId']);
$router->get('/api/posts/:postNameOrId/history', [$this, 'getHistory']);
$router->put('/api/posts/:postNameOrId', [$this, 'updatePost']);
$router->delete('/api/posts/:postNameOrId', [$this, 'deletePost']);
$router->post('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
$router->put('/api/posts/:postNameOrId/feature', [$this, 'featurePost']);
}
public function getFeatured()
{
$post = $this->postFeatureService->getFeaturedPost();
$user = $this->postFeatureService->getFeaturedPostUser();
return [
'user' => $this->userViewProxy->fromEntity($user),
'post' => $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig()),
];
}
public function getByNameOrId($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
}
public function getHistory($postNameOrId)
{
$this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY);
$post = $this->getByNameOrId($postNameOrId);
return ['data' => $this->snapshotViewProxy->fromArray($this->postService->getHistory($post))];
}
public function getFiltered()
{
$this->privilegeService->assertPrivilege(Privilege::LIST_POSTS);
$filter = $this->postSearchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize($this->config->posts->postsPerPage);
$this->postService->decorateFilterFromBrowsingSettings($filter);
$result = $this->postService->getFiltered($filter);
$entities = $this->postViewProxy->fromArray($result->getEntities(), $this->getLightFetchConfig());
return [
'data' => $entities,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
public function createPost()
{
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS);
$formData = new UploadFormData($this->inputReader);
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS);
if ($formData->anonymous)
$this->privilegeService->assertPrivilege(Privilege::UPLOAD_POSTS_ANONYMOUSLY);
$post = $this->postService->createPost($formData);
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
}
public function updatePost($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$formData = new PostEditFormData($this->inputReader);
if ($formData->content !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_CONTENT);
if ($formData->thumbnail !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_THUMBNAIL);
if ($formData->safety !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_SAFETY);
if ($formData->source !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_SOURCE);
if ($formData->tags !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_POST_TAGS);
$this->postService->updatePost($post, $formData);
$post = $this->postService->getByNameOrId($postNameOrId);
return $this->postViewProxy->fromEntity($post, $this->getFullFetchConfig());
}
public function deletePost($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$this->postService->deletePost($post);
}
public function featurePost($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$this->postFeatureService->featurePost($post);
}
private function getFullFetchConfig()
{
return
[
PostViewProxy::FETCH_RELATIONS => true,
PostViewProxy::FETCH_TAGS => true,
PostViewProxy::FETCH_USER => true,
PostViewProxy::FETCH_HISTORY => true,
PostViewProxy::FETCH_OWN_SCORE => true,
PostViewProxy::FETCH_FAVORITES => true,
PostViewProxy::FETCH_NOTES => true,
];
}
private function getLightFetchConfig()
{
return
[
PostViewProxy::FETCH_TAGS => true,
];
}
}

View File

@ -1,77 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\PostNoteViewProxy;
use Szurubooru\FormData\PostNoteFormData;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\Services\PostNotesService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService;
final class PostNotesController extends AbstractController
{
private $inputReader;
private $postService;
private $postNotesService;
private $privilegeService;
private $postNoteViewProxy;
public function __construct(
InputReader $inputReader,
PostService $postService,
PostNotesService $postNotesService,
PrivilegeService $privilegeService,
PostNoteViewProxy $postNoteViewProxy)
{
$this->inputReader = $inputReader;
$this->postService = $postService;
$this->postNotesService = $postNotesService;
$this->privilegeService = $privilegeService;
$this->postNoteViewProxy = $postNoteViewProxy;
}
public function registerRoutes(Router $router)
{
$router->get('/api/notes/:postNameOrId', [$this, 'getPostNotes']);
$router->post('/api/notes/:postNameOrId', [$this, 'addPostNote']);
$router->put('/api/notes/:postNoteId', [$this, 'editPostNote']);
$router->delete('/api/notes/:postNoteId', [$this, 'deletePostNote']);
}
public function getPostNotes($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$postNotes = $this->postNotesService->getByPost($post);
return $this->postNoteViewProxy->fromArray($postNotes);
}
public function addPostNote($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
$this->privilegeService->assertPrivilege(Privilege::ADD_POST_NOTES);
$formData = new PostNoteFormData($this->inputReader);
$postNote = $this->postNotesService->createPostNote($post, $formData);
return $this->postNoteViewProxy->fromEntity($postNote);
}
public function editPostNote($postNoteId)
{
$postNote = $this->postNotesService->getById($postNoteId);
$this->privilegeService->assertPrivilege(Privilege::EDIT_POST_NOTES);
$formData = new PostNoteFormData($this->inputReader);
$postNote = $this->postNotesService->updatePostNote($postNote, $formData);
return $this->postNoteViewProxy->fromEntity($postNote);
}
public function deletePostNote($postNoteId)
{
$postNote = $this->postNotesService->getById($postNoteId);
$this->privilegeService->assertPrivilege(Privilege::DELETE_POST_NOTES);
return $this->postNotesService->deletePostNote($postNote);
}
}

View File

@ -1,89 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Entities\Entity;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Router;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\CommentService;
use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\ScoreService;
final class ScoreController extends AbstractController
{
private $privilegeService;
private $authService;
private $postService;
private $commentService;
private $scoreService;
private $inputReader;
public function __construct(
PrivilegeService $privilegeService,
AuthService $authService,
PostService $postService,
CommentService $commentService,
ScoreService $scoreService,
InputReader $inputReader)
{
$this->privilegeService = $privilegeService;
$this->authService = $authService;
$this->postService = $postService;
$this->commentService = $commentService;
$this->scoreService = $scoreService;
$this->inputReader = $inputReader;
}
public function registerRoutes(Router $router)
{
$router->get('/api/posts/:postNameOrId/score', [$this, 'getPostScore']);
$router->post('/api/posts/:postNameOrId/score', [$this, 'setPostScore']);
$router->get('/api/comments/:commentId/score', [$this, 'getCommentScore']);
$router->post('/api/comments/:commentId/score', [$this, 'setCommentScore']);
}
public function getPostScore($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
return $this->getScore($post);
}
public function setPostScore($postNameOrId)
{
$post = $this->postService->getByNameOrId($postNameOrId);
return $this->setScore($post);
}
public function getCommentScore($commentId)
{
$comment = $this->commentService->getById($commentId);
return $this->getScore($comment);
}
public function setCommentScore($commentId)
{
$comment = $this->commentService->getById($commentId);
return $this->setScore($comment);
}
private function setScore(Entity $entity)
{
$this->privilegeService->assertLoggedIn();
$score = intval($this->inputReader->score);
$user = $this->authService->getLoggedInUser();
$result = $this->scoreService->setUserScore($user, $entity, $score);
return [
'score' => $this->scoreService->getScoreValue($entity),
'ownScore' => $result->getScore(),
];
}
private function getScore(Entity $entity)
{
$user = $this->authService->getLoggedInUser();
return [
'score' => $this->scoreService->getScoreValue($entity),
'ownScore' => $this->scoreService->getUserScoreValue($user, $entity),
];
}
}

View File

@ -1,127 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Controllers\ViewProxies\TagViewProxy;
use Szurubooru\FormData\TagEditFormData;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\SearchServices\Parsers\TagSearchParser;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\TagService;
final class TagController extends AbstractController
{
private $privilegeService;
private $tagService;
private $tagViewProxy;
private $tagSearchParser;
private $inputReader;
public function __construct(
PrivilegeService $privilegeService,
TagService $tagService,
TagViewProxy $tagViewProxy,
TagSearchParser $tagSearchParser,
InputReader $inputReader)
{
$this->privilegeService = $privilegeService;
$this->tagService = $tagService;
$this->tagViewProxy = $tagViewProxy;
$this->tagSearchParser = $tagSearchParser;
$this->inputReader = $inputReader;
}
public function registerRoutes(Router $router)
{
$router->get('/api/tags', [$this, 'getTags']);
$router->get('/api/tags/:tagName', [$this, 'getTag']);
$router->get('/api/tags/:tagName/siblings', [$this, 'getTagSiblings']);
$router->put('/api/tags/:tagName', [$this, 'updateTag']);
$router->put('/api/tags/:tagName/merge', [$this, 'mergeTag']);
$router->delete('/api/tags/:tagName', [$this, 'deleteTag']);
}
public function getTag($tagName)
{
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
$tag = $this->tagService->getByName($tagName);
return $this->tagViewProxy->fromEntity($tag, $this->getFullFetchConfig());
}
public function getTags()
{
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
$filter = $this->tagSearchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize(50);
$result = $this->tagService->getFiltered($filter);
$entities = $this->tagViewProxy->fromArray($result->getEntities(), $this->getFullFetchConfig());
return [
'data' => $entities,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
public function getTagSiblings($tagName)
{
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
$tag = $this->tagService->getByName($tagName);
$result = $this->tagService->getSiblings($tagName);
$entities = $this->tagViewProxy->fromArray($result);
return [
'data' => $entities,
];
}
public function updateTag($tagName)
{
$tag = $this->tagService->getByName($tagName);
$formData = new TagEditFormData($this->inputReader);
if ($formData->name !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_NAME);
if ($formData->category !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_CATEGORY);
if ($formData->banned !== null)
$this->privilegeService->assertPrivilege(Privilege::BAN_TAGS);
if ($formData->implications !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_IMPLICATIONS);
if ($formData->suggestions !== null)
$this->privilegeService->assertPrivilege(Privilege::CHANGE_TAG_SUGGESTIONS);
$tag = $this->tagService->updateTag($tag, $formData);
return $this->tagViewProxy->fromEntity($tag, $this->getFullFetchConfig());
}
public function deleteTag($tagName)
{
$tag = $this->tagService->getByName($tagName);
$this->privilegeService->assertPrivilege(Privilege::DELETE_TAGS);
return $this->tagService->deleteTag($tag);
}
public function mergeTag($tagName)
{
$targetTagName = $this->inputReader->targetTag;
$sourceTag = $this->tagService->getByName($tagName);
$targetTag = $this->tagService->getByName($targetTagName);
$this->privilegeService->assertPrivilege(Privilege::MERGE_TAGS);
return $this->tagService->mergeTag($sourceTag, $targetTag);
}
private function getFullFetchConfig()
{
return
[
TagViewProxy::FETCH_IMPLICATIONS => true,
TagViewProxy::FETCH_SUGGESTIONS => true,
TagViewProxy::FETCH_HISTORY => true,
];
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Dao\PublicFileDao;
use Szurubooru\Entities\User;
use Szurubooru\Router;
use Szurubooru\Services\NetworkingService;
use Szurubooru\Services\ThumbnailService;
use Szurubooru\Services\UserService;
final class UserAvatarController extends AbstractController
{
private $fileDao;
private $userService;
private $networkingService;
private $thumbnailService;
public function __construct(
PublicFileDao $fileDao,
UserService $userService,
NetworkingService $networkingService,
ThumbnailService $thumbnailService)
{
$this->fileDao = $fileDao;
$this->userService = $userService;
$this->networkingService = $networkingService;
$this->thumbnailService = $thumbnailService;
}
public function registerRoutes(Router $router)
{
$router->get('/api/users/:userName/avatar/:size', [$this, 'getAvatarByName']);
}
public function getAvatarByName($userName, $size)
{
try
{
$user = $this->userService->getByName($userName);
}
catch (\Exception $e)
{
$this->serveBlankFile($size);
}
switch ($user->getAvatarStyle())
{
case User::AVATAR_STYLE_GRAVATAR:
$hash = md5(strtolower(trim($user->getEmail() ? $user->getEmail() : $user->getId() . $user->getName())));
$url = 'https://www.gravatar.com/avatar/' . $hash . '?d=retro&s=' . $size;
$this->serveFromUrl($url);
break;
case User::AVATAR_STYLE_BLANK:
$this->serveBlankFile($size);
break;
case User::AVATAR_STYLE_MANUAL:
$this->serveFromFile($user->getCustomAvatarSourceContentPath(), $size);
break;
default:
$this->serveBlankFile($size);
break;
}
}
private function serveFromUrl($url)
{
$this->networkingService->redirect($url);
}
private function serveFromFile($sourceName, $size)
{
$thumbnailName = $this->thumbnailService->generateIfNeeded($sourceName, $size, $size);
$this->networkingService->serveFile($this->fileDao->getFullPath($thumbnailName));
}
private function serveBlankFile($size)
{
$this->serveFromFile($this->getBlankAvatarSourceContentPath(), $size);
}
private function getBlankAvatarSourceContentPath()
{
return 'avatars/blank.png';
}
}

View File

@ -1,176 +0,0 @@
<?php
namespace Szurubooru\Controllers;
use Szurubooru\Config;
use Szurubooru\Controllers\ViewProxies\UserViewProxy;
use Szurubooru\FormData\RegistrationFormData;
use Szurubooru\FormData\UserEditFormData;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege;
use Szurubooru\Router;
use Szurubooru\SearchServices\Parsers\UserSearchParser;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\TokenService;
use Szurubooru\Services\UserService;
final class UserController extends AbstractController
{
private $config;
private $privilegeService;
private $userService;
private $tokenService;
private $userSearchParser;
private $inputReader;
private $userViewProxy;
public function __construct(
Config $config,
PrivilegeService $privilegeService,
UserService $userService,
TokenService $tokenService,
UserSearchParser $userSearchParser,
InputReader $inputReader,
UserViewProxy $userViewProxy)
{
$this->config = $config;
$this->privilegeService = $privilegeService;
$this->userService = $userService;
$this->tokenService = $tokenService;
$this->userSearchParser = $userSearchParser;
$this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy;
}
public function registerRoutes(Router $router)
{
$router->post('/api/users', [$this, 'createUser']);
$router->get('/api/users', [$this, 'getFiltered']);
$router->get('/api/users/:userNameOrEmail', [$this, 'getByNameOrEmail']);
$router->put('/api/users/:userNameOrEmail', [$this, 'updateUser']);
$router->delete('/api/users/:userNameOrEmail', [$this, 'deleteUser']);
$router->post('/api/password-reset/:userNameOrEmail', [$this, 'passwordReset']);
$router->post('/api/finish-password-reset/:tokenName', [$this, 'finishPasswordReset']);
$router->post('/api/activation/:userNameOrEmail', [$this, 'activation']);
$router->post('/api/finish-activation/:tokenName', [$this, 'finishActivation']);
}
public function getByNameOrEmail($userNameOrEmail)
{
if (!$this->privilegeService->isLoggedIn($userNameOrEmail))
$this->privilegeService->assertPrivilege(Privilege::VIEW_USERS);
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userViewProxy->fromEntity($user);
}
public function getFiltered()
{
$this->privilegeService->assertPrivilege(Privilege::LIST_USERS);
$filter = $this->userSearchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize($this->config->users->usersPerPage);
$result = $this->userService->getFiltered($filter);
$entities = $this->userViewProxy->fromArray($result->getEntities());
return [
'data' => $entities,
'pageSize' => $result->getPageSize(),
'totalRecords' => $result->getTotalRecords()];
}
public function createUser()
{
$this->privilegeService->assertPrivilege(Privilege::REGISTER);
$formData = new RegistrationFormData($this->inputReader);
$user = $this->userService->createUser($formData);
return $this->userViewProxy->fromEntity($user);
}
public function updateUser($userNameOrEmail)
{
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
$formData = new UserEditFormData($this->inputReader);
if ($formData->avatarStyle !== null || $formData->avatarContent !== null)
{
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userNameOrEmail)
? Privilege::CHANGE_OWN_AVATAR_STYLE
: Privilege::CHANGE_ALL_AVATAR_STYLES);
}
if ($formData->userName !== null)
{
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userNameOrEmail)
? Privilege::CHANGE_OWN_NAME
: Privilege::CHANGE_ALL_NAMES);
}
if ($formData->password !== null)
{
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userNameOrEmail)
? Privilege::CHANGE_OWN_PASSWORD
: Privilege::CHANGE_ALL_PASSWORDS);
}
if ($formData->email !== null)
{
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userNameOrEmail)
? Privilege::CHANGE_OWN_EMAIL_ADDRESS
: Privilege::CHANGE_ALL_EMAIL_ADDRESSES);
}
if ($formData->accessRank)
{
$this->privilegeService->assertPrivilege(Privilege::CHANGE_ACCESS_RANK);
}
if ($formData->browsingSettings)
{
$this->privilegeService->assertLoggedIn($userNameOrEmail);
}
if ($formData->banned !== null)
{
$this->privilegeService->assertPrivilege(Privilege::BAN_USERS);
}
$user = $this->userService->updateUser($user, $formData);
return $this->userViewProxy->fromEntity($user);
}
public function deleteUser($userNameOrEmail)
{
$this->privilegeService->assertPrivilege(
$this->privilegeService->isLoggedIn($userNameOrEmail)
? Privilege::DELETE_OWN_ACCOUNT
: Privilege::DELETE_ACCOUNTS);
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userService->deleteUser($user);
}
public function passwordReset($userNameOrEmail)
{
$user = $this->userService->getByNameOrEmail($userNameOrEmail);
return $this->userService->sendPasswordResetEmail($user);
}
public function activation($userNameOrEmail)
{
$user = $this->userService->getByNameOrEmail($userNameOrEmail, true);
return $this->userService->sendActivationEmail($user);
}
public function finishPasswordReset($tokenName)
{
$token = $this->tokenService->getByName($tokenName);
return ['newPassword' => $this->userService->finishPasswordReset($token)];
}
public function finishActivation($tokenName)
{
$token = $this->tokenService->getByName($tokenName);
$this->userService->finishActivation($token);
}
}

View File

@ -1,17 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
abstract class AbstractViewProxy
{
public abstract function fromEntity($entity, $config = []);
public function fromArray($entities, $config = [])
{
return array_values(array_map(
function($entity) use ($config)
{
return static::fromEntity($entity, $config);
},
$entities));
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\ScoreService;
class CommentViewProxy extends AbstractViewProxy
{
private $authService;
private $scoreService;
private $userViewProxy;
const FETCH_OWN_SCORE = 'fetchOwnScore';
public function __construct(
AuthService $authService,
ScoreService $scoreService,
UserViewProxy $userViewProxy)
{
$this->authService = $authService;
$this->scoreService = $scoreService;
$this->userViewProxy = $userViewProxy;
}
public function fromEntity($comment, $config = [])
{
$result = new \StdClass;
if ($comment)
{
$result->id = $comment->getId();
$result->creationTime = $comment->getCreationTime();
$result->lastEditTime = $comment->getLastEditTime();
$result->text = $comment->getText();
$result->postId = $comment->getPostId();
$result->user = $this->userViewProxy->fromEntity($comment->getUser());
$result->score = $comment->getScore();
if (!empty($config[self::FETCH_OWN_SCORE]) && $this->authService->isLoggedIn())
$result->ownScore = $this->scoreService->getUserScoreValue($this->authService->getLoggedInUser(), $comment);
}
return $result;
}
}

View File

@ -1,21 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
class PostNoteViewProxy extends AbstractViewProxy
{
public function fromEntity($postNote, $config = [])
{
$result = new \StdClass;
if ($postNote)
{
$result->id = $postNote->getId();
$result->postId = $postNote->getPostId();
$result->text = $postNote->getText();
$result->left = $postNote->getLeft();
$result->top = $postNote->getTop();
$result->width = $postNote->getWidth();
$result->height = $postNote->getHeight();
}
return $result;
}
}

View File

@ -1,121 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
use Szurubooru\Entities\Post;
use Szurubooru\Helpers\EnumHelper;
use Szurubooru\Helpers\MimeHelper;
use Szurubooru\Privilege;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\FavoritesService;
use Szurubooru\Services\PostHistoryService;
use Szurubooru\Services\PostNotesService;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\ScoreService;
class PostViewProxy extends AbstractViewProxy
{
const FETCH_USER = 'fetchUser';
const FETCH_TAGS = 'fetchTags';
const FETCH_RELATIONS = 'fetchRelations';
const FETCH_HISTORY = 'fetchHistory';
const FETCH_OWN_SCORE = 'fetchOwnScore';
const FETCH_FAVORITES = 'fetchFavorites';
const FETCH_NOTES = 'fetchNotes';
private $privilegeService;
private $authService;
private $postHistoryService;
private $favoritesService;
private $scoreService;
private $postNotesService;
private $tagViewProxy;
private $userViewProxy;
private $snapshotViewProxy;
private $postNoteViewProxy;
public function __construct(
PrivilegeService $privilegeService,
AuthService $authService,
PostHistoryService $postHistoryService,
FavoritesService $favoritesService,
ScoreService $scoreService,
PostNotesService $postNotesService,
TagViewProxy $tagViewProxy,
UserViewProxy $userViewProxy,
SnapshotViewProxy $snapshotViewProxy,
PostNoteViewProxy $postNoteViewProxy)
{
$this->privilegeService = $privilegeService;
$this->authService = $authService;
$this->postHistoryService = $postHistoryService;
$this->favoritesService = $favoritesService;
$this->scoreService = $scoreService;
$this->postNotesService = $postNotesService;
$this->tagViewProxy = $tagViewProxy;
$this->userViewProxy = $userViewProxy;
$this->snapshotViewProxy = $snapshotViewProxy;
$this->postNoteViewProxy = $postNoteViewProxy;
}
public function fromEntity($post, $config = [])
{
$result = new \StdClass;
if (!$post)
return $result;
$result->id = $post->getId();
$result->idMarkdown = $post->getIdMarkdown();
$result->name = $post->getName();
$result->uploadTime = $post->getUploadTime();
$result->lastEditTime = $post->getLastEditTime();
$result->safety = EnumHelper::postSafetyToString($post->getSafety());
$result->contentType = EnumHelper::postTypeToString($post->getContentType());
$result->contentChecksum = $post->getContentChecksum();
$result->contentMimeType = $post->getContentMimeType();
$result->contentExtension = MimeHelper::getExtension($post->getContentMimeType());
$result->source = $post->getSource();
$result->imageWidth = $post->getImageWidth();
$result->imageHeight = $post->getImageHeight();
$result->featureCount = $post->getFeatureCount();
$result->lastFeatureTime = $post->getLastFeatureTime();
$result->originalFileSize = $post->getOriginalFileSize();
$result->favoriteCount = $post->getFavoriteCount();
$result->score = $post->getScore();
$result->commentCount = $post->getCommentCount();
$result->flags = new \StdClass;
$result->flags->loop = ($post->getFlags() & Post::FLAG_LOOP);
if (!empty($config[self::FETCH_TAGS]))
{
$result->tags = $this->tagViewProxy->fromArray($post->getTags());
usort($result->tags, function($tag1, $tag2)
{
return strcasecmp($tag1->name, $tag2->name);
});
}
if (!empty($config[self::FETCH_USER]))
$result->user = $this->userViewProxy->fromEntity($post->getUser());
if (!empty($config[self::FETCH_RELATIONS]))
$result->relations = $this->fromArray($post->getRelatedPosts());
if (!empty($config[self::FETCH_HISTORY]))
{
if ($this->privilegeService->hasPrivilege(Privilege::VIEW_HISTORY))
$result->history = $this->snapshotViewProxy->fromArray($this->postHistoryService->getPostHistory($post));
else
$result->history = [];
}
if (!empty($config[self::FETCH_OWN_SCORE]) && $this->authService->isLoggedIn())
$result->ownScore = $this->scoreService->getUserScoreValue($this->authService->getLoggedInUser(), $post);
if (!empty($config[self::FETCH_FAVORITES]))
$result->favorites = $this->userViewProxy->fromArray($this->favoritesService->getFavoriteUsers($post));
if (!empty($config[self::FETCH_NOTES]))
$result->notes = $this->postNoteViewProxy->fromArray($this->postNotesService->getByPost($post));
return $result;
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
class SnapshotViewProxy extends AbstractViewProxy
{
private $userViewProxy;
public function __construct(UserViewProxy $userViewProxy)
{
$this->userViewProxy = $userViewProxy;
}
public function fromEntity($snapshot, $config = [])
{
$result = new \StdClass;
if ($snapshot)
{
$result->time = $snapshot->getTime();
$result->type = $snapshot->getType();
$result->primaryKey = $snapshot->getPrimaryKey();
$result->operation = $snapshot->getOperation();
$result->user = $this->userViewProxy->fromEntity($snapshot->getUser());
$result->data = $snapshot->getData();
$result->dataDifference = $snapshot->getDataDifference();
}
return $result;
}
}

View File

@ -1,52 +0,0 @@
<?php
namespace Szurubooru\Controllers\ViewProxies;
use Szurubooru\Privilege;
use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\TagHistoryService;
class TagViewProxy extends AbstractViewProxy
{
const FETCH_IMPLICATIONS = 'fetchImplications';
const FETCH_SUGGESTIONS = 'fetchSuggestions';
const FETCH_HISTORY = 'fetchHistory';
private $privilegeService;
private $tagHistoryService;
private $snapshotViewProxy;
public function __construct(
PrivilegeService $privilegeService,
TagHistoryService $tagHistoryService,
SnapshotViewProxy $snapshotViewProxy)
{
$this->privilegeService = $privilegeService;
$this->tagHistoryService = $tagHistoryService;
$this->snapshotViewProxy = $snapshotViewProxy;
}
public function fromEntity($tag, $config = [])
{
$result = new \StdClass;
if ($tag)
{
$result->name = $tag->getName();
$result->usages = $tag->getUsages();
$result->banned = $tag->isBanned();
$result->category = $tag->getCategory();
if (!empty($config[self::FETCH_IMPLICATIONS]))
$result->implications = $this->fromArray($tag->getImpliedTags());
if (!empty($config[self::FETCH_SUGGESTIONS]))
$result->suggestions = $this->fromArray($tag->getSuggestedTags());
if (!empty($config[self::FETCH_HISTORY]))
{
$result->history = $this->privilegeService->hasPrivilege(Privilege::VIEW_HISTORY)
? $this->snapshotViewProxy->fromArray($this->tagHistoryService->getTagHistory($tag))
: [];
}
}
return $result;
}
}

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