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:
parent
e3d3d4d8be
commit
5b915f911e
@ -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>
|
||||
|
@ -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, [
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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]);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
}
|
||||
|
||||
|
@ -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.',
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user