diff options
-rw-r--r-- | application/config/routes.php | 1 | ||||
-rw-r--r-- | application/controllers/file/file_default.php | 34 | ||||
-rw-r--r-- | application/controllers/file/multipaste.php | 113 | ||||
-rw-r--r-- | application/core/MY_Controller.php | 3 | ||||
-rw-r--r-- | application/service/multipaste_queue.php | 96 | ||||
-rw-r--r-- | application/test/tests/test_service_multipaste_queue.php | 93 | ||||
-rw-r--r-- | application/views/file/multipaste/queue.php | 32 | ||||
-rw-r--r-- | application/views/file/upload_history.php | 8 | ||||
-rw-r--r-- | application/views/file/upload_history_thumbnails.php | 7 | ||||
-rw-r--r-- | application/views/header.php | 3 | ||||
-rw-r--r-- | public_html/data/css/style.css | 37 | ||||
-rw-r--r-- | public_html/data/js/application.js | 3 | ||||
-rw-r--r-- | public_html/data/js/multipaste.js | 86 | ||||
-rw-r--r-- | public_html/data/js/thumbnail-view.js | 42 |
14 files changed, 531 insertions, 27 deletions
diff --git a/application/config/routes.php b/application/config/routes.php index 9583ff37c..f44f283f0 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -40,6 +40,7 @@ $route['default_controller'] = "file/file_default"; $route['user/(:any)'] = "user/$1"; +$route['file/multipaste/(:any)'] = "file/multipaste/$1"; $route['file/(:any)'] = "file/file_default/$1"; $route['tools/(:any)'] = "tools/$1"; $route['api/(:any)'] = "api/route/$1"; diff --git a/application/controllers/file/file_default.php b/application/controllers/file/file_default.php index 2b77866a0..3b5eb0ee4 100644 --- a/application/controllers/file/file_default.php +++ b/application/controllers/file/file_default.php @@ -683,6 +683,40 @@ class File_default extends MY_Controller { $this->load->view('footer', $this->data); } + public function handle_history_submit() + { + $this->muser->require_access("apikey"); + + $process = $this->input->post("process"); + + $dispatcher = [ + "delete" => function() { + return $this->do_delete(); + }, + "multipaste" => function() { + return $this->_append_multipaste_queue(); + }, + ]; + + if (isset($dispatcher[$process])) { + $dispatcher[$process](); + } else { + throw new \exceptions\UserInputException("file/handle_history_submit/invalid-process-value", "Value in process field not found in dispatch table"); + } + } + + private function _append_multipaste_queue() + { + $ids = $this->input->post("ids"); + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->append($ids); + redirect("file/multipaste/queue"); + } + function upload_history() { $this->muser->require_access("apikey"); diff --git a/application/controllers/file/multipaste.php b/application/controllers/file/multipaste.php new file mode 100644 index 000000000..759a781f0 --- /dev/null +++ b/application/controllers/file/multipaste.php @@ -0,0 +1,113 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +class Multipaste extends MY_Controller { + + function __construct() { + parent::__construct(); + + $this->load->model('mfile'); + $this->load->model('mmultipaste'); + } + + public function append_multipaste_queue() { + $this->muser->require_access("basic"); + + $ids = $this->input->post("ids"); + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->append($ids); + + redirect("file/multipaste/queue"); + } + + public function review_multipaste() { + $this->muser->require_access("basic"); + + $this->load->view('header', $this->data); + $this->load->view('file/review_multipaste', $this->data); + $this->load->view('footer', $this->data); + } + + public function queue() { + $this->muser->require_access("basic"); + + $m = new \service\multipaste_queue(); + $ids = $m->get(); + + $this->data['ids'] = $ids; + $this->data['items'] = array_map(function($id) {return $this->_get_multipaste_item($id);}, $ids); + + $this->load->view('header', $this->data); + $this->load->view('file/multipaste/queue', $this->data); + $this->load->view('footer', $this->data); + } + + public function form_submit() { + $this->muser->require_access("basic"); + + $ids = $this->input->post('ids'); + $process = $this->input->post('process'); + + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->set($ids); + + $dispatcher = [ + 'save' => function() use ($ids, $m) { + redirect("file/multipaste/queue"); + }, + 'create' => function() use ($ids, $m) { + $userid = $this->muser->get_userid(); + $limits = $this->muser->get_upload_id_limits(); + $ret = \service\files::create_multipaste($ids, $userid, $limits); + $m->set([]); + redirect($ret['url_id'].'/'); + }, + ]; + + if (isset($dispatcher[$process])) { + $dispatcher[$process](); + } else { + throw new \exceptions\UserInputException("file/multipaste/form_submit/invalid-process-value", "Value in process field not found in dispatch table"); + } + } + + public function ajax_submit() { + $this->muser->require_access("basic"); + $ids = $this->input->post('ids'); + + if ($ids === false) { + $ids = []; + } + + $m = new \service\multipaste_queue(); + $m->set($ids); + } + + private function _get_multipaste_item($id) { + $filedata = $this->mfile->get_filedata($id); + $item = []; + $item['id'] = $filedata['id']; + $item['tooltip'] = \service\files::tooltip($filedata); + $item['title'] = $filedata['filename']; + if (\libraries\Image::type_supported($filedata["mimetype"])) { + $item['thumbnail'] = site_url("file/thumbnail/".$filedata['id']); + } + + return $item; + } + +} diff --git a/application/core/MY_Controller.php b/application/core/MY_Controller.php index ede6577da..078a4faa1 100644 --- a/application/core/MY_Controller.php +++ b/application/core/MY_Controller.php @@ -104,6 +104,9 @@ class MY_Controller extends CI_Controller { $this->load->model("muser"); $this->data["user_logged_in"] = $this->muser->logged_in(); $this->data['redirect_uri'] = $this->uri->uri_string(); + if ($this->muser->has_session()) { + $this->data['show_multipaste_queue'] = !empty((new \service\multipaste_queue)->get()); + } } protected function _require_cli_request() diff --git a/application/service/multipaste_queue.php b/application/service/multipaste_queue.php new file mode 100644 index 000000000..453ea3429 --- /dev/null +++ b/application/service/multipaste_queue.php @@ -0,0 +1,96 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace service; + +class multipaste_queue { + + public function __construct($session = null, $mfile = null, $mmultipaste = null) { + $CI =& get_instance(); + + $this->session = $session; + $this->mfile = $mfile; + $this->mmultipaste = $mmultipaste; + + if ($this->session === null) { + $this->session = $CI->session; + } + + if ($this->mfile === null) { + $CI->load->model("mfile"); + $this->mfile = $CI->mfile; + } + + if ($this->mmultipaste === null) { + $CI->load->model("mmultipaste"); + $this->mmultipaste = $CI->mmultipaste; + } + } + + /** + * Append ids to the queue + * + * @param array ids + * @return void + */ + public function append(array $ids) { + $old_ids = $this->get(); + + # replace multipaste ids with their corresponding paste ids + $ids = array_map(function($id) {return array_values($this->resolve_multipaste($id));}, $ids); + $ids = array_reduce($ids, function($a, $b) {return array_merge($a, $b);}, []); + + $ids = array_unique(array_merge($old_ids, $ids)); + $this->set($ids); + } + + /** + * Return array of ids in a multipaste if the argument id is a multipaste. + * Otherwise return an array containing just the argument id. + * + * @param id + * @return array of ids + */ + private function resolve_multipaste($id) { + if (strpos($id, "m-") === 0) { + if ($this->mmultipaste->valid_id($id)) { + return array_map(function($filedata) {return $filedata['id'];}, $this->mmultipaste->get_files($id)); + } + } + return [$id]; + } + + /** + * Get the queue + * + * @return array of ids + */ + public function get() { + $ids = $this->session->userdata("multipaste_queue"); + if ($ids === false) { + $ids = []; + } + + assert(is_array($ids)); + return $ids; + } + + /** + * Set the queue to $ids + * + * @param array ids + * @return void + */ + public function set(array $ids) { + $ids = array_filter($ids, function($id) {return $this->mfile->valid_id($id);}); + + $this->session->set_userdata("multipaste_queue", $ids); + } + +} diff --git a/application/test/tests/test_service_multipaste_queue.php b/application/test/tests/test_service_multipaste_queue.php new file mode 100644 index 000000000..0427425a0 --- /dev/null +++ b/application/test/tests/test_service_multipaste_queue.php @@ -0,0 +1,93 @@ +<?php +/* + * Copyright 2016 Florian "Bluewind" Pritz <bluewind@server-speed.net> + * + * Licensed under AGPLv3 + * (see COPYING for full license text) + * + */ + +namespace test\tests; + +class test_service_multipaste_queue extends \test\Test { + + public function __construct() + { + parent::__construct(); + } + + public function init() + { + $this->session = \Mockery::mock("Session"); + $this->session->shouldReceive("userdata")->never()->byDefault(); + $this->session->shouldReceive("set_userdata")->never()->byDefault(); + + $this->mfile = \Mockery::mock("Mfile"); + $this->mfile->shouldReceive("valid_id")->never()->byDefault(); + + $this->mmultipaste = \Mockery::mock("Mmultipaste"); + $this->mmultipaste->shouldReceive("valid_id")->never()->byDefault(); + + $this->m = new \service\multipaste_queue($this->session, $this->mfile, $this->mmultipaste); + } + + public function cleanup() + { + \Mockery::close(); + } + + public function test_get() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(false); + $this->t->is_deeply($this->m->get(), [], "Fresh queue is empty"); + } + + public function test_set() + { + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + + $this->t->is($this->m->set(['abc', '123']), null, "set() should succeed"); + } + + public function test_append() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(false); + $this->mfile->shouldReceive('valid_id')->with('abc')->times(2)->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc'])->once(); + $this->t->is($this->m->append(['abc']), null, "append([abc]) should succeed"); + + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(['abc']); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['123']), null, "append([123]) should succeed"); + } + + public function test_append_itemAlreadyInQueue() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn(['abc', '123']); + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['abc']), null, "append([abc]) should succeed"); + } + + public function test_append_multipaste() + { + $this->session->shouldReceive('userdata')->with("multipaste_queue")->once()->andReturn([]); + $this->mmultipaste->shouldReceive('valid_id')->with('m-abc')->once()->andReturn(true); + $this->mmultipaste->shouldReceive('get_files')->with('m-abc')->once()->andReturn([ + ['id' => 'abc'], + ['id' => '123'], + ]); + $this->mfile->shouldReceive('valid_id')->with('abc')->once()->andReturn(true); + $this->mfile->shouldReceive('valid_id')->with('123')->once()->andReturn(true); + $this->session->shouldReceive('set_userdata')->with("multipaste_queue", ['abc', '123'])->once(); + $this->t->is($this->m->append(['m-abc']), null, "append([m-abc]) should succeed"); + } + + +} + diff --git a/application/views/file/multipaste/queue.php b/application/views/file/multipaste/queue.php new file mode 100644 index 000000000..3f42b3cdf --- /dev/null +++ b/application/views/file/multipaste/queue.php @@ -0,0 +1,32 @@ +<div class="multipasteQueue"> + <?php echo form_open("file/multipaste/form_submit", ["data-ajax_url" => site_url("file/multipaste/ajax_submit")]); ?> + <div class="items"><!-- + <?php foreach ($items as $item) {?> + --><div data-id="<?php echo $item['id']; ?>"> + <input type="hidden" name="ids[<?php echo $item['id']; ?>]" value="<?php echo $item['id']; ?>"> + <div class='item'> + <?php if (isset($item['thumbnail'])) { ?> + <img + src="<?php echo $item['thumbnail']; ?>" + title="<?php echo $item['title']; ?>" + data-content="<?php echo $item['tooltip']; ?>"> + <?php } else { ?> + <div> + <?php echo $item['title']; ?><br> + <?php echo $item['tooltip']; ?> + </div> + <?php } ?> + </div> + <button class='multipaste_queue_delete btn-danger btn btn-xs'>Remove</button> + </div><!-- + <?php } ?> + --></div> + <button type="submit" class="btn btn-default" name="process" value="save"> + <div class="ajaxFeedback" style="display: none"> + <span class="glyphicon glyphicon-refresh spinning"></span> + </div> + Only save queue order + </button> + <button type="submit" class="btn btn-primary" name="process" value="create">Create multipaste</button> + </form> +</div> diff --git a/application/views/file/upload_history.php b/application/views/file/upload_history.php index 253c0aa9e..c6cbe7946 100644 --- a/application/views/file/upload_history.php +++ b/application/views/file/upload_history.php @@ -1,7 +1,10 @@ -<?php echo form_open("file/do_delete") ?> +<?php echo form_open("file/handle_history_submit") ?> <div class="nav-history"> <div class="container"> - <input class="btn btn-danger pull-right" type="submit" value="Delete checked" name="process"> + <div class="pull-right"> + <button class="btn btn-danger" name="process" value="delete">Delete checked</button> + <button class="btn btn-primary" name="process" value="multipaste">Add checked to multipaste queue</button> + </div> <?php include 'nav_history.php'; ?> </div> </div> @@ -31,7 +34,6 @@ </tbody> </table> </div> - <input class="btn btn-danger" type="submit" value="Delete checked" name="process"> </form> <p>Total sum of your distinct uploads: <?php echo $total_size; ?>.</p> diff --git a/application/views/file/upload_history_thumbnails.php b/application/views/file/upload_history_thumbnails.php index 9fb9c5450..7d4fc6298 100644 --- a/application/views/file/upload_history_thumbnails.php +++ b/application/views/file/upload_history_thumbnails.php @@ -1,10 +1,11 @@ <div class="nav-history"> <div class="container"> <div class="pull-right"> - <?php echo form_open("file/do_delete/", array("id" => "delete_form", "style" => "display: inline")); ?> - <button type="submit" class="btn btn-danger" id="delete_button" style="display: none">Delete selected</button> + <?php echo form_open("file/handle_history_submit/", array("id" => "submit_form", "style" => "display: inline")); ?> + <button type="submit" class="btn btn-danger" style="display: none" name='process' value='delete'>Delete selected</button> + <button type="submit" class="btn btn-primary" style="display: none" name='process' value='multipaste'>Add selected to multipaste queue</button> </form> - <button class="btn btn-default" id="toggle_delete_mode" style="display: inline">Delete mode</button> + <button class="btn btn-default" id="toggle_select_mode" style="display: inline">Select mode</button> </div> <?php include 'nav_history.php'; ?> diff --git a/application/views/header.php b/application/views/header.php index fb7f01b0c..06f197875 100644 --- a/application/views/header.php +++ b/application/views/header.php @@ -101,6 +101,9 @@ if (is_cli_client() && !isset($force_full_html)) { <?php include "user/nav.php"; ?> </ul> </li> + <?php if (isset($show_multipaste_queue) && $show_multipaste_queue) {?> + <li class="btn-primary multipaste_button"><a href="<?php echo site_url("file/multipaste/queue"); ?>">Multipaste queue</a></li> + <?php } ?> <?php } ?> </ul> </div> diff --git a/public_html/data/css/style.css b/public_html/data/css/style.css index 57366d3d6..166a00c12 100644 --- a/public_html/data/css/style.css +++ b/public_html/data/css/style.css @@ -42,6 +42,42 @@ height: 50px; } +.multipasteQueue .items>div { + height: 190px; + display: inline-block; + width: 150px; + vertical-align: top; + margin: 0 1px 20px 1px; +} + +.multipasteQueue .items .item { + height: 160px; +} + +.nav li.multipaste_button a { + color: #fff; +} + +.ajaxFeedback { + display: inline-block; +} + +/* Source: http://stackoverflow.com/a/26283602 */ +.glyphicon.spinning { + animation: spin 1s infinite linear; + -webkit-animation: spin2 1s infinite linear; +} + +@keyframes spin { + from { transform: scale(1) rotate(0deg); } + to { transform: scale(1) rotate(360deg); } +} + +@-webkit-keyframes spin2 { + from { -webkit-transform: rotate(0deg); } + to { -webkit-transform: rotate(360deg); } +} + @media (max-width: 768px) { .dont-float { float: left; @@ -324,6 +360,7 @@ body { word-break: break-word; word-wrap: normal; max-width: 400px; + min-width: 250px } #upload_history .popover { diff --git a/public_html/data/js/application.js b/public_html/data/js/application.js index 674f68008..d212ce04c 100644 --- a/public_html/data/js/application.js +++ b/public_html/data/js/application.js @@ -7,6 +7,7 @@ define( 'lexer-input', 'tabwidth-input', 'thumbnail-view', + 'multipaste', 'uploader', 'tablesorter', 'jquery', @@ -20,6 +21,7 @@ define( LexerInput, TabwidthInput, ThumbnailView, + Multipaste, Uploader, TableSorter, $ @@ -44,6 +46,7 @@ define( TabwidthInput.initialize(); LexerInput.initialize(); ThumbnailView.initialize(); + Multipaste.initialize(); Uploader.initialize(); TableSorter.initialize(); this.configureTooltips(); diff --git a/public_html/data/js/multipaste.js b/public_html/data/js/multipaste.js new file mode 100644 index 000000000..21434ab0d --- /dev/null +++ b/public_html/data/js/multipaste.js @@ -0,0 +1,86 @@ +(function () { +'use strict'; +define(['underscore', 'util', 'jquery', 'jquery-ui'], function (_, Util, $) { + var ui = { + itemsContainer: ".multipasteQueue .items", + queueDeleteButton: ".multipaste_queue_delete", + submitButton: ".multipasteQueue button[type=submit]", + itemImages: ".multipasteQueue .items img", + form: ".multipasteQueue form", + csrfToken: "form input[name=csrf_test_name]", + ajaxFeedback: "form .ajaxFeedback", + }; + + var timer = 0; + + var PrivateFunctions = { + setupQueueDeleteButtons: function() { + $(ui.queueDeleteButton).on('click', function(event) { + event.stopImmediatePropagation(); + var id = $(event.target).data('id'); + $(event.target).parent().remove(); + PrivateFunctions.saveQueue(); + }); + }, + setupTooltips: function() { + $(ui.itemImages).popover({ + trigger: 'hover', + placement: 'auto bottom', + html: true + }); + }, + setupButtons: function() { + this.setupQueueDeleteButtons(); + }, + setupSortable: function() { + $(ui.itemsContainer).sortable({ + revert: 100, + placeholder: "ui-state-highlight", + tolerance: "pointer", + stop: function(e, u) { + u.item.find("img").first().popover("show"); + }, + start: function(e, u) { + u.item.find("img").first().popover("show"); + }, + update: function(e, u) { + PrivateFunctions.saveQueue(); + }, + }); + + $(ui.itemsContainer).disableSelection(); + }, + saveQueue: function() { + var queue = $(ui.itemsContainer).sortable("toArray", {attribute: "data-id"}); + console.log("queue changed ", queue); + clearTimeout(timer); + timer = setTimeout(function() { + var url = $(ui.form).data("ajax_url"); + var csrf_token = $(ui.csrfToken).attr("value"); + $(ui.ajaxFeedback).show(); + $.ajax({ + method: "POST", + url: url, + data: { + csrf_test_name: csrf_token, + ids: queue + }, + complete: function() { + $(ui.ajaxFeedback).hide(); + }, + }); + }, 2000); + }, + }; + + var Multipaste = { + initialize: function () { + PrivateFunctions.setupButtons(); + PrivateFunctions.setupSortable(); + PrivateFunctions.setupTooltips(); + }, + }; + + return Multipaste; +}); +})(); diff --git a/public_html/data/js/thumbnail-view.js b/public_html/data/js/thumbnail-view.js index a2820069a..dc2f547ab 100644 --- a/public_html/data/js/thumbnail-view.js +++ b/public_html/data/js/thumbnail-view.js @@ -1,22 +1,22 @@ (function () { 'use strict'; -define(['jquery', 'underscore', 'jquery.colorbox'], function ($, _) { +define(['jquery', 'underscore', 'multipaste', 'jquery.colorbox'], function ($, _, Multipaste) { var ui = { thumbnailLinks: '.upload_thumbnails a', - deleteButton: '#delete_button', - deleteForm: '#delete_form', + formButtons: '#submit_form button[type=submit]', + submitForm: '#submit_form', markedThumbnails: '.upload_thumbnails .marked', colorbox: '.colorbox', thumbnails: '.upload_thumbnails', - toggleDeleteModeButton: '#toggle_delete_mode' + toggleSelectModeButton: '#toggle_select_mode', }; var PrivateFunctions = { - inDeleteMode: false, + inSelectMode: false, setupEvents: function () { - $(ui.toggleDeleteModeButton).on('click', _.bind(this.toggleDeleteMode, this)); - $(ui.thumbnailLinks).on('click', _.bind(this.toggleMarkForDeletion, this)); + $(ui.toggleSelectModeButton).on('click', _.bind(this.toggleSelectMode, this)); + $(ui.thumbnailLinks).on('click', _.bind(this.thumbnailClick, this)); $(window).resize(_.bind(this.onResize, this)); }, @@ -53,39 +53,39 @@ define(['jquery', 'underscore', 'jquery.colorbox'], function ($, _) { }); }, - toggleDeleteMode: function () { - if (this.inDeleteMode) { - $(ui.deleteButton).hide(); - $(ui.deleteForm).find('input').remove(); + toggleSelectMode: function () { + if (this.inSelectMode) { + $(ui.formButtons).hide(); + $(ui.submitForm).find('input').remove(); $(ui.markedThumbnails).removeClass('marked'); this.setupColorbox(); } else { - $(ui.deleteButton).show(); + $(ui.formButtons).show(); this.removeColorbox(); } - this.inDeleteMode = !this.inDeleteMode; + this.inSelectMode = !this.inSelectMode; }, - deleteInput: function (id) { + submitInput: function (id) { return $('<input>').attr({ type: 'hidden', name: 'ids[' + id + ']', value: id, - id: 'delete_' +id + id: 'submit_' +id }); }, - toggleMarkForDeletion: function (event) { - if (!this.inDeleteMode) { return; } + thumbnailClick: function (event) { + if (!this.inSelectMode) { return; } event.preventDefault(); var id = $(event.target).closest('a').data('id'); - var deleteInput = $(ui.deleteForm).find('input#delete_' + id); + var submitInput = $(ui.submitForm).find('input#submit_' + id); - if (deleteInput.length === 0) { - $(ui.deleteForm).append(this.deleteInput(id)); + if (submitInput.length === 0) { + $(ui.submitForm).append(this.submitInput(id)); } else { - deleteInput.remove(); + submitInput.remove(); } $(event.target).closest('a').toggleClass('marked'); }, |