diff --git a/app/controllers/InlineController.php b/app/controllers/InlineController.php new file mode 100755 index 0000000..696785b --- /dev/null +++ b/app/controllers/InlineController.php @@ -0,0 +1,164 @@ + ['only' => ['create', 'copy']] + ]; + } + + public function create() + { + # If this user already has an inline with no images, use it. + $inline = Inline::where("(SELECT count(*) FROM inline_images WHERE inline_images.inline_id = inlines.id) = 0 AND user_id = ?", current_user()->id)->first(); + + if (!$inline) { + $inline = Inline::create(['user_id' => current_user()->id]); + } + + $this->redirectTo(['#edit', 'id' => $inline->id]); + } + + public function index() + { + $query = Inline::none(); + if (!current_user()->is_anonymous()) { + $query->order('user_id = ' . current_user()->id . ' DESC'); + } + $query->order('created_at desc'); + + $this->inlines = $query->paginate($this->page_number(), 20); + + $this->respond_to_list('inlines'); + } + + public function delete() + { + $inline = Inline::find($this->params()->id); + + if (!current_user()->has_permission($inline)) { + $this->access_denied(); + return; + } + + $inline->destroy(); + $this->respond_to_success('Image group deleted', '#index'); + } + + public function addImage() + { + $inline = Inline::find($this->params()->id); + + if (!current_user()->has_permission($inline)) { + $this->access_denied(); + return; + } + + if ($this->request()->isPost()) { + $new_image = InlineImage::create(array_merge($this->params()->image ?: [], ['inline_id' => $inline->id, 'file' => $this->params()->files()->image['file']])); + if ($new_image->errors()->any()) { + $this->respond_to_error($new_image, ['#edit', 'id' => $inline->id]); + return; + } + + $this->redirectTo(['#edit', 'id' => $inline->id]); + } + } + + public function deleteImage() + { + $image = InlineImage::find($this->params()->id); + + $inline = $image->inline; + + if (!current_user()->has_permission($inline)) { + $this->access_denied(); + return; + } + + $image->destroy(); + $this->redirectTo(['#edit', 'id' => $inline->id]); + } + + public function update() + { + $inline = Inline::find($this->params()->id); + + if (!current_user()->has_permission($inline)) { + $this->access_denied(); + return; + } + + $inline->updateAttributes($this->params()->inline); + + $images = $this->params()->image ?: []; + foreach ($images as $id => $p) { + $image = InlineImage::where('id = ? AND inline_id = ?', $id, $inline->id)->first(); + if (isset($p['description'])) { + $image->description = $p['description']; + } + if (isset($p['sequence'])) { + $image->sequence = $p['sequence']; + } + if ($image->changedAttributes()) { + $image->save(); + } + } + + $inline->reload(); + $inline->renumber_sequences(); + + $this->notice('Image updated'); + $this->redirectTo(['#edit', 'id' => $inline->id]); + } + + # Create a copy of an inline image and all of its images. Allow copying from images + # owned by someone else. + public function copy() + { + $inline = Inline::find($this->params()->id); + + $new_inline = Inline::create([ + 'user_id' => current_user()->id, + 'description' => $inline->description + ]); + + foreach ($inline->inline_images as $image) { + $new_attributes = array_merge($image->attributes(), ['inline_id' => $new_inline->id]); + unset($new_attributes['id']); + $new_image = InlineImage::create($new_attributes); + } + + $this->respond_to_success('Image copied', ['#edit', 'id' => $new_inline->id]); + } + + public function edit() + { + $this->inline = Inline::find($this->params()->id); + } + + public function crop() + { + $this->inline = Inline::find($this->params()->id); + $image = $this->inline->inline_images->toArray(); + $this->image = array_shift($image); + if (!$this->image) { + throw new Rails\ActiveRecord\Exception\RecordNotFoundException( + "Inline images set #" . $this->inline->id . " doesn't have inline images" + ); + } + if (!current_user()->has_permission($this->inline)) { + $this->access_denied(); + return; + } + + if ($this->request()->isPost()) { + if ($this->inline->crop($this->params()->toArray())) { + $this->redirectTo(['#edit', 'id' => $this->inline->id]); + } else { + $this->respond_to_error($this->inline, ['#edit', 'id' => $this->inline->id]); + } + } + } +} diff --git a/app/helpers/ApplicationHelper.php b/app/helpers/ApplicationHelper.php index fd52d71..e0acaa2 100755 --- a/app/helpers/ApplicationHelper.php +++ b/app/helpers/ApplicationHelper.php @@ -91,7 +91,12 @@ class ApplicationHelper extends Rails\ActionView\Helper if (!$inline->inline_images) return ""; - $url = $inline->inline_images->first->preview_url(); + if ($inline->inline_images->any()) { + $url = $inline->inline_images[0]->preview_url(); + } else { + $url = ''; + } + if (!$preview_html) $preview_html = ''; @@ -108,29 +113,30 @@ class ApplicationHelper extends Rails\ActionView\Helper '; $inline_id = "inline-$id-$num"; - $script = 'InlineImage.register("'.$inline_id.'", '.to_json($inline).');'; + $script = 'InlineImage.register("' . $inline_id . '", ' . $inline->toJson() . ');'; return array($block, $script, $inline_id); } public function format_inlines($text, $id) { - $num = 0; + $num = 0; $list = []; - // preg_match('/image #(\d+)/i', $text, $m); - // foreach ($m as $t) { - // $i = Inline::find($m[1]); - // if ($i) { - // list($block, $script) = format_inline($i, $num, $id); - // $list[] = $script; - // $num++; - // return $block; - // } else - // return $t; - // } + $text = preg_replace_callback('/image #(\d+)/i', function($m) use (&$list, &$num, $id) { + $i = Inline::where(['id' => (int)$m[1]])->first(); + if ($i) { + list($block, $script) = $this->format_inline($i, $num, $id); + $list[] = $script; + $num++; + return $block; + } else { + return $m[0]; + } + }, $text); - if ($num > 0 ) - $text .= ''; + if ($num > 0) { + $text .= ''; + } return $text; } diff --git a/app/helpers/InlineHelper.php b/app/helpers/InlineHelper.php new file mode 100755 index 0000000..c81c0d6 --- /dev/null +++ b/app/helpers/InlineHelper.php @@ -0,0 +1,18 @@ +has_sample()) { + $url = $image->sample_url(); + $tag_options['width'] = $image->sample_width(); + $tag_options['height'] = $image->sample_height(); + } else { + $url = $image->file_url(); + $tag_options['width'] = $image->width; + $tag_options['height'] = $image->height; + } + + return $this->imageTag($url, $tag_options); + } +} diff --git a/app/models/Inline.php b/app/models/Inline.php index 48fae05..3011fdf 100755 --- a/app/models/Inline.php +++ b/app/models/Inline.php @@ -1,18 +1,34 @@ [ 'user' ], 'has_many' => [ - 'inline_images' => [function() { $this->order('sequence'); }, 'dependent' => 'destroy', 'class_name' => 'inlineImage'] + 'inline_images' => [function() { $this->order('sequence'); } /*Not yet supported: 'dependent' => 'destroy'*/, 'class_name' => 'InlineImage'] ] ]; } + protected function callbacks() + { + return [ + 'before_destroy' => [ + 'destroy_inline_images' + ] + ]; + } + + protected function destroy_inline_images() + { + foreach ($this->inline_images as $i) { + $i->destroy(); + } + } + # Sequence numbers must start at 1 and increase monotonically, to keep the UI simple. # If we've been given sequences with gaps or duplicates, sanitize them. public function renumber_sequences() @@ -65,7 +81,7 @@ class Inline extends Rails\ActiveRecord\Base try { # Create one crop for the image, and InlineImage will create the sample and preview from that. - Moebooru\Reizer::resize($image->file_ext, $image->file_path(), $new_image->tempfile_image_path(), $size, 95); + Moebooru\Resizer::resize($image->file_ext, $image->file_path(), $new_image->tempfile_image_path(), $size, 95); chmod($new_image->tempfile_image_path(), 0775); } catch (Exception $e) { if (is_file($new_image->tempfile_image_path())) { @@ -85,10 +101,10 @@ class Inline extends Rails\ActiveRecord\Base public function api_attributes() { return [ - 'id' => $this->id, - 'description' => $this->description, - 'user_id' => $this->user_id, - 'images' => $this->inline_images->toArray() + 'id' => (int)$this->id, + 'description' => (string)$this->description, + 'user_id' => (int)$this->user_id, + 'images' => $this->inline_images->asJson() ]; } diff --git a/app/models/InlineImage.php b/app/models/InlineImage.php index f487509..89d52b3 100755 --- a/app/models/InlineImage.php +++ b/app/models/InlineImage.php @@ -26,6 +26,8 @@ */ class InlineImage extends Rails\ActiveRecord\Base { + use Moebooru\TempfilePrefix; + public $source; public $received_file; @@ -64,17 +66,17 @@ class InlineImage extends Rails\ActiveRecord\Base public function tempfile_image_path() { - return $this->temfile_prefix . '.upload'; + return $this->tempfile_prefix() . '.upload'; } public function tempfile_sample_path() { - return $this->temfile_prefix . '-sample.upload'; + return $this->tempfile_prefix() . '-sample.upload'; } public function tempfile_preview_path() { - return $this->temfile_prefix . '-preview.upload'; + return $this->tempfile_prefix() . '-preview.upload'; } /** @@ -102,15 +104,15 @@ class InlineImage extends Rails\ActiveRecord\Base } /** - * @param string $f + * @param Rails\ActionDispatch\Http\UploadedFile $f */ public function setFile($f) { - if (!is_file($f) || !filesize($f)) { + if (!$f->size()) { return; } - copy($f, $this->tempfile_image_path()); + copy($f->tempName(), $this->tempfile_image_path()); $this->got_file(); } @@ -143,7 +145,7 @@ class InlineImage extends Rails\ActiveRecord\Base } if (!file_exists($this->tempfile_image_path())) { - $this->errors()->add('base', "No file received"); + $this->errors()->addToBase("No file received"); return false; } @@ -156,7 +158,7 @@ class InlineImage extends Rails\ActiveRecord\Base } if (!in_array($this->file_ext, ['jpg', 'png', 'gif'])) { - $this->errors()->add('file', 'is an invalid content type: ' . $this->file_ext); + $this->errors()->add('file', 'is an invalid content type: ' . $this->file_ext ?: 'unknown'); } return true; @@ -191,8 +193,10 @@ class InlineImage extends Rails\ActiveRecord\Base return true; } - # We can generate the sample image during upload or offline. Use tempfile_image_path - # if it exists, otherwise use file_path. + /** + * We can generate the sample image during upload or offline. Use tempfile_image_path + * if it exists, otherwise use file_path. + */ $path = $this->tempfile_image_path(); if (!is_file($path)) { $path = $this->file_path(); @@ -263,8 +267,8 @@ class InlineImage extends Rails\ActiveRecord\Base return true; } - if (!is_dir(dirname($file_path))) { - mkdir(dirname($file_path), 0777, true); + if (!is_dir(dirname($this->file_path()))) { + mkdir(dirname($this->file_path()), 0777, true); } rename($this->tempfile_image_path(), $this->file_path()); @@ -290,7 +294,7 @@ class InlineImage extends Rails\ActiveRecord\Base return; } $siblings = $this->inline->inline_images; - $max_sequence = max($siblings->getAttributes('sequence')) ?: 0; + $max_sequence = ($siblings->getAttributes('sequence') && max($siblings->getAttributes('sequence'))) ?: 0; $this->sequence = $max_sequence + 1; } @@ -390,19 +394,19 @@ class InlineImage extends Rails\ActiveRecord\Base public function api_attributes() { return [ - 'id' => $this->id, - 'sequence' => $this->sequence, - 'md5' => $this->md5, - 'width' => $this->width, - 'height' => $this->height, - 'sample_width' => $this->sample_width, - 'sample_height' => $this->sample_height, - 'preview_width' => $this->preview_dimensions()['width'], + 'id' => (int)$this->id, + 'sequence' => $this->sequence, + 'md5' => $this->md5, + 'width' => (int)$this->width, + 'height' => (int)$this->height, + 'sample_width' => $this->sample_width, + 'sample_height' => $this->sample_height, + 'preview_width' => $this->preview_dimensions()['width'], 'preview_height' => $this->preview_dimensions()['height'], - 'description' => $this->description, - 'file_url' => $this->file_url(), - 'sample_url' => $this->sample_url(), - 'preview_url' => $this->preview_url() + 'description' => (string)$this->description, + 'file_url' => $this->file_url(), + 'sample_url' => $this->sample_url(), + 'preview_url' => $this->preview_url() ]; } diff --git a/app/views/inline/_footer.php b/app/views/inline/_footer.php new file mode 100755 index 0000000..6d8cc00 --- /dev/null +++ b/app/views/inline/_footer.php @@ -0,0 +1,5 @@ +contentFor('subnavbar', function(){ ?> +
  • linkTo($this->t('.list'), ['action' => "index"]) ?>
  • + content('footer') ?> + + diff --git a/app/views/inline/crop.php b/app/views/inline/crop.php new file mode 100755 index 0000000..4827db3 --- /dev/null +++ b/app/views/inline/crop.php @@ -0,0 +1,50 @@ +
    +t('.crop_avatar') ?> +

    + +

    + inline_image_tag($this->image, ['use_sample' => true], ['id' => "image"]) ?> +
    + + formTag([], ['id' => "crop", 'level' => 'member'], function(){ ?> + hiddenFieldTag("id", $this->image->id) ?> + hiddenFieldTag("left", 0) ?> + hiddenFieldTag("right", 0) ?> + hiddenFieldTag("top", 0) ?> + hiddenFieldTag("bottom", 0) ?> + + + +
    diff --git a/app/views/inline/edit.php b/app/views/inline/edit.php new file mode 100755 index 0000000..88e373a --- /dev/null +++ b/app/views/inline/edit.php @@ -0,0 +1,191 @@ +inline->inline_images->size() > 0) { + list($block, $script, $inline_html_id) = $this->format_inline($this->inline, 0, "inline", ""); +} else { + $block = $script = $inline_html_id = null; +} +?> + +
    + t('.tag', ['id' => $this->inline->id]) ?> + has_permission($this->inline)) : ?> + + | t('.preview') ?> + + inline->inline_images->size() < 9) : ?> + | t('.add_image') ?> + + +

    + + +

    +
    + +
    + +
    + +
    + inline->inline_images as $image) : ?> + formTag(['action' => "delete_image"], ['id' => "delete-image-" . $image->id], function() use ($image) { ?> + hiddenFieldTag("id", $image->id) ?> + + + + formTag('#update', function() { ?> + +
    + + + format_text($this->inline->description) ?> + + inline->description) echo $this->t('.add_set_description_info') ?> + +
    +
    + + +
    + + inline->inline_images as $image) : ?> +
    +
    + + has_permission($this->inline)) : ?> + linkTo($this->t('.remove'), "#", ['onclick' => "if(confirm('" . $this->t('.remove_confirm') . "')) $('delete-image-" . $image->id . "').submit(); return false;"]) ?> + | linkToFunction($this->t('.move.up'), "orderShift(" . $image->id . ", -1)") ?> + | linkToFunction($this->t('.move.down'), "orderShift(" . $image->id . ", +1)") ?> + | + + description) : ?> + t('.add_image_description') ?> + + h($image->description) ?> + + + + + + h($image->description) ?> + + +
    + + + hiddenFieldTag("image[" . $image->id . "][sequence]", $image->sequence, ['size' => 10, 'disabled' => (!current_user()->has_permission($this->inline)), 'class' => "inline-sequence"]) ?> +
    +
    + +
    + + + hiddenFieldTag("id", $this->inline->id) ?> + submitTag($this->t('.save'), ['disabled' => (!current_user()->has_permission($this->inline))]) ?> + +
    + + + + +formTag(['action' => "delete"], ['id' => "delete-group"], function(){ ?> + hiddenFieldTag("id", $this->inline->id) ?> + +formTag(['action' => "copy"], ['id' => "copy-group"], function() { ?> + hiddenFieldTag("id", $this->inline->id) ?> + + +contentFor('subnavbar', function() { ?> + has_permission($this->inline)) : ?> +
  • linkTo($this->t('.delete'), "#", ['onclick' => "if(confirm('" . $this->t('.delete_confirm') . "')) $('delete-group').submit(); return false;"]) ?>
  • +
  • linkTo($this->t('.crop'), ['action' => "crop", 'id' => $this->inline->id]) ?>
  • + + +
  • linkTo($this->t('.copy'), "#", ['onclick' => "$('copy-group').submit(); return false;", 'level' => 'member']) ?>
  • + + +partial("footer") ?> diff --git a/app/views/inline/index.php b/app/views/inline/index.php new file mode 100755 index 0000000..2bd489b --- /dev/null +++ b/app/views/inline/index.php @@ -0,0 +1,45 @@ +
    + + + + + + + + + + + + inlines as $p) : ?> + + + + + + + + + +
    t('.first_image') ?>t('.description2') ?>t('.user') ?>t('.images') ?>t('.created') ?>
    + $p->id]) ?>'> + inline_images->any()) : ?> + imageTag($p->inline_images[0]->preview_url(), ['alt' => "thumb", 'width' => $p->inline_images[0]->preview_dimensions()['width'], 'height' => $p->inline_images[0]->preview_dimensions()['height']]) ?> + + (no images) + + + h($p->description) ?>linkTo($this->h($p->user->pretty_name()), ["user#show", 'id' => $p->user->id]) ?>inline_images->size() ?>t('time.x_ago', ['t' => $this->t($this->timeAgoInWords($p->created_at))]) ?>
    +
    + +
    + willPaginate($this->inlines) ?> +
    + +formTag(["#create"], ['id' => "create-new"], function(){ ?> + + +contentFor('subnavbar', function() { ?> +
  • linkTo($this->t('.create'), "#", ['level' => 'member', 'onclick' => "$('create-new').submit(); return false;"]) ?>
  • + + +partial("footer") ?> diff --git a/config/locales/my-imouto.yml b/config/locales/my-imouto.yml index 6f1a04f..9c88e70 100755 --- a/config/locales/my-imouto.yml +++ b/config/locales/my-imouto.yml @@ -18,4 +18,10 @@ en: notes_create_notice: To create a note,
    Shift + Click + Drag
    over the image. More info in the %{notes_help}. (%{close}) static13a: Moderate - static62a: Index \ No newline at end of file + static62a: Index + + # These ones are missing: + inline: + index: + description2: Image-set description + created: Created \ No newline at end of file diff --git a/db/table_schema/production/inline_images.php b/db/table_schema/production/inline_images.php new file mode 100644 index 0000000..388c8ad --- /dev/null +++ b/db/table_schema/production/inline_images.php @@ -0,0 +1,74 @@ + + array ( + 'id' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'inline_id' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'md5' => + array ( + 'type' => 'varchar(32)', + 'default' => NULL, + ), + 'file_ext' => + array ( + 'type' => 'varchar(4)', + 'default' => NULL, + ), + 'description' => + array ( + 'type' => 'text', + 'default' => NULL, + ), + 'sequence' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'width' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'height' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'sample_width' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'sample_height' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'created_at' => + array ( + 'type' => 'datetime', + 'default' => NULL, + ), + 'updated_at' => + array ( + 'type' => 'datetime', + 'default' => NULL, + ), + ), + 1 => + array ( + 'pri' => + array ( + 0 => 'id', + ), + ), +) +; \ No newline at end of file diff --git a/db/table_schema/production/inlines.php b/db/table_schema/production/inlines.php new file mode 100644 index 0000000..418a185 --- /dev/null +++ b/db/table_schema/production/inlines.php @@ -0,0 +1,39 @@ + + array ( + 'id' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'user_id' => + array ( + 'type' => 'int(11)', + 'default' => NULL, + ), + 'description' => + array ( + 'type' => 'text', + 'default' => NULL, + ), + 'created_at' => + array ( + 'type' => 'datetime', + 'default' => NULL, + ), + 'updated_at' => + array ( + 'type' => 'datetime', + 'default' => NULL, + ), + ), + 1 => + array ( + 'pri' => + array ( + 0 => 'id', + ), + ), +) +; \ No newline at end of file diff --git a/lib/Moebooru/TempfilePrefix.php b/lib/Moebooru/TempfilePrefix.php new file mode 100755 index 0000000..a1c4b85 --- /dev/null +++ b/lib/Moebooru/TempfilePrefix.php @@ -0,0 +1,15 @@ +tempfile_prefix) { + $this->tempfile_prefix = \Rails::publicPath() . '/data/temp-' . uniqid('tfp_'); + } + return $this->tempfile_prefix; + } +}