From 0c2807963242946cbe696a4be5cdf7cc8fd0dce4 Mon Sep 17 00:00:00 2001 From: ildyria Date: Sun, 20 Oct 2024 23:35:43 +0200 Subject: [PATCH 1/9] WIP --- .../js/components/drawers/AlbumStatistics.vue | 220 ++++++++++++++++++ resources/js/components/gallery/AlbumHero.vue | 31 ++- .../js/composables/album/albumRefresher.ts | 1 - resources/js/services/statistics-service.ts | 4 +- resources/js/views/Statistics.vue | 3 +- resources/js/views/gallery-panels/Album.vue | 23 +- 6 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 resources/js/components/drawers/AlbumStatistics.vue diff --git a/resources/js/components/drawers/AlbumStatistics.vue b/resources/js/components/drawers/AlbumStatistics.vue new file mode 100644 index 00000000000..510b0d3936c --- /dev/null +++ b/resources/js/components/drawers/AlbumStatistics.vue @@ -0,0 +1,220 @@ + + diff --git a/resources/js/components/gallery/AlbumHero.vue b/resources/js/components/gallery/AlbumHero.vue index 088f9855095..0238a5e8faa 100644 --- a/resources/js/components/gallery/AlbumHero.vue +++ b/resources/js/components/gallery/AlbumHero.vue @@ -10,7 +10,7 @@ From 582bd0d12fdf68d97aa5b0ac268df67ed0dae31c Mon Sep 17 00:00:00 2001 From: ildyria Date: Mon, 21 Oct 2024 12:45:12 +0200 Subject: [PATCH 3/9] Formatting --- .../js/components/statistics/AlbumsTable.vue | 110 +++++++++--------- .../statistics/SizeVariantMeter.vue | 83 +++++++------ .../js/components/statistics/TotalCard.vue | 45 ++++--- 3 files changed, 117 insertions(+), 121 deletions(-) diff --git a/resources/js/components/statistics/AlbumsTable.vue b/resources/js/components/statistics/AlbumsTable.vue index 512285d2d75..df6610d71e6 100644 --- a/resources/js/components/statistics/AlbumsTable.vue +++ b/resources/js/components/statistics/AlbumsTable.vue @@ -1,72 +1,71 @@ \ No newline at end of file + diff --git a/resources/js/components/statistics/SizeVariantMeter.vue b/resources/js/components/statistics/SizeVariantMeter.vue index 5de8094a53c..fe1483de319 100644 --- a/resources/js/components/statistics/SizeVariantMeter.vue +++ b/resources/js/components/statistics/SizeVariantMeter.vue @@ -1,42 +1,42 @@ \ No newline at end of file + diff --git a/resources/js/components/statistics/TotalCard.vue b/resources/js/components/statistics/TotalCard.vue index 7e0839333ab..444ad399028 100644 --- a/resources/js/components/statistics/TotalCard.vue +++ b/resources/js/components/statistics/TotalCard.vue @@ -1,30 +1,29 @@ \ No newline at end of file + From bac00e809bb4a66325d92d08306f710bb7250770 Mon Sep 17 00:00:00 2001 From: ildyria Date: Mon, 21 Oct 2024 13:38:02 +0200 Subject: [PATCH 4/9] more work --- app/Actions/Statistics/Spaces.php | 49 +++++++++- app/Http/Controllers/StatisticsController.php | 16 +-- .../Statistics/SpacePerAlbumRequest.php | 11 +-- .../Statistics/SpaceSizeVariantRequest.php | 60 ++++++++++++ .../js/components/drawers/AlbumStatistics.vue | 98 ++----------------- .../statistics/SizeVariantMeter.vue | 54 +++++----- .../js/composables/album/albumStatistics.ts | 89 +++++++++++++++++ resources/js/services/statistics-service.ts | 4 +- resources/js/views/Statistics.vue | 4 +- resources/js/views/gallery-panels/Album.vue | 1 - 10 files changed, 250 insertions(+), 136 deletions(-) create mode 100644 app/Http/Requests/Statistics/SpaceSizeVariantRequest.php create mode 100644 resources/js/composables/album/albumStatistics.ts diff --git a/app/Actions/Statistics/Spaces.php b/app/Actions/Statistics/Spaces.php index 90afd3d9b7a..c812443b5cc 100644 --- a/app/Actions/Statistics/Spaces.php +++ b/app/Actions/Statistics/Spaces.php @@ -63,7 +63,7 @@ public function getFullSpacePerUser(?int $owner_id = null): Collection * * @return Collection */ - public function getSpacePerSizeVariantType(?int $owner_id = null): Collection + public function getSpacePerSizeVariantTypePerUser(?int $owner_id = null): Collection { return DB::table('size_variants') ->when($owner_id !== null, fn ($query) => $query @@ -88,6 +88,53 @@ public function getSpacePerSizeVariantType(?int $owner_id = null): Collection ]); } + /** + * Return the amount of data stored on the server (optionally for an album). + * + * @param string $album_id + * + * @return Collection + */ + public function getSpacePerSizeVariantTypePerAlbum(string $album_id): Collection + { + $query = DB::table('albums') + ->where('albums.id', '=', $album_id) + ->joinSub( + query: DB::table('albums', 'descendants')->select('descendants.id', 'descendants._lft', 'descendants._rgt'), + as: 'descendants', + first: function (JoinClause $join) { + $join->on('albums._lft', '<=', 'descendants._lft') + ->on('albums._rgt', '>=', 'descendants._rgt'); + } + ) + ->joinSub( + query: DB::table('photos'), + as: 'photos', + first: 'photos.album_id', + operator: '=', + second: 'descendants.id', + ) + ->joinSub( + query: DB::table('size_variants')->select(['size_variants.id', 'size_variants.photo_id', 'size_variants.type', 'size_variants.filesize']), + as: 'size_variants', + first: 'size_variants.photo_id', + operator: '=', + second: 'photos.id', + ) + ->select( + 'size_variants.type', + DB::raw('SUM(size_variants.filesize) as size') + ) + ->groupBy('size_variants.type') + ->orderBy('size_variants.type', 'asc'); + + return $query->get() + ->map(fn ($item) => [ + 'type' => SizeVariantType::from($item->type), + 'size' => intval($item->size), + ]); + } + /** * Return size statistics per album. * diff --git a/app/Http/Controllers/StatisticsController.php b/app/Http/Controllers/StatisticsController.php index cbd1ec9a014..4fdabca7ea9 100644 --- a/app/Http/Controllers/StatisticsController.php +++ b/app/Http/Controllers/StatisticsController.php @@ -5,6 +5,7 @@ use App\Actions\Statistics\Spaces; use App\Http\Requests\Statistics\SpacePerAlbumRequest; use App\Http\Requests\Statistics\SpacePerUserRequest; +use App\Http\Requests\Statistics\SpaceSizeVariantRequest; use App\Http\Resources\Statistics\Album; use App\Http\Resources\Statistics\Sizes; use App\Http\Resources\Statistics\UserSpace; @@ -29,16 +30,19 @@ public function getSpacePerUser(SpacePerUserRequest $request, Spaces $spaces): C } /** - * @param SpacePerUserRequest $request - * @param Spaces $spaces + * @param SpaceSizeVariantRequest $request + * @param Spaces $spaces * * @return Collection */ - public function getSpacePerSizeVariantType(SpacePerUserRequest $request, Spaces $spaces): Collection + public function getSpacePerSizeVariantType(SpaceSizeVariantRequest $request, Spaces $spaces): Collection { - $spaceData = $spaces->getSpacePerSizeVariantType( - owner_id: $request->ownerId() - ); + $albumId = $request->album()?->id; + $ownerId = $albumId === null ? $request->ownerId() : null; + + $spaceData = $albumId === null + ? $spaces->getSpacePerSizeVariantTypePerUser(owner_id: $ownerId) + : $spaces->getSpacePerSizeVariantTypePerAlbum(album_id: $albumId); return Sizes::collect($spaceData); } diff --git a/app/Http/Requests/Statistics/SpacePerAlbumRequest.php b/app/Http/Requests/Statistics/SpacePerAlbumRequest.php index 0b899ae08e0..cd583a4d493 100644 --- a/app/Http/Requests/Statistics/SpacePerAlbumRequest.php +++ b/app/Http/Requests/Statistics/SpacePerAlbumRequest.php @@ -2,14 +2,13 @@ namespace App\Http\Requests\Statistics; -use App\Contracts\Http\Requests\HasAlbum; +use App\Contracts\Http\Requests\HasAbstractAlbum; use App\Contracts\Http\Requests\HasOwnerId; use App\Contracts\Http\Requests\RequestAttribute; use App\Contracts\Models\AbstractAlbum; use App\Http\Requests\BaseApiRequest; -use App\Http\Requests\Traits\HasAlbumTrait; +use App\Http\Requests\Traits\HasAbstractAlbumTrait; use App\Http\Requests\Traits\HasOwnerIdTrait; -use App\Models\Album; use App\Models\Configs; use App\Policies\AlbumPolicy; use App\Policies\SettingsPolicy; @@ -17,9 +16,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; -class SpacePerAlbumRequest extends BaseApiRequest implements HasAlbum, HasOwnerId +class SpacePerAlbumRequest extends BaseApiRequest implements HasAbstractAlbum, HasOwnerId { - use HasAlbumTrait; + use HasAbstractAlbumTrait; use HasOwnerIdTrait; /** @@ -51,7 +50,7 @@ protected function processValidatedValues(array $values, array $files): void { /** @var string|null */ $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; - $this->album = $albumID === null ? null : Album::query()->findOrFail($albumID); + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumID); // Filter only to user if user is not admin if (Auth::check() && Auth::user()?->may_administrate !== true) { diff --git a/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php b/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php new file mode 100644 index 00000000000..b85c18d3ac1 --- /dev/null +++ b/app/Http/Requests/Statistics/SpaceSizeVariantRequest.php @@ -0,0 +1,60 @@ +album === null) { + return Gate::check(UserPolicy::CAN_EDIT, [User::class]); + } + + return Gate::check(AlbumPolicy::CAN_ACCESS, [AbstractAlbum::class, $this->album]); + } + + /** + * {@inheritDoc} + */ + public function rules(): array + { + return [ + RequestAttribute::ALBUM_ID_ATTRIBUTE => ['sometimes', new RandomIDRule(true)], + ]; + } + + /** + * {@inheritDoc} + */ + protected function processValidatedValues(array $values, array $files): void + { + /** @var string|null */ + $albumID = $values[RequestAttribute::ALBUM_ID_ATTRIBUTE] ?? null; + $this->album = $this->albumFactory->findNullalbleAbstractAlbumOrFail($albumID); + + // Filter only to user if user is not admin + if (Auth::check() && Auth::user()?->may_administrate !== true) { + $this->owner_id = intval(Auth::id()); + } + } +} diff --git a/resources/js/components/drawers/AlbumStatistics.vue b/resources/js/components/drawers/AlbumStatistics.vue index 510b0d3936c..f567852a51d 100644 --- a/resources/js/components/drawers/AlbumStatistics.vue +++ b/resources/js/components/drawers/AlbumStatistics.vue @@ -15,6 +15,8 @@ + + ; -export type DataForTable = { key: string; value: number }; - -export type PhotoStats = { - iso: DataForTable[]; - focal: DataForTable[]; - lens: DataForTable[]; - model: DataForTable[]; - shutter: DataForTable[]; - aperture: DataForTable[]; - year: DataForTable[]; - month: DataForTable[]; - day: DataForTable[]; -}; - const props = defineProps<{ photos: App.Http.Resources.Models.PhotoResource[]; album: App.Http.Resources.Models.AlbumResource | App.Http.Resources.Models.SmartAlbumResource | App.Http.Resources.Models.TagAlbumResource; config: App.Http.Resources.GalleryConfigs.AlbumConfig; }>(); +const { getStatistics } = useAlbumsStatistics(); + const photosData = ref(getStatistics(props.photos)); const albumSpace = ref(undefined as undefined | App.Http.Resources.Statistics.Album[]); const totalAlbumSpace = ref(undefined as undefined | App.Http.Resources.Statistics.Album[]); -const is_collapsed = ref(false); - -const albumData = computed(() => { - if (is_collapsed.value === false) { - return albumSpace.value?.filter((a) => !a.is_nsfw || are_nsfw_visible.value); - } - return totalAlbumSpace.value?.filter((a) => !a.is_nsfw || are_nsfw_visible.value); -}); const total = computed(() => { if (albumSpace.value === undefined) { @@ -140,74 +124,6 @@ const total = computed(() => { return sumData; }); -function getStatistics(photos: App.Http.Resources.Models.PhotoResource[]): PhotoStats { - const stats = { - iso: {} as Record, - focal: {} as Record, - lens: {} as Record, - model: {} as Record, - shutter: {} as Record, - aperture: {} as Record, - year: {} as Record, - month: {} as Record, - day: {} as Record, - }; - for (const photo of photos) { - if (photo.precomputed.is_video || photo.precomputed.is_raw) { - continue; - } - - if (photo.iso) { - stats.iso[photo.iso] = stats.iso[photo.iso] ? stats.iso[photo.iso] + 1 : 1; - } - if (photo.focal) { - stats.focal[photo.focal] = stats.focal[photo.focal] ? stats.focal[photo.focal] + 1 : 1; - } - if (photo.preformatted.aperture) { - stats.aperture["ƒ / " + photo.preformatted.aperture] = stats.aperture["ƒ / " + photo.preformatted.aperture] - ? stats.aperture["ƒ / " + photo.preformatted.aperture] + 1 - : 1; - } - if (photo.lens) { - stats.lens[photo.lens] = stats.lens[photo.lens] ? stats.lens[photo.lens] + 1 : 1; - } - if (photo.model) { - stats.model[photo.model] = stats.model[photo.model] ? stats.model[photo.model] + 1 : 1; - } - if (photo.preformatted.shutter) { - stats.shutter[photo.preformatted.shutter] = stats.shutter[photo.preformatted.shutter] ? stats.shutter[photo.preformatted.shutter] + 1 : 1; - } - if (photo.taken_at) { - const year = photo.taken_at.slice(0, 4); - const month = photo.taken_at.slice(0, 7); - const day = photo.taken_at.slice(0, 10); - stats.year[year] = stats.year[year] ? stats.year[year] + 1 : 1; - stats.month[month] = stats.month[month] ? stats.month[month] + 1 : 1; - stats.day[day] = stats.day[day] ? stats.day[day] + 1 : 1; - } - } - - return { - iso: recordToType(stats.iso), - focal: recordToType(stats.focal), - lens: recordToType(stats.lens), - model: recordToType(stats.model), - shutter: recordToType(stats.shutter), - aperture: recordToType(stats.aperture), - year: recordToType(stats.year), - month: recordToType(stats.month), - day: recordToType(stats.day), - }; -} - -function recordToType(record: Record): DataForTable[] { - const data = [] as DataForTable[]; - Object.entries(record).forEach(([key, value]) => { - data.push({ key, value }); - }); - return data.sort((a, b) => b.value - a.value); -} - if (props.config.is_base_album) { StatisticsService.getAlbumSpace(props.album.id).then((response) => { albumSpace.value = response.data; diff --git a/resources/js/components/statistics/SizeVariantMeter.vue b/resources/js/components/statistics/SizeVariantMeter.vue index fe1483de319..5e17b2280d2 100644 --- a/resources/js/components/statistics/SizeVariantMeter.vue +++ b/resources/js/components/statistics/SizeVariantMeter.vue @@ -1,31 +1,26 @@ diff --git a/resources/js/components/gallery/AlbumHero.vue b/resources/js/components/gallery/AlbumHero.vue index 0238a5e8faa..b69a2b57ac3 100644 --- a/resources/js/components/gallery/AlbumHero.vue +++ b/resources/js/components/gallery/AlbumHero.vue @@ -46,14 +46,14 @@ @@ -70,12 +70,15 @@ diff --git a/resources/js/components/forms/basic/InputText.vue b/resources/js/components/forms/basic/InputText.vue index 3fb44208d1f..c8a644fb17c 100644 --- a/resources/js/components/forms/basic/InputText.vue +++ b/resources/js/components/forms/basic/InputText.vue @@ -17,7 +17,7 @@ import { ref } from "vue"; import InputText, { InputTextPassThroughOptions } from "primevue/inputtext"; import type { PassThroughOptions } from "primevue/passthrough"; -import type { DesignToken, PassThrough } from "@primevue/core"; +import type { DesignToken, Nullable, PassThrough } from "@primevue/core"; const props = defineProps<{ size?: "small" | "large" | undefined; @@ -32,6 +32,6 @@ const props = defineProps<{ }>(); const emits = defineEmits(["updated"]); -const modelValue = defineModel(); +const modelValue = defineModel>(); const classValue = ref((props.class ?? "") + " border-0 p-3 w-full border-b hover:border-b-primary-400 focus:border-b-primary-400"); diff --git a/resources/js/components/forms/basic/Password.vue b/resources/js/components/forms/basic/Password.vue index d335003bddc..c00e0088247 100644 --- a/resources/js/components/forms/basic/Password.vue +++ b/resources/js/components/forms/basic/Password.vue @@ -216,6 +216,6 @@ const props = defineProps<{ // class?: string; // }>(); -const modelValue = defineModel(); +const modelValue = defineModel>(); const inputClass = ref((props.inputClass ?? "") + " border-0 p-3 w-full border-b hover:border-b-danger-600 focus:border-b-danger-600"); diff --git a/resources/js/components/forms/basic/Textarea.vue b/resources/js/components/forms/basic/Textarea.vue index dea4c825c5e..0267c8286ea 100644 --- a/resources/js/components/forms/basic/Textarea.vue +++ b/resources/js/components/forms/basic/Textarea.vue @@ -16,7 +16,7 @@ import { ref } from "vue"; import Textarea, { TextareaPassThroughOptions } from "primevue/textarea"; import type { PassThroughOptions } from "primevue/passthrough"; -import type { DesignToken, PassThrough } from "@primevue/core"; +import type { DesignToken, Nullable, PassThrough } from "@primevue/core"; const props = defineProps<{ autoResize?: boolean | undefined; @@ -30,7 +30,7 @@ const props = defineProps<{ class?: string; }>(); -const modelValue = defineModel(); +const modelValue = defineModel>(); const classValue = ref( (props.class ?? "") + " p-3 w-full border-t-transparent border-r-transparent border-b border-l hover:border-b-primary-400 hover:border-l-primary-400 focus:border-b-primary-400 focus:border-l-primary-400", diff --git a/resources/js/components/forms/profile/ApiToken.vue b/resources/js/components/forms/profile/ApiToken.vue index 1c4670b7314..acdd5b00dc5 100644 --- a/resources/js/components/forms/profile/ApiToken.vue +++ b/resources/js/components/forms/profile/ApiToken.vue @@ -62,7 +62,7 @@ import InputText from "@/components/forms/basic/InputText.vue"; import ProfileService from "@/services/profile-service"; import { useToast } from "primevue/usetoast"; -const visible = defineModel(); +const visible = defineModel(); const isDisabled = ref(true); const token = ref(undefined as undefined | string); diff --git a/resources/js/components/forms/search/SearchBox.vue b/resources/js/components/forms/search/SearchBox.vue index 5adcd49a605..ab3ce22ac0a 100644 --- a/resources/js/components/forms/search/SearchBox.vue +++ b/resources/js/components/forms/search/SearchBox.vue @@ -33,6 +33,7 @@ const search = defineModel("search", { required: true }); const emits = defineEmits<{ search: [terms: string]; + clear: []; }>(); const isValid = computed(() => { @@ -40,6 +41,13 @@ const isValid = computed(() => { }); const debouncedFn = useDebounceFn(() => { - emits("search", search.value); + if (search.value === "" || !isValid.value) { + emits("clear"); + return; + } + + if (search.value !== undefined && search.value.length >= props.searchMinimumLengh) { + emits("search", search.value); + } }, 1000); diff --git a/resources/js/components/forms/settings/VersionField.vue b/resources/js/components/forms/settings/VersionField.vue deleted file mode 100644 index 5154a50fa9b..00000000000 --- a/resources/js/components/forms/settings/VersionField.vue +++ /dev/null @@ -1,72 +0,0 @@ - - diff --git a/resources/js/components/forms/sharing/ShareLine.vue b/resources/js/components/forms/sharing/ShareLine.vue index 7ed144ae55f..08f1a2924c7 100644 --- a/resources/js/components/forms/sharing/ShareLine.vue +++ b/resources/js/components/forms/sharing/ShareLine.vue @@ -1,6 +1,6 @@