Browse Source

nicer photo uploading experience

pull/82/head
Aaron Parecki 7 years ago
parent
commit
202a7876ec
No known key found for this signature in database GPG Key ID: 276C2817346D6056
4 changed files with 278 additions and 89 deletions
  1. +17
    -15
      lib/helpers.php
  2. +23
    -0
      public/css/style.css
  3. +4
    -0
      views/layout.php
  4. +234
    -74
      views/new-post.php

+ 17
- 15
lib/helpers.php View File

@ -363,21 +363,23 @@ function validate_photo(&$file) {
// Only does anything if the exif library is loaded, otherwise is a noop. // Only does anything if the exif library is loaded, otherwise is a noop.
function correct_photo_rotation($filename) { function correct_photo_rotation($filename) {
if(class_exists('IMagick')) { if(class_exists('IMagick')) {
$image = new IMagick($filename);
$orientation = $image->getImageOrientation();
switch($orientation) {
case IMagick::ORIENTATION_BOTTOMRIGHT:
$image->rotateImage(new ImagickPixel('#00000000'), 180);
break;
case IMagick::ORIENTATION_RIGHTTOP:
$image->rotateImage(new ImagickPixel('#00000000'), 90);
break;
case IMagick::ORIENTATION_LEFTBOTTOM:
$image->rotateImage(new ImagickPixel('#00000000'), -90);
break;
}
$image->setImageOrientation(IMagick::ORIENTATION_TOPLEFT);
$image->writeImage($filename);
try {
$image = new IMagick($filename);
$orientation = $image->getImageOrientation();
switch($orientation) {
case IMagick::ORIENTATION_BOTTOMRIGHT:
$image->rotateImage(new ImagickPixel('#00000000'), 180);
break;
case IMagick::ORIENTATION_RIGHTTOP:
$image->rotateImage(new ImagickPixel('#00000000'), 90);
break;
case IMagick::ORIENTATION_LEFTBOTTOM:
$image->rotateImage(new ImagickPixel('#00000000'), -90);
break;
}
$image->setImageOrientation(IMagick::ORIENTATION_TOPLEFT);
$image->writeImage($filename);
} catch(Exception $e){}
} }
} }

+ 23
- 0
public/css/style.css View File

@ -203,6 +203,29 @@ body {
} }
/**
* nicer file upload
*/
.btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
.glyphicon-spin { .glyphicon-spin {
-webkit-animation: spin 1000ms infinite linear; -webkit-animation: spin 1000ms infinite linear;

+ 4
- 0
views/layout.php View File

@ -22,9 +22,13 @@
<script src="/js/jquery-1.7.1.min.js"></script> <script src="/js/jquery-1.7.1.min.js"></script>
<script src="/libs/localforage.js"></script> <script src="/libs/localforage.js"></script>
<script src="/bootstrap/js/bootstrap.min.js"></script>
<script src="/libs/tokenfield/bootstrap-tokenfield.min.js"></script> <script src="/libs/tokenfield/bootstrap-tokenfield.min.js"></script>
<link rel="stylesheet" href="/libs/tokenfield/bootstrap-tokenfield.min.css"> <link rel="stylesheet" href="/libs/tokenfield/bootstrap-tokenfield.min.css">
<link rel="stylesheet" href="/libs/tokenfield/tokenfield-typeahead.min.css"> <link rel="stylesheet" href="/libs/tokenfield/tokenfield-typeahead.min.css">
<script src="/libs/awesomplete/awesomplete.min.js"></script>
<link rel="stylesheet" href="/libs/awesomplete/awesomplete.css">
<link rel="stylesheet" href="/css/style.css"> <link rel="stylesheet" href="/css/style.css">

+ 234
- 74
views/new-post.php View File

@ -41,22 +41,11 @@
<input type="text" id="note_slug" value="" class="form-control"> <input type="text" id="note_slug" value="" class="form-control">
</div> </div>
<a href="javascript:expandPhotoSection();" id="expand-photo-section"><i class="glyphicon glyphicon-camera" style="color: #aaa; font-size: 36px;"></i></a>
<div class="form-group hidden" id="photo-section">
<label for="note_photo">Photo</label>
<input type="file" name="note_photo" id="note_photo" accept="image/*">
<a href="javascript:switchToManualPhotoURL();" id="note_manual_photo">enter photo url</a>
<a href="javascript:addPhotoURL();" class="hidden" id="add_photo">add photo</a>
<br>
<div id="photo_preview_container" class="hidden">
<img src="" id="photo_preview" style="max-width: 300px; max-height: 300px;">
<div>
<button type="button" class="btn btn-danger btn-sm" id="remove_photo"><span class="glyphicon glyphicon-trash" aria-hidden="true"></span> Remove image</button>
</div>
</div>
<div class="form-group hidden" id="photo-previews">
</div> </div>
<a href="javascript:addNewPhoto();" id="expand-photo-section"><i class="glyphicon glyphicon-camera" style="color: #aaa; font-size: 36px;"></i></a>
<div class="form-group" style="margin-top: 1em;"> <div class="form-group" style="margin-top: 1em;">
<label for="note_syndicate-to">Syndicate <a href="javascript:reload_syndications()">(refresh list)</a></label> <label for="note_syndicate-to">Syndicate <a href="javascript:reload_syndications()">(refresh list)</a></label>
<div id="syndication-container"> <div id="syndication-container">
@ -118,6 +107,62 @@
</div> </div>
</div> </div>
<!-- Add Photo -->
<div class="modal fade" id="photo-modal" tabindex="-1" role="dialog" aria-labelledby="photo-modal-title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="photo-modal-title">Add Photo</h4>
</div>
<div class="modal-body">
<div id="modal_photo_preview" class="hidden">
<img style="width:100%;">
</div>
<label id="note_photo_button" class="btn btn-default btn-file" style="margin-bottom: 1em;">
Choose File <input type="file" name="note_photo" id="note_photo" accept="image/*">
</label>
<input type="url" id="note_photo_url" class="form-control" placeholder="Paste image URL">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary save-btn">Add</button>
</div>
</div>
</div>
</div>
<!-- Edit Photo -->
<div class="modal fade" id="edit-photo-modal" tabindex="-1" role="dialog" aria-labelledby="edit-photo-modal-title" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="edit-photo-modal-title">Edit Photo</h4>
</div>
<div class="modal-body">
<div id="modal_edit_photo_preview" style="margin-bottom: 4px;">
<img style="width:100%;">
</div>
<input type="text" id="note_photo_alt" class="form-control hidden" placeholder="Image alt text">
<input type="hidden" id="modal_edit_photo_index">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger remove-btn" data-dismiss="modal">Remove</button>
<!-- <button type="button" class="btn btn-primary save-btn">Save</button> -->
</div>
</div>
</div>
</div>
<style type="text/css"> <style type="text/css">
#reply { #reply {
@ -130,6 +175,34 @@
font-weight: bold; font-weight: bold;
} }
#modal_photo_preview img, #modal_edit_photo_preview img {
width: 100%;
border-radius: 4px;
margin-bottom: 4px;
}
#photo-previews span {
width: 24%;
height: 180px;
margin-right: 1px;
position: relative;
overflow: hidden;
display: inline-block;
}
#photo-previews img {
position: absolute;
left: 50%;
top: 50%;
height: 100%;
width: auto;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
cursor: pointer;
}
.pcheck206 { color: #6ba15c; } /* tweet fits within the limit even after adding RT @username */ .pcheck206 { color: #6ba15c; } /* tweet fits within the limit even after adding RT @username */
.pcheck207 { color: #c4b404; } /* danger zone, tweet will overflow when RT @username is added */ .pcheck207 { color: #c4b404; } /* danger zone, tweet will overflow when RT @username is added */
.pcheck200,.pcheck208 { color: #59cb3a; } /* exactly fits 140 chars, both with or without RT */ .pcheck200,.pcheck208 { color: #59cb3a; } /* exactly fits 140 chars, both with or without RT */
@ -176,7 +249,7 @@ function saveNoteState() {
inReplyTo: $("#note_in_reply_to").val(), inReplyTo: $("#note_in_reply_to").val(),
category: $("#note_category").val(), category: $("#note_category").val(),
slug: $("#note_slug").val(), slug: $("#note_slug").val(),
photo: $("#note_photo_url").val()
photos: photos
}; };
state.syndications = []; state.syndications = [];
$("#syndication-container button.btn-info").each(function(i,btn){ $("#syndication-container button.btn-info").each(function(i,btn){
@ -193,8 +266,9 @@ function restoreNoteState() {
$("#note_in_reply_to").val(note.inReplyTo); $("#note_in_reply_to").val(note.inReplyTo);
$("#note_category").val(note.category); $("#note_category").val(note.category);
$("#note_slug").val(note.slug); $("#note_slug").val(note.slug);
if(note.photo) {
replacePhotoWithPhotoURL(note.photo);
if(note.photos) {
photos = note.photos;
refreshPhotoPreviews();
} }
if(note.inReplyTo) { if(note.inReplyTo) {
expandReplySection(); expandReplySection();
@ -212,33 +286,6 @@ function restoreNoteState() {
}); });
} }
function replacePhotoWithPhotoURL(url) {
$("#note_photo").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control">');
$(".note_photo_url").val(url);
$("#note_photo").remove();
$("#photo_preview").attr("src", url);
$("#photo_preview_container").removeClass("hidden");
$("#note_manual_photo").addClass("hidden");
}
function switchToManualPhotoURL() {
$("#note_photo").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control">');
$("#note_photo").remove();
$("#note_photo_url").change(function(){
$("#photo_preview").attr("src", $(this).val());
$("#photo_preview_container").removeClass("hidden");
});
$("#note_manual_photo").addClass("hidden");
$("#add_photo").removeClass("hidden");
}
function addPhotoURL() {
$(".note_photo_url:last").after('<input type="url" name="note_photo_url[]" value="" class="note_photo_url form-control" style="margin-top:2px;">');
if($(".note_photo_url").length == 4) {
$("#add_photo").remove();
}
}
function expandReplySection() { function expandReplySection() {
$("#expand-reply").click(); $("#expand-reply").click();
$("#note_in_reply_to").change(); $("#note_in_reply_to").change();
@ -251,37 +298,125 @@ function activateTokenField() {
}); });
} }
function expandPhotoSection() {
$("#photo-section").removeClass("hidden");
$("#expand-photo-section").addClass("hidden");
var hasMediaEndpoint = <?= $this->media_endpoint ? 'true' : 'false' ?>;
var photos = [];
function addNewPhoto() {
// Reset modal
$("#note_photo").val("");
$("#note_photo_url").val("");
$("#modal_photo_preview").addClass("hidden");
$("#note_photo_button").removeClass("hidden");
$("#note_photo_url").removeClass("hidden");
// Show the modal
$("#photo-modal").modal();
} }
$(function(){ $(function(){
$("#note_photo").on("change", function(e){
var userHasSetCategory = false;
// If the user has a media endpoint, upload the photo to it right now
if(hasMediaEndpoint) {
var formData = new FormData();
formData.append("null","null");
formData.append("photo", e.target.files[0]);
var request = new XMLHttpRequest();
request.open("POST", "/micropub/media");
request.onreadystatechange = function() {
if(request.readyState == XMLHttpRequest.DONE) {
try {
var response = JSON.parse(request.responseText);
if(response.location) {
$("#modal_photo_preview img").attr("src", response.location);
$("#note_photo_url").removeClass("hidden").val(response.location);
$("#note_photo_button").addClass("hidden");
} else {
console.log("Endpoint did not return a location header", response);
}
} catch(e) {
console.log(e);
}
}
}
request.send(formData);
} else {
$("#modal_photo_preview img").attr("src", URL.createObjectURL(e.target.files[0]));
}
$("#modal_photo_preview").removeClass("hidden");
$("#note_photo_button").addClass("hidden");
$("#note_photo_url").addClass("hidden");
});
var hasMediaEndpoint = <?= $this->media_endpoint ? 'true' : 'false' ?>;
$("#note_photo_url").on("change", function(){
$("#modal_photo_preview img").attr("src", $(this).val());
$("#modal_photo_preview").removeClass("hidden");
$("#note_photo_button").addClass("hidden");
});
$("#note_content, #note_category, #note_in_reply_to, #note_slug, #note_photo_url").on('keyup change', function(e){
$("#photo-modal .save-btn").click(function(){
if($("#note_photo_url").val()) {
photos.push({
url: $("#note_photo_url").val(),
external: true
});
} else {
photos.push({
url: URL.createObjectURL(document.getElementById("note_photo").files[0]),
file: document.getElementById("note_photo").files[0],
external: false
});
}
$("#photo-modal").modal('hide');
refreshPhotoPreviews();
saveNoteState(); saveNoteState();
}); });
$("#note_content").on('keyup', function(){
var scrollHeight = document.getElementById("note_content").scrollHeight;
var currentHeight = parseInt($("#note_content").css("height"));
if(Math.abs(scrollHeight - currentHeight) > 20) {
$("#note_content").css("height", (scrollHeight+30)+"px");
$("#edit-photo-modal .save-btn").click(function(){
});
$("#edit-photo-modal .remove-btn").click(function(){
var new_photos = [];
for(i=0; i<photos.length; i++) {
if(i != $("#modal_edit_photo_index").val()) {
new_photos.push(photos[i]);
}
} }
photos = new_photos;
refreshPhotoPreviews();
saveNoteState();
}); });
$("#expand-reply").click(function(){
$('.reply-section').removeClass('hidden');
$(this).addClass('hidden');
return false;
});
function refreshPhotoPreviews() {
$("#photo-previews").html("");
for(i=0; i<photos.length; i++) {
console.log(photos[i]);
$("#photo-previews").append('<span><img src="'+photos[i].url+'"></span>');
}
if(photos.length == 0) {
$("#photo-previews").addClass("hidden");
} else {
$("#photo-previews").removeClass("hidden");
}
$("#photo-previews img").unbind("click").bind("click", function(){
console.log("Photo was tapped: "+$(this).attr("src"));
$("#modal_edit_photo_preview img").attr("src", $(this).attr("src"));
var index = false;
for(i=0; i<photos.length; i++) {
if(photos[i].url == $(this).attr("src")) {
index = i;
}
}
$("#modal_edit_photo_index").val(index);
$("#edit-photo-modal").modal();
}); });
}
// Preview the photo when one is chosen
$("#photo_preview_container").addClass("hidden");
/*
$("#note_photo").on("change", function(e){ $("#note_photo").on("change", function(e){
// If the user has a media endpoint, upload the photo to it right now // If the user has a media endpoint, upload the photo to it right now
if(hasMediaEndpoint) { if(hasMediaEndpoint) {
@ -314,14 +449,34 @@ $(function(){
$("#photo_preview_container").removeClass("hidden"); $("#photo_preview_container").removeClass("hidden");
} }
}); });
$("#remove_photo").on("click", function(){
$("#note_photo").val("");
$(".note_photo_url").val("");
$("#photo_preview").attr("src", "" );
$("#photo_preview_container").addClass("hidden");
*/
$(function(){
var userHasSetCategory = false;
$("#note_content, #note_category, #note_in_reply_to, #note_slug").on('keyup change', function(e){
saveNoteState(); saveNoteState();
}); });
$("#note_content").on('keyup', function(){
var scrollHeight = document.getElementById("note_content").scrollHeight;
var currentHeight = parseInt($("#note_content").css("height"));
if(Math.abs(scrollHeight - currentHeight) > 20) {
$("#note_content").css("height", (scrollHeight+30)+"px");
}
});
$("#expand-reply").click(function(){
$('.reply-section').removeClass('hidden');
$(this).addClass('hidden');
return false;
});
// Preview the photo when one is chosen
$("#photo_preview_container").addClass("hidden");
$("#note_content").on('change keyup', function(e){ $("#note_content").on('change keyup', function(e){
var text = $("#note_content").val(); var text = $("#note_content").val();
var tweet_length = tw_text_proxy(text).length; var tweet_length = tw_text_proxy(text).length;
@ -443,15 +598,20 @@ $(function(){
formData.append("<?= $this->user->micropub_slug_field ?>", v); formData.append("<?= $this->user->micropub_slug_field ?>", v);
} }
// Add either the photo as a file, or the photo URL depending on whether the user has a media endpoint
if(document.getElementById("note_photo") && document.getElementById("note_photo").files[0]) {
formData.append("photo", document.getElementById("note_photo").files[0]);
} else if($(".note_photo_url").val()) {
$(".note_photo_url").each(function(){
if($(this).val()) {
formData.append("photo[]", $(this).val());
if(photos.length == 1) {
if(photos[0].external) {
formData.append("photo", photos[0].url);
} else {
formData.append("photo", photos[0].file);
}
} else {
for(i=0; i<photos.length; i++) {
if(photos[i].external) {
formData.append("photo[]", photos[i].url);
} else {
formData.append("photo[]", photos[i].file);
} }
});
}
} }
// Need to append a placeholder field because if the file size max is hit, $_POST will // Need to append a placeholder field because if the file size max is hit, $_POST will

Loading…
Cancel
Save