diff --git a/database/migrations/2019_08_15_171345_create_files_table.php b/database/migrations/2019_08_15_171345_create_files_table.php index ce610b0b..93f78bbe 100644 --- a/database/migrations/2019_08_15_171345_create_files_table.php +++ b/database/migrations/2019_08_15_171345_create_files_table.php @@ -25,7 +25,6 @@ class CreateFilesTable extends Migration $table->text('filesize'); $table->text('type')->nullable(); - $table->longText('metadata')->nullable(); $table->enum('author', ['user', 'member', 'visitor'])->default('user'); diff --git a/database/migrations/2020_05_26_092649_create_user_settings_table.php b/database/migrations/2020_05_26_092649_create_user_settings_table.php index d35f3538..38fa4dcf 100644 --- a/database/migrations/2020_05_26_092649_create_user_settings_table.php +++ b/database/migrations/2020_05_26_092649_create_user_settings_table.php @@ -27,8 +27,8 @@ class CreateUserSettingsTable extends Migration $table->text('country')->nullable(); $table->text('phone_number')->nullable(); $table->decimal('timezone', 10, 1)->nullable(); - $table->text('emoji_type')->default('twemoji'); - $table->text('theme_mode')->default('system'); + $table->text('emoji_type'); + $table->text('theme_mode'); $table->charset = 'utf8mb4'; $table->collation = 'utf8mb4_unicode_ci'; }); diff --git a/database/migrations/2022_01_25_152729_create_exifs_table.php b/database/migrations/2022_01_25_152729_create_exifs_table.php new file mode 100644 index 00000000..6e35f3d5 --- /dev/null +++ b/database/migrations/2022_01_25_152729_create_exifs_table.php @@ -0,0 +1,54 @@ +uuid('id')->primary()->index(); + $table->uuid('file_id')->index(); + + $table->timestamp('date_time_original')->nullable(); + $table->string('artist')->nullable(); + $table->integer('height')->nullable(); + $table->integer('width')->nullable(); + $table->string('x_resolution')->nullable(); + $table->string('y_resolution')->nullable(); + $table->integer('color_space')->nullable(); + $table->string('camera')->nullable(); + $table->string('model')->nullable(); + $table->string('aperture_value')->nullable(); + $table->string('exposure_time')->nullable(); + $table->string('focal_length')->nullable(); + $table->integer('iso')->nullable(); + $table->string('aperture_f_number')->nullable(); + $table->string('ccd_width')->nullable(); + $table->string('longitude')->nullable(); + $table->string('latitude')->nullable(); + $table->string('longitude_ref')->nullable(); + $table->string('latitude_ref')->nullable(); + + $table->charset = 'utf8mb4'; + $table->collation = 'utf8mb4_unicode_ci'; + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('exifs'); + } +} diff --git a/resources/js/App.vue b/resources/js/App.vue index b3282af8..f85e45f7 100644 --- a/resources/js/App.vue +++ b/resources/js/App.vue @@ -54,9 +54,10 @@ export default { }, methods: { spotlightListener(e) { - if (e.key === 'k' && e.metaKey) { - events.$emit('spotlight:show') - } + if (e.key === 'k' && e.metaKey || e.key === 'k' && e.ctrlKey) { + e.preventDefault() + events.$emit('spotlight:show'); + } }, handleDarkMode() { const app = document.getElementsByTagName('html')[0] diff --git a/resources/js/components/FilesView/ImageMetaData.vue b/resources/js/components/FilesView/ImageMetaData.vue index d8d9c09d..9bbe96b8 100644 --- a/resources/js/components/FilesView/ImageMetaData.vue +++ b/resources/js/components/FilesView/ImageMetaData.vue @@ -1,108 +1,93 @@ @@ -111,34 +96,33 @@ export default { @import '../../../sass/vuefilemanager/mixins'; .meta-data-list { - list-style: none; - padding: 0px; - margin: 0px; + list-style: none; + padding: 0px; + margin: 0px; - li { - display: flex; - justify-content: space-between; - padding: 9px 0; - border-bottom: 1px solid $light_mode_border; + li { + display: flex; + justify-content: space-between; + padding: 9px 0; + border-bottom: 1px solid $light_mode_border; - b, - span { - @include font-size(14); - color: $text; - } - } + b, span { + @include font-size(14); + color: $text; + } + } } .dark { - .meta-data-list { - li { - border-color: $dark_mode_border_color; - b, - span { - color: $dark_mode_text_primary !important; - } - } - } + .meta-data-list { + li { + border-color: $dark_mode_border_color; + + b, span { + color: $dark_mode_text_primary !important; + } + } + } } - + \ No newline at end of file diff --git a/resources/js/components/FilesView/InfoSidebar.vue b/resources/js/components/FilesView/InfoSidebar.vue index 299caa13..97168400 100644 --- a/resources/js/components/FilesView/InfoSidebar.vue +++ b/resources/js/components/FilesView/InfoSidebar.vue @@ -113,44 +113,46 @@ import ListInfoItem from '../Others/ListInfoItem' import MemberAvatar from './MemberAvatar' import { mapGetters } from 'vuex' -export default { - name: 'InfoSidebar', - components: { - TeamMembersPreview, - FilePreviewDetail, - ImageMetaData, - CopyShareLink, - MemberAvatar, - TitlePreview, - ListInfoItem, - UnlockIcon, - EyeOffIcon, - Edit2Icon, - LockIcon, - }, - computed: { - ...mapGetters(['permissionOptions', 'clipboard', 'user']), - isEmpty() { - return this.clipboard.length === 0 - }, - isSingleFile() { - return this.clipboard.length === 1 - }, - singleFile() { - return this.clipboard[0] - }, - canShowMetaData() { - return ( - this.clipboard[0].data.attributes.metadata && this.clipboard[0].data.attributes.metadata.ExifImageWidth - ) - }, - isLocked() { - return this.clipboard[0].data.relationships.shared.protected - }, - sharedInfo() { - let title = this.permissionOptions.find((option) => { - return option.value === this.clipboard[0].data.relationships.shared.permission - }) + export default { + name: 'InfoSidebar', + components: { + TeamMembersPreview, + FilePreviewDetail, + ImageMetaData, + CopyShareLink, + MemberAvatar, + TitlePreview, + ListInfoItem, + UnlockIcon, + EyeOffIcon, + Edit2Icon, + LockIcon, + }, + computed: { + ...mapGetters([ + 'permissionOptions', + 'clipboard', + 'user', + ]), + isEmpty() { + return this.clipboard.length === 0 + }, + isSingleFile() { + return this.clipboard.length === 1 + }, + singleFile() { + return this.clipboard[0] + }, + canShowMetaData() { + return this.clipboard[0].data.relationships.exif + }, + isLocked() { + return this.clipboard[0].data.relationships.shared.protected + }, + sharedInfo() { + let title = this.permissionOptions.find(option => { + return option.value === this.clipboard[0].data.relationships.shared.permission + }) return title ? this.$t(title.label) : this.$t('shared.can_download') }, diff --git a/resources/js/components/FilesView/SearchBar.vue b/resources/js/components/FilesView/SearchBar.vue index b592b377..91a71ca3 100644 --- a/resources/js/components/FilesView/SearchBar.vue +++ b/resources/js/components/FilesView/SearchBar.vue @@ -22,15 +22,15 @@ diff --git a/resources/js/components/Spotlight/Spotlight.vue b/resources/js/components/Spotlight/Spotlight.vue index 47d870b1..fbcdc865 100644 --- a/resources/js/components/Spotlight/Spotlight.vue +++ b/resources/js/components/Spotlight/Spotlight.vue @@ -41,6 +41,7 @@ @keydown.delete="undoFilter" @keydown.enter="showSelected" @keydown.meta="showByShortcut" + @keydown.ctrl="showByShortcut" @keyup.down="onPageDown" @keyup.up="onPageUp" type="text" @@ -630,7 +631,7 @@ export default { return this.user.data.attributes.role === 'admin' }, metaKeyIcon() { - return this.$isApple() ? '⌘' : 'alt' + return this.$isApple() ? '⌘' : 'Ctrl' }, isNotEmptyQuery() { return this.query !== '' diff --git a/src/App/Users/Models/UserSetting.php b/src/App/Users/Models/UserSetting.php index 6188d6c4..8f76c04a 100644 --- a/src/App/Users/Models/UserSetting.php +++ b/src/App/Users/Models/UserSetting.php @@ -108,6 +108,8 @@ class UserSetting extends Model static::creating(function ($user) { $user->id = Str::uuid(); $user->color = config('vuefilemanager.colors')[rand(0, 5)]; + $user->emoji_type = 'twemoji'; + $user->theme_mode = 'system'; }); } } diff --git a/src/Domain/Files/Actions/StoreFileExifMetadataAction.php b/src/Domain/Files/Actions/StoreFileExifMetadataAction.php new file mode 100644 index 00000000..01886c4d --- /dev/null +++ b/src/Domain/Files/Actions/StoreFileExifMetadataAction.php @@ -0,0 +1,41 @@ +exif()->create([ + 'date_time_original' => $data->DateTimeOriginal ?? null, + 'artist' => $data->OwnerName ?? null, + 'width' => $data->COMPUTED->Width ?? null, + 'height' => $data->COMPUTED->Height ?? null, + 'x_resolution' => $data->XResolution ?? null, + 'y_resolution' => $data->YResolution ?? null, + 'color_space' => $data->ColorSpace ?? null, + 'camera' => $data->Make ?? null, + 'model' => $data->Model ?? null, + 'aperture_value' => $data->ApertureValue ?? null, + 'exposure_time' => $data->ExposureTime ?? null, + 'focal_length' => $data->FocalLength ?? null, + 'iso' => $data->ISOSpeedRatings ?? null, + 'aperture_f_number' => $data->COMPUTED->ApertureFNumber ?? null, + 'ccd_width' => $data->COMPUTED->CCDWidth ?? null, + 'longitude' => $data->GPSLongitude ?? null, + 'latitude' => $data->GPSLatitude ?? null, + 'longitude_ref' => $data->GPSLongitudeRef ?? null, + 'latitude_ref' => $data->GPSLatitudeRef ?? null + ]); + } + + } +} diff --git a/src/Domain/Files/Actions/UploadFileAction.php b/src/Domain/Files/Actions/UploadFileAction.php index 9098433d..56786fa5 100644 --- a/src/Domain/Files/Actions/UploadFileAction.php +++ b/src/Domain/Files/Actions/UploadFileAction.php @@ -11,6 +11,7 @@ use Domain\Files\Models\File as UserFile; use Domain\Traffic\Actions\RecordUploadAction; use App\Users\Exceptions\InvalidUserActionException; use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Domain\Files\Actions\StoreFileExifMetadataAction; class UploadFileAction { @@ -19,6 +20,7 @@ class UploadFileAction public ProcessImageThumbnailAction $createImageThumbnail, public GetFileParentId $getFileParentId, public MoveFileToExternalStorageAction $moveFileToExternalStorage, + public StoreFileExifMetadataAction $storeExifMetadata, ) { } @@ -57,8 +59,6 @@ class UploadFileAction // If last then process file if ($request->boolean('is_last')) { - $metadata = get_image_meta_data($file); - $disk_local = Storage::disk('local'); // Get user data @@ -90,18 +90,23 @@ class UploadFileAction // Store user upload size ($this->recordUpload)($fileSize, $user->id); - // Return new file - return UserFile::create([ + // Create new file + $item = UserFile::create([ 'mimetype' => get_file_type_from_mimetype($file_mimetype), 'type' => get_file_type($file_mimetype), 'parent_id' => ($this->getFileParentId)($request, $user->id), - 'metadata' => $metadata, 'name' => $request->input('filename'), 'basename' => $fileName, 'author' => $userId ? 'visitor' : 'user', 'filesize' => $fileSize, 'user_id' => $user->id, ]); + + // Store exif metadata for files + ($this->storeExifMetadata)($item, $file); + + // Return new file + return $item; } } } diff --git a/src/Domain/Files/Models/Exif.php b/src/Domain/Files/Models/Exif.php new file mode 100644 index 00000000..2af4554b --- /dev/null +++ b/src/Domain/Files/Models/Exif.php @@ -0,0 +1,44 @@ + 'array', + 'latitude' => 'array', + ]; + + /** + * Get parent + */ + public function file(): HasOne + { + return $this->HasOne(File::class, 'id', 'file_id'); + } + + public static function boot() + { + parent::boot(); + + static::creating(function ($model) { + $model->id = (string) Str::uuid(); + }); + } +} diff --git a/src/Domain/Files/Models/File.php b/src/Domain/Files/Models/File.php index 17cc21f9..7ec82454 100644 --- a/src/Domain/Files/Models/File.php +++ b/src/Domain/Files/Models/File.php @@ -27,7 +27,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; * @property string thumbnail * @property string filesize * @property string type - * @property array metadata * @property string basename * @property string name * @property string mimetype @@ -55,10 +54,6 @@ class File extends Model 'file_url', ]; - protected $casts = [ - 'metadata' => 'array', - ]; - public array $sortable = [ 'name', 'created_at', @@ -201,6 +196,11 @@ class File extends Model return $this->hasOne(User::class, 'id', 'user_id'); } + public function exif(): HasOne + { + return $this->hasOne(Exif::class); + } + public function toSearchableArray(): array { $name = mb_convert_encoding( @@ -225,5 +225,11 @@ class File extends Model static::creating(function ($file) { $file->id = (string) Str::uuid(); }); + + static::deleting(function($file) { + if($file->isForceDeleting()) { + $file->exif()->forceDelete(); + }; + }); } } diff --git a/src/Domain/Files/Resources/FileResource.php b/src/Domain/Files/Resources/FileResource.php index 1bf74509..fa0f80c2 100644 --- a/src/Domain/Files/Resources/FileResource.php +++ b/src/Domain/Files/Resources/FileResource.php @@ -28,7 +28,6 @@ class FileResource extends JsonResource 'mimetype' => $this->mimetype, 'file_url' => $this->file_url, 'thumbnail' => $this->thumbnail, - 'metadata' => $this->metadata, 'parent_id' => $this->parent_id, 'created_at' => set_time_by_user_timezone($this->owner, $this->created_at), 'updated_at' => set_time_by_user_timezone($this->owner, $this->updated_at), @@ -64,6 +63,33 @@ class FileResource extends JsonResource ], ], ]), + $this->mergeWhen($this->exif, fn() => [ + 'exif' => [ + 'data' => [ + 'type' => 'exif', + 'id' => $this->exif->id, + 'attributes' => [ + 'date_time_original' => format_date($this->exif->date_time_original), + 'artist' => $this->exif->artist, + 'height' => $this->exif->height, + 'width' => $this->exif->width, + 'x_resolution' => substr($this->exif->x_resolution, 0, strrpos($this->exif->x_resolution, '/')), + 'y_resolution' => substr($this->exif->y_resolution, 0, strrpos($this->exif->y_resolution, '/')), + 'color_space' => $this->exif->color_space, + 'camera' => $this->exif->camera, + 'model' => $this->exif->model, + 'aperture_value' => intval($this->exif->aperture_value) / 100, + 'exposure_time' => $this->exif->exposure_time, + 'focal_length' => $this->exif->focal_length, + 'iso' => $this->exif->iso, + 'aperture_f_number' => $this->exif->aperture_f_number, + 'ccd_width' => $this->exif->ccd_width, + 'longitude' => format_gps_coordinates($this->exif->longitude, $this->exif->longitude_ref), + 'latitude' => format_gps_coordinates($this->exif->latitude, $this->exif->latitude_ref), + ], + ], + ] + ]) ], ], ]; diff --git a/src/Support/helpers.php b/src/Support/helpers.php index 01f92eeb..53a2b246 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -1082,4 +1082,21 @@ if (! function_exists('replace_occurrence')) { }); } } + + if(! function_exists('format_gps_coordinates')) { + /** + * Format GPS coordinates + */ + function format_gps_coordinates($coordinates, $ref) + { + if($coordinates && $ref) { + + return + explode('/',$coordinates[0])[0] . '°' . + explode('/', $coordinates[1])[0] . "'" . + substr(explode(',', $coordinates[2])[0], 0, 5) / 1000 . '"' . + $ref; + } + }; + } } diff --git a/tests/Domain/Files/FileTest.php b/tests/Domain/Files/FileTest.php index 9e6c31d3..9a23f4ce 100644 --- a/tests/Domain/Files/FileTest.php +++ b/tests/Domain/Files/FileTest.php @@ -385,4 +385,37 @@ class FileTest extends TestCase ); }); } + + /** + * @test + */ + public function it_store_file_exif_data_after_file_upload() + { + + $file = UploadedFile::fake() + ->image('fake-image.jpg', 2000, 2000); + + $user = User::factory() + ->hasSettings() + ->create(); + + $this + ->actingAs($user) + ->postJson('/api/upload', [ + 'filename' => $file->name, + 'file' => $file, + 'parent_id' => null, + 'path' => '/' . $file->name, + 'is_last' => 'true', + ])->assertStatus(201); + + $file = File::first(); + + $this->assertDatabaseHas('exifs', [ + 'file_id' => $file->id, + 'height' => 2000, + 'width' => 2000, + ]); + } + }