BLUE-13: improved the design and handling of the analysis screen. Also fixed bulk uploads to work since the storage changes in 1.1

This commit is contained in:
Andy Heathershaw 2016-10-30 18:36:34 +00:00
parent e3d3d4d8be
commit 5b915f911e
10 changed files with 186 additions and 66 deletions

2
.idea/webServers.xml generated
View File

@ -3,7 +3,7 @@
<component name="WebServers">
<option name="servers">
<webServer id="b14a34b0-0127-4886-964a-7be75a2281ac" name="Development" url="http://blue-twilight-dev.andys.eu">
<fileTransfer host="scar.andys.eu" port="22" privateKey="C:\Users\aheathershaw\.ssh\id_rsa" rootFolder="/srv/www/blue-twilight-dev" accessType="SFTP" keyPair="true">
<fileTransfer host="scar.andys.eu" port="22" privateKey="$USER_HOME$/.ssh/id_rsa" rootFolder="/srv/www/blue-twilight-dev" accessType="SFTP" keyPair="true">
<advancedOptions>
<advancedOptions dataProtectionLevel="Private" />
</advancedOptions>

View File

@ -2,11 +2,38 @@
namespace App\Helpers;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Symfony\Component\HttpFoundation\File\File;
class FileHelper
{
public static function deleteIfEmpty($folderPath)
{
// Another request may have got here first!
if (!is_dir($folderPath))
{
return;
}
$queueIterator = new \DirectoryIterator($folderPath);
$files = 0;
foreach ($queueIterator as $item)
{
if ($item->isDot())
{
continue;
}
$files++;
}
if ($files == 0)
{
@rmdir($folderPath);
}
}
public static function getQueuePath($queueUid)
{
$path = join(DIRECTORY_SEPARATOR, [
@ -25,6 +52,27 @@ class FileHelper
return $path;
}
public static function saveExtractedFile(File $extractedFile, $destinationPath, $overrideFilename = null)
{
$tempFilename = join(DIRECTORY_SEPARATOR, [
$destinationPath,
is_null($overrideFilename) ? MiscHelper::randomString(20) : basename($overrideFilename)
]);
// Only add an extension if an override filename was not given, assume this is present
if (is_null($overrideFilename))
{
$extension = $extractedFile->guessExtension();
if (!is_null($extension))
{
$tempFilename .= '.' . $extension;
}
}
$extractedFile->move(dirname($tempFilename), basename($tempFilename));
return new File($tempFilename);
}
public static function saveUploadedFile(UploadedFile $uploadedFile, $destinationPath, $overrideFilename = null)
{
$tempFilename = join(DIRECTORY_SEPARATOR, [

View File

@ -65,7 +65,7 @@ class ImageHelper
}
// TODO make the /tmp folder configurable
$tempName = tempnam('/tmp', 'btw_thumb_');
$tempName = tempnam(sys_get_temp_dir(), 'btw_thumb_');
$tempNameWithExtension = ($tempName . '.jpg');
rename($tempName, $tempNameWithExtension);

View File

@ -37,6 +37,11 @@ class AlbumController extends Controller
->orderBy('created_at')
->get();
if (count($photos) == 0)
{
return redirect(route('albums.show', ['id' => $album->id]));
}
return Theme::render('admin.analyse_album', ['album' => $album, 'photos' => $photos, 'queue_token' => $queue_token]);
}

View File

@ -306,6 +306,7 @@ class PhotoController extends Controller
$zip->open($archiveFile->getPathname());
$zip->extractTo($queueFolder);
$zip->close();
@unlink($archiveFile->getPathname());
break;
default:
@ -329,6 +330,13 @@ class PhotoController extends Controller
continue;
}
if (substr($fileInfo->getFilename(), 0, 1) == '.')
{
// Temporary/hidden file - skip
@unlink($fileInfo->getPathname());
continue;
}
$result = getimagesize($fileInfo->getPathname());
if ($result === false)
{
@ -340,7 +348,7 @@ class PhotoController extends Controller
$photoFile = new File($fileInfo->getPathname());
/** @var File $savedFile */
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($photoFile);
$savedFile = FileHelper::saveExtractedFile($photoFile, $queueFolder);
$photo = new Photo();
$photo->album_id = $album->id;
@ -354,8 +362,6 @@ class PhotoController extends Controller
$photo->save();
}
@rmdir($queueFolder);
return redirect(route('albums.analyse', [
'id' => $album->id,
'queue_token' => $queueUid

View File

@ -51,75 +51,87 @@ class PhotoService
public function analyse($queueToken)
{
$photoFile = join(DIRECTORY_SEPARATOR, [
FileHelper::getQueuePath($queueToken),
$this->photo->storage_file_name
]);
$queuePath = FileHelper::getQueuePath($queueToken);
$photoFile = join(DIRECTORY_SEPARATOR, [$queuePath, $this->photo->storage_file_name]);
$imageInfo = null;
$originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo);
if ($originalPhotoResource === false)
try
{
throw new \Exception(sprintf('The image "%s" does not appear to be a valid image, or cannot be read', pathinfo($photoFile, PATHINFO_FILENAME)));
}
$this->photo->width = $imageInfo[0];
$this->photo->height = $imageInfo[1];
$this->photo->mime_type = $imageInfo['mime'];
// Read the Exif data
$exifData = @exif_read_data($photoFile);
$isExifDataFound = ($exifData !== false && is_array($exifData));
$angleToRotate = 0;
// If Exif data contains an Orientation, ensure we rotate the original image as such
if ($isExifDataFound && isset($exifData['Orientation']))
{
switch ($exifData['Orientation'])
$imageInfo = null;
$originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo);
if ($originalPhotoResource === false)
{
case 3:
$angleToRotate = 180;
break;
case 6:
$angleToRotate = 270;
break;
case 8:
$angleToRotate = 90;
break;
throw new \Exception(sprintf('The image "%s" does not appear to be a valid image, or cannot be read', pathinfo($photoFile, PATHINFO_FILENAME)));
}
if ($angleToRotate > 0)
{
$originalPhotoResource = $this->imageHelper->rotateImage($originalPhotoResource, $angleToRotate);
$this->photo->width = $imageInfo[0];
$this->photo->height = $imageInfo[1];
$this->photo->mime_type = $imageInfo['mime'];
if ($angleToRotate == 90 || $angleToRotate == 270)
// Read the Exif data
$exifData = @exif_read_data($photoFile);
$isExifDataFound = ($exifData !== false && is_array($exifData));
$angleToRotate = 0;
// If Exif data contains an Orientation, ensure we rotate the original image as such
if ($isExifDataFound && isset($exifData['Orientation']))
{
switch ($exifData['Orientation'])
{
$this->photo->width = $imageInfo[1];
$this->photo->height = $imageInfo[0];
case 3:
$angleToRotate = 180;
break;
case 6:
$angleToRotate = 270;
break;
case 8:
$angleToRotate = 90;
break;
}
$this->imageHelper->saveImage($originalPhotoResource, $photoFile, $imageInfo);
if ($angleToRotate > 0)
{
$originalPhotoResource = $this->imageHelper->rotateImage($originalPhotoResource, $angleToRotate);
if ($angleToRotate == 90 || $angleToRotate == 270)
{
$this->photo->width = $imageInfo[1];
$this->photo->height = $imageInfo[0];
}
$this->imageHelper->saveImage($originalPhotoResource, $photoFile, $imageInfo);
}
}
}
if ($isExifDataFound)
if ($isExifDataFound)
{
$this->photo->metadata_version = self::METADATA_VERSION;
$this->photo->taken_at = $this->metadataDateTime($exifData);
$this->photo->camera_make = $this->metadataCameraMake($exifData);
$this->photo->camera_model = $this->metadataCameraModel($exifData);
$this->photo->camera_software = $this->metadataCameraSoftware($exifData);
}
$this->photo->is_analysed = true;
$this->photo->save();
// Save the original
$this->albumSource->saveThumbnail($this->photo, $photoFile);
$this->regenerateThumbnails($originalPhotoResource);
}
catch (\Exception $ex)
{
$this->photo->metadata_version = self::METADATA_VERSION;
$this->photo->taken_at = $this->metadataDateTime($exifData);
$this->photo->camera_make = $this->metadataCameraMake($exifData);
$this->photo->camera_model = $this->metadataCameraModel($exifData);
$this->photo->camera_software = $this->metadataCameraSoftware($exifData);
throw $ex;
}
finally
{
@unlink($photoFile);
$this->photo->is_analysed = true;
$this->photo->save();
// Save the original
$this->albumSource->saveThumbnail($this->photo, $photoFile);
$this->regenerateThumbnails($originalPhotoResource);
// If the queue directory is now empty, get rid of it
FileHelper::deleteIfEmpty($queuePath);
}
}
public function changeAlbum(Album $newAlbum)
@ -213,6 +225,7 @@ class PhotoService
{
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail);
$this->albumSource->saveThumbnail($this->photo, $generatedThumbnailPath, $thumbnail['name']);
@unlink($generatedThumbnailPath);
}
if (is_null($originalPhotoResource) && !is_null($photoPath))

View File

@ -3,6 +3,8 @@ function AnalyseAlbumViewModel() {
self.imagesFailed = ko.observableArray();
self.imagesToAnalyse = ko.observableArray();
self.imagesInProgress = ko.observableArray();
self.imagesRecentlyCompleted = ko.observableArray();
self.numberSuccessful = ko.observable(0);
self.numberFailed = ko.observable(0);
@ -23,11 +25,20 @@ function AnalyseAlbumViewModel() {
// When an image is added to the array, automatically issue it for analysis
self.imagesToAnalyse.subscribe(function (changes) {
// We only care about additions
if (changes[0].status !== 'added')
{
return;
}
// changes[0].value is an instance of AnalyseImageViewModel
var item = changes[0].value;
$.ajax(
item.url(),
{
beforeSend: function() {
self.imagesInProgress.push(item);
},
dataType: 'json',
error: function (xhr, textStatus, errorThrown) {
self.numberFailed(self.numberFailed() + 1);
@ -44,6 +55,15 @@ function AnalyseAlbumViewModel() {
self.numberSuccessful(self.numberSuccessful() + 1);
item.isSuccessful(true);
item.isPending(false);
// Push into our "recently completed" array
self.imagesRecentlyCompleted.push(item);
self.imagesInProgress.remove(item);
// Remove it again after a few seconds
window.setTimeout(function() {
self.imagesRecentlyCompleted.remove(item);
}, 2000);
}
else {
self.numberFailed(self.numberFailed() + 1);
@ -70,9 +90,12 @@ function AnalyseImageViewModel(image_info) {
self.url = ko.observable(image_info.url);
self.iconClass = ko.computed(function () {
var string = 'fa fa-fw ';
var string = 'fa fa-fw fa-';
if (!self.isPending()) {
if (self.isPending()) {
string += 'refresh';
}
else {
string += (self.isSuccessful() ? 'check text-success' : 'times text-danger')
}

View File

@ -17,6 +17,10 @@ return [
'album_security_intro' => 'The settings below affect the visibility of this album to other users.',
'album_settings_tab' => 'Settings',
'album_upload_tab' => 'Upload',
'analyse_and_more' => [
'and' => '... and ',
'others' => ' others'
],
'analyse_photos_failed' => 'The following items could not be analysed and were removed:',
'bulk_photos_changed' => ':number photo was updated successfully.|:number photos were updated successfully.',
'cannot_delete_own_user_account' => 'It is not possible to delete your own user account. Please ask another administrator to delete it for you.',

View File

@ -19,8 +19,17 @@
</div>
</div>
<div data-bind="foreach: imagesToAnalyse" style="margin-top: 20px;">
<p><span data-bind="text: name"></span> ... <i data-bind="css: iconClass"></i></p>
{{-- We display a queue of 3 recently completed items, and 5 up-next items, as well as a counter saying "... and XYZ more" --}}
{{-- That's what the 3's and 5's are in the next couple of blocks --}}
<div data-bind="foreach: imagesRecentlyCompleted.slice((imagesRecentlyCompleted().length - 3 < 0) ? 0 : imagesRecentlyCompleted().length - 3, 3)" style="margin-top: 20px;">
<p class="text-success"><span data-bind="text: name"></span> ... <i class="fa fa-fw fa-check"></i></p>
</div>
<div data-bind="foreach: imagesInProgress.slice(0, 5)" style="margin-top: 20px;">
<p><span data-bind="text: name"></span> ... <i data-bind="css: iconClass()"></i></p>
</div>
<div data-bind="visible: imagesInProgress().length > 5">
<p>@lang('admin.analyse_and_more.and') <span data-bind="text: imagesInProgress().length - 5"></span> @lang('admin.analyse_and_more.others')</p>
</div>
</div>
</div>

View File

@ -324,7 +324,19 @@
$('#bulk-upload-form').submit(function(event) {
// Set the in progress flag - no need to unset it as this is a synchronous process so the browser
// will reload the page in some way after completion
viewModel.isBulkUploadInProgress(true);
if (!viewModel.isBulkUploadInProgress())
{
viewModel.isBulkUploadInProgress(true);
// Wait a minute to give KnockoutJS chance to update the UI
window.setTimeout(function() {
$('#bulk-upload-form').submit();
}, 1000);
return false;
}
// Already set the flag, let the upload commence
return true;
});
}