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,
+ ]);
+ }
+
}