diff --git a/app/Contracts/Http/Requests/HasTimelineAlbum.php b/app/Contracts/Http/Requests/HasTimelineAlbum.php new file mode 100644 index 00000000000..4bdaef3f9bd --- /dev/null +++ b/app/Contracts/Http/Requests/HasTimelineAlbum.php @@ -0,0 +1,13 @@ +album_sorting = $request->albumSortingCriterion(); $album->photo_layout = $request->photoLayout(); + $album->album_timeline = $request->album_timeline(); + $album->photo_timeline = $request->photo_timeline(); + $album = $setHeader->do( album: $album, is_compact: $request->is_compact(), @@ -133,6 +136,7 @@ public function updateTagAlbum(UpdateTagAlbumRequest $request): EditableBaseAlbu $album->copyright = $request->copyright(); $album->photo_sorting = $request->photoSortingCriterion(); $album->photo_layout = $request->photoLayout(); + $album->photo_timeline = $request->photo_timeline(); $album->save(); return EditableBaseAlbumResource::fromModel($album); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 46c53d769e5..db7d6cf34a4 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -93,5 +93,6 @@ class Kernel extends HttpKernel 'login_required' => \App\Http\Middleware\LoginRequired::class, 'cache_control' => \App\Http\Middleware\CacheControl::class, 'support' => \LycheeVerify\Http\Middleware\VerifySupporterStatus::class, + 'config_integrity' => \App\Http\Middleware\ConfigIntegrity::class, ]; } diff --git a/app/Http/Middleware/ConfigIntegrity.php b/app/Http/Middleware/ConfigIntegrity.php new file mode 100644 index 00000000000..d2dc312f39a --- /dev/null +++ b/app/Http/Middleware/ConfigIntegrity.php @@ -0,0 +1,47 @@ +whereIn('key', self::SE_FIELDS)->update(['level' => 1]); + } catch (\Exception $e) { + // Do nothing: we are not installed yet, so we fail silently. + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Requests/Album/UpdateAlbumRequest.php b/app/Http/Requests/Album/UpdateAlbumRequest.php index 1a559356c2d..eda155e4968 100644 --- a/app/Http/Requests/Album/UpdateAlbumRequest.php +++ b/app/Http/Requests/Album/UpdateAlbumRequest.php @@ -11,6 +11,8 @@ use App\Contracts\Http\Requests\HasPhoto; use App\Contracts\Http\Requests\HasPhotoLayout; use App\Contracts\Http\Requests\HasPhotoSortingCriterion; +use App\Contracts\Http\Requests\HasTimelineAlbum; +use App\Contracts\Http\Requests\HasTimelinePhoto; use App\Contracts\Http\Requests\HasTitle; use App\Contracts\Http\Requests\RequestAttribute; use App\Contracts\Models\AbstractAlbum; @@ -22,6 +24,8 @@ use App\Enum\LicenseType; use App\Enum\OrderSortingType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelineAlbumGranularity; +use App\Enum\TimelinePhotoGranularity; use App\Http\Requests\BaseApiRequest; use App\Http\Requests\Traits\HasAlbumSortingCriterionTrait; use App\Http\Requests\Traits\HasAlbumTrait; @@ -33,19 +37,22 @@ use App\Http\Requests\Traits\HasPhotoLayoutTrait; use App\Http\Requests\Traits\HasPhotoSortingCriterionTrait; use App\Http\Requests\Traits\HasPhotoTrait; +use App\Http\Requests\Traits\HasTimelineAlbumTrait; +use App\Http\Requests\Traits\HasTimelinePhotoTrait; use App\Http\Requests\Traits\HasTitleTrait; use App\Models\Album; use App\Models\Photo; use App\Policies\AlbumPolicy; use App\Rules\CopyrightRule; use App\Rules\DescriptionRule; +use App\Rules\EnumRequireSupportRule; use App\Rules\RandomIDRule; use App\Rules\TitleRule; use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\ValidationException; -class UpdateAlbumRequest extends BaseApiRequest implements HasAlbum, HasTitle, HasDescription, HasLicense, HasPhotoSortingCriterion, HasAlbumSortingCriterion, HasCopyright, HasPhoto, HasCompactBoolean, HasPhotoLayout +class UpdateAlbumRequest extends BaseApiRequest implements HasAlbum, HasTitle, HasDescription, HasLicense, HasPhotoSortingCriterion, HasAlbumSortingCriterion, HasCopyright, HasPhoto, HasCompactBoolean, HasPhotoLayout, HasTimelineAlbum, HasTimelinePhoto { use HasAlbumTrait; use HasLicenseTrait; @@ -58,6 +65,8 @@ class UpdateAlbumRequest extends BaseApiRequest implements HasAlbum, HasTitle, H use HasAlbumSortingCriterionTrait; use HasCopyrightTrait; use HasPhotoLayoutTrait; + use HasTimelineAlbumTrait; + use HasTimelinePhotoTrait; public function authorize(): bool { @@ -92,6 +101,8 @@ public function rules(): array RequestAttribute::COPYRIGHT_ATTRIBUTE => ['present', 'nullable', new CopyrightRule()], RequestAttribute::IS_COMPACT_ATTRIBUTE => ['required', 'boolean'], RequestAttribute::HEADER_ID_ATTRIBUTE => ['present', new RandomIDRule(true)], + RequestAttribute::ALBUM_TIMELINE_ALBUM => ['present', 'nullable', new Enum(TimelineAlbumGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], + RequestAttribute::ALBUM_TIMELINE_PHOTO => ['present', 'nullable', new Enum(TimelinePhotoGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], ]; } @@ -129,6 +140,9 @@ protected function processValidatedValues(array $values, array $files): void $this->aspectRatio = AspectRatioType::tryFrom($values[RequestAttribute::ALBUM_ASPECT_RATIO_ATTRIBUTE]); $this->photoLayout = PhotoLayoutType::tryFrom($values[RequestAttribute::ALBUM_PHOTO_LAYOUT]); + $this->album_timeline = TimelineAlbumGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_ALBUM]); + $this->photo_timeline = TimelinePhotoGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_PHOTO]); + $this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE]; $this->is_compact = static::toBoolean($values[RequestAttribute::IS_COMPACT_ATTRIBUTE]); diff --git a/app/Http/Requests/Album/UpdateTagAlbumRequest.php b/app/Http/Requests/Album/UpdateTagAlbumRequest.php index 99a07ca451d..7bcf6550092 100644 --- a/app/Http/Requests/Album/UpdateTagAlbumRequest.php +++ b/app/Http/Requests/Album/UpdateTagAlbumRequest.php @@ -8,12 +8,14 @@ use App\Contracts\Http\Requests\HasPhotoSortingCriterion; use App\Contracts\Http\Requests\HasTagAlbum; use App\Contracts\Http\Requests\HasTags; +use App\Contracts\Http\Requests\HasTimelinePhoto; use App\Contracts\Http\Requests\HasTitle; use App\Contracts\Http\Requests\RequestAttribute; use App\DTO\PhotoSortingCriterion; use App\Enum\ColumnSortingPhotoType; use App\Enum\OrderSortingType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelinePhotoGranularity; use App\Http\Requests\BaseApiRequest; use App\Http\Requests\Traits\Authorize\AuthorizeCanEditAlbumTrait; use App\Http\Requests\Traits\HasCopyrightTrait; @@ -22,16 +24,18 @@ use App\Http\Requests\Traits\HasPhotoSortingCriterionTrait; use App\Http\Requests\Traits\HasTagAlbumTrait; use App\Http\Requests\Traits\HasTagsTrait; +use App\Http\Requests\Traits\HasTimelinePhotoTrait; use App\Http\Requests\Traits\HasTitleTrait; use App\Models\TagAlbum; use App\Rules\CopyrightRule; use App\Rules\DescriptionRule; +use App\Rules\EnumRequireSupportRule; use App\Rules\RandomIDRule; use App\Rules\TitleRule; use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\ValidationException; -class UpdateTagAlbumRequest extends BaseApiRequest implements HasTagAlbum, HasTitle, HasDescription, HasPhotoSortingCriterion, HasCopyright, HasTags, HasPhotoLayout +class UpdateTagAlbumRequest extends BaseApiRequest implements HasTagAlbum, HasTitle, HasDescription, HasPhotoSortingCriterion, HasCopyright, HasTags, HasPhotoLayout, HasTimelinePhoto { use HasTagAlbumTrait; use HasTitleTrait; @@ -40,6 +44,7 @@ class UpdateTagAlbumRequest extends BaseApiRequest implements HasTagAlbum, HasTi use HasCopyrightTrait; use HasTagsTrait; use HasPhotoLayoutTrait; + use HasTimelinePhotoTrait; use AuthorizeCanEditAlbumTrait; /** @@ -60,6 +65,7 @@ public function rules(): array RequestAttribute::TAGS_ATTRIBUTE . '.*' => 'required|string|min:1', RequestAttribute::COPYRIGHT_ATTRIBUTE => ['present', 'nullable', new CopyrightRule()], RequestAttribute::ALBUM_PHOTO_LAYOUT => ['present', 'nullable', new Enum(PhotoLayoutType::class)], + RequestAttribute::ALBUM_TIMELINE_PHOTO => ['present', 'nullable', new Enum(TimelinePhotoGranularity::class), new EnumRequireSupportRule(TimelinePhotoGranularity::class, [TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED], $this->verify)], ]; } @@ -88,6 +94,7 @@ protected function processValidatedValues(array $values, array $files): void new PhotoSortingCriterion($photoColumn->toColumnSortingType(), $photoOrder); $this->photoLayout = PhotoLayoutType::tryFrom($values[RequestAttribute::ALBUM_PHOTO_LAYOUT]); + $this->photo_timeline = TimelinePhotoGranularity::tryFrom($values[RequestAttribute::ALBUM_TIMELINE_PHOTO]); $this->copyright = $values[RequestAttribute::COPYRIGHT_ATTRIBUTE]; $this->tags = $values[RequestAttribute::TAGS_ATTRIBUTE]; } diff --git a/app/Http/Requests/Traits/HasTimelineAlbumTrait.php b/app/Http/Requests/Traits/HasTimelineAlbumTrait.php new file mode 100644 index 00000000000..cf0bfebc476 --- /dev/null +++ b/app/Http/Requests/Traits/HasTimelineAlbumTrait.php @@ -0,0 +1,18 @@ +album_timeline; + } +} diff --git a/app/Http/Requests/Traits/HasTimelinePhotoTrait.php b/app/Http/Requests/Traits/HasTimelinePhotoTrait.php new file mode 100644 index 00000000000..8ce84246e7f --- /dev/null +++ b/app/Http/Requests/Traits/HasTimelinePhotoTrait.php @@ -0,0 +1,18 @@ +photo_timeline; + } +} diff --git a/app/Http/Resources/Collections/RootAlbumResource.php b/app/Http/Resources/Collections/RootAlbumResource.php index 14499ccaf74..fb877003a70 100644 --- a/app/Http/Resources/Collections/RootAlbumResource.php +++ b/app/Http/Resources/Collections/RootAlbumResource.php @@ -3,9 +3,13 @@ namespace App\Http\Resources\Collections; use App\DTO\TopAlbumDTO; +use App\Enum\ColumnSortingType; +use App\Enum\TimelineAlbumGranularity; use App\Http\Resources\GalleryConfigs\RootConfig; use App\Http\Resources\Models\ThumbAlbumResource; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\RootAlbumRightsResource; +use App\Models\Configs; use Illuminate\Support\Collection; use Spatie\LaravelData\Data; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -54,6 +58,9 @@ public function __construct( $this->smart_albums = $smart_albums; $this->tag_albums = $tag_albums; $this->albums = $albums; + $sorting = Configs::getValueAsEnum('sorting_albums_col', ColumnSortingType::class); + $album_granularity = Configs::getValueAsEnum('timeline_albums_granularity', TimelineAlbumGranularity::class); + $this->albums = TimelineData::setTimeLineDataForAlbums($this->albums, $sorting, $album_granularity); $this->shared_albums = $shared_albums; $this->config = $config; $this->rights = $rights; diff --git a/app/Http/Resources/Editable/EditableBaseAlbumResource.php b/app/Http/Resources/Editable/EditableBaseAlbumResource.php index 18c5a563914..8bed48d53e1 100644 --- a/app/Http/Resources/Editable/EditableBaseAlbumResource.php +++ b/app/Http/Resources/Editable/EditableBaseAlbumResource.php @@ -7,6 +7,8 @@ use App\Enum\AspectRatioType; use App\Enum\LicenseType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelineAlbumGranularity; +use App\Enum\TimelinePhotoGranularity; use App\Models\Album; use App\Models\TagAlbum; use Spatie\LaravelData\Data; @@ -26,6 +28,8 @@ class EditableBaseAlbumResource extends Data public ?PhotoLayoutType $photo_layout; public ?string $header_id; public ?string $cover_id; + public ?TimelineAlbumGranularity $album_timeline; + public ?TimelinePhotoGranularity $photo_timeline; /** @var string[] */ public array $tags; public bool $is_model_album; @@ -43,6 +47,8 @@ public function __construct(Album|TagAlbum $album) $this->header_id = null; $this->cover_id = null; $this->photo_layout = $album->photo_layout; + $this->album_timeline = null; + $this->photo_timeline = $album->photo_timeline; if ($album instanceof Album) { $this->is_model_album = true; @@ -51,6 +57,7 @@ public function __construct(Album|TagAlbum $album) $this->header_id = $album->header_id; $this->cover_id = $album->cover_id; $this->aspect_ratio = $album->album_thumb_aspect_ratio; + $this->album_timeline = $album->album_timeline; } if ($album instanceof TagAlbum) { diff --git a/app/Http/Resources/GalleryConfigs/AlbumConfig.php b/app/Http/Resources/GalleryConfigs/AlbumConfig.php index ceb7e6b0d22..3ee452fe13c 100644 --- a/app/Http/Resources/GalleryConfigs/AlbumConfig.php +++ b/app/Http/Resources/GalleryConfigs/AlbumConfig.php @@ -6,6 +6,9 @@ use App\Enum\AspectRatioCSSType; use App\Enum\AspectRatioType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelineAlbumGranularity; +use App\Enum\TimelinePhotoGranularity; +use App\Http\Resources\Traits\HasTimelineData; use App\Models\Album; use App\Models\Configs; use App\Models\Extensions\BaseAlbum; @@ -18,6 +21,8 @@ #[TypeScript()] class AlbumConfig extends Data { + use HasTimelineData; + public bool $is_base_album; public bool $is_model_album; public bool $is_accessible; @@ -28,6 +33,8 @@ class AlbumConfig extends Data public bool $is_nsfw_warning_visible; public AspectRatioCSSType $album_thumb_css_aspect_ratio; public PhotoLayoutType $photo_layout; + public bool $is_album_timeline_enabled = false; + public bool $is_photo_timeline_enabled = false; public function __construct(AbstractAlbum $album) { @@ -53,6 +60,24 @@ public function __construct(AbstractAlbum $album) } $this->photo_layout = (($album instanceof BaseAlbum) ? $album->photo_layout : null) ?? Configs::getValueAsEnum('layout', PhotoLayoutType::class); + + // Set default values. + $this->is_photo_timeline_enabled = Configs::getValueAsBool('timeline_photos_enabled'); + $this->is_album_timeline_enabled = Configs::getValueAsBool('timeline_albums_enabled'); + + if ($album instanceof Album) { + $this->is_album_timeline_enabled = $album->album_timeline !== null || $this->is_album_timeline_enabled; + $this->is_album_timeline_enabled = $album->album_timeline !== TimelineAlbumGranularity::DISABLED && $this->is_album_timeline_enabled; + } + + if ($album instanceof BaseAlbum) { + $this->is_photo_timeline_enabled = $album->photo_timeline !== null || $this->is_photo_timeline_enabled; + $this->is_photo_timeline_enabled = $album->photo_timeline !== TimelinePhotoGranularity::DISABLED && $this->is_photo_timeline_enabled; + } + + // Masking to require login for timeline or allow it to be public. + $this->is_photo_timeline_enabled = $this->is_photo_timeline_enabled && (Configs::getValueAsBool('timeline_photos_public') || Auth::check()); + $this->is_album_timeline_enabled = $this->is_album_timeline_enabled && (Configs::getValueAsBool('timeline_albums_public') || Auth::check()); } public function setIsMapAccessible(): void diff --git a/app/Http/Resources/GalleryConfigs/InitConfig.php b/app/Http/Resources/GalleryConfigs/InitConfig.php index 19e988e9469..e5d539e1773 100644 --- a/app/Http/Resources/GalleryConfigs/InitConfig.php +++ b/app/Http/Resources/GalleryConfigs/InitConfig.php @@ -47,6 +47,9 @@ class InitConfig extends Data // Slideshow setting public int $slideshow_timeout; + // Timeline settings + public bool $is_timeline_left_border_visible; + // Site title & dropbox key if logged in as admin. public string $title; public string $dropbox_api_key; @@ -91,6 +94,9 @@ public function __construct() // Slideshow settings $this->slideshow_timeout = Configs::getValueAsInt('slideshow_timeout'); + // Timeline settings + $this->is_timeline_left_border_visible = Configs::getValueAsBool('timeline_left_border_enabled'); + // Site title & dropbox key if logged in as admin. $this->title = Configs::getValueAsString('site_title'); $this->dropbox_api_key = Auth::user()?->may_administrate === true ? Configs::getValueAsString('dropbox_key') : 'disabled'; diff --git a/app/Http/Resources/GalleryConfigs/RootConfig.php b/app/Http/Resources/GalleryConfigs/RootConfig.php index 7036e0a05a8..832c4870b78 100644 --- a/app/Http/Resources/GalleryConfigs/RootConfig.php +++ b/app/Http/Resources/GalleryConfigs/RootConfig.php @@ -5,6 +5,7 @@ use App\Contracts\Models\AbstractAlbum; use App\Enum\AspectRatioCSSType; use App\Enum\AspectRatioType; +use App\Enum\TimelineAlbumGranularity; use App\Factories\AlbumFactory; use App\Models\Configs; use App\Models\Photo; @@ -20,6 +21,8 @@ class RootConfig extends Data { public bool $is_map_accessible = false; public bool $is_mod_frame_enabled = false; + public bool $is_photo_timeline_enabled = false; + public bool $is_album_timeline_enabled = false; public bool $is_search_accessible = false; public bool $show_keybinding_help_button = false; #[LiteralTypeScriptType('App.Enum.AspectRatioType')] @@ -30,15 +33,29 @@ class RootConfig extends Data public bool $back_button_enabled; public string $back_button_text; public string $back_button_url; + public TimelineAlbumGranularity $timeline_album_granularity; public function __construct() { + $is_logged_in = Auth::check(); $count_locations = Photo::whereNotNull('latitude')->whereNotNull('longitude')->count() > 0; $map_display = Configs::getValueAsBool('map_display'); - $public_display = Auth::check() || Configs::getValueAsBool('map_display_public'); + $public_display = $is_logged_in || Configs::getValueAsBool('map_display_public'); + $this->is_map_accessible = $count_locations && $map_display && $public_display; $this->is_mod_frame_enabled = $this->checkModFrameEnabled(); - $this->is_search_accessible = Auth::check() || Configs::getValueAsBool('search_public'); + + $timeline_photos_enabled = Configs::getValueAsBool('timeline_photos_enabled'); + $timeline_photos_public = Configs::getValueAsBool('timeline_photos_public'); + $this->is_photo_timeline_enabled = $timeline_photos_enabled && ($is_logged_in || $timeline_photos_public); + + $timeline_albums_enabled = Configs::getValueAsBool('timeline_albums_enabled'); + $timeline_albums_public = Configs::getValueAsBool('timeline_albums_public'); + $this->is_album_timeline_enabled = $timeline_albums_enabled && ($is_logged_in || $timeline_albums_public); + $this->timeline_album_granularity = Configs::getValueAsEnum('timeline_albums_granularity', TimelineAlbumGranularity::class); + + $this->is_search_accessible = $is_logged_in || Configs::getValueAsBool('search_public'); + $this->album_thumb_css_aspect_ratio = Configs::getValueAsEnum('default_album_thumb_aspect_ratio', AspectRatioType::class)->css(); $this->show_keybinding_help_button = Configs::getValueAsBool('show_keybinding_help_button'); $this->login_button_position = Configs::getValueAsString('login_button_position'); diff --git a/app/Http/Resources/Models/AlbumResource.php b/app/Http/Resources/Models/AlbumResource.php index 6f74665edb2..ec6217ea6d9 100644 --- a/app/Http/Resources/Models/AlbumResource.php +++ b/app/Http/Resources/Models/AlbumResource.php @@ -2,13 +2,17 @@ namespace App\Http\Resources\Models; +use App\Enum\ColumnSortingType; use App\Http\Resources\Editable\EditableBaseAlbumResource; use App\Http\Resources\Models\Utils\AlbumProtectionPolicy; use App\Http\Resources\Models\Utils\PreFormattedAlbumData; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\AlbumRightsResource; use App\Http\Resources\Traits\HasHeaderUrl; use App\Http\Resources\Traits\HasPrepPhotoCollection; +use App\Http\Resources\Traits\HasTimelineData; use App\Models\Album; +use App\Models\Configs; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Spatie\LaravelData\Data; @@ -20,6 +24,7 @@ class AlbumResource extends Data { use HasPrepPhotoCollection; use HasHeaderUrl; + use HasTimelineData; public string $id; public string $title; @@ -71,7 +76,21 @@ public function __construct(Album $album) $this->has_albums = !$album->isLeaf(); $this->albums = $album->relationLoaded('children') ? ThumbAlbumResource::collect($album->children) : null; $this->photos = $album->relationLoaded('photos') ? PhotoResource::collect($album->photos) : null; - $this->prepPhotosCollection(); + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = $this->getPhotoTimeline($album->photo_timeline); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } + + if ($this->albums->count() > 0) { + // setup timeline data + $sorting = $album->album_sorting?->column ?? Configs::getValueAsEnum('sorting_albums_col', ColumnSortingType::class); + $album_granularity = $this->getAlbumTimeline($album->album_timeline); + $this->albums = TimelineData::setTimeLineDataForAlbums($this->albums, $sorting, $album_granularity); + } // thumb $this->cover_id = $album->cover_id; diff --git a/app/Http/Resources/Models/PhotoResource.php b/app/Http/Resources/Models/PhotoResource.php index 086656b9780..e166db5c669 100644 --- a/app/Http/Resources/Models/PhotoResource.php +++ b/app/Http/Resources/Models/PhotoResource.php @@ -5,9 +5,11 @@ use App\Enum\LicenseType; use App\Http\Resources\Models\Utils\PreComputedPhotoData; use App\Http\Resources\Models\Utils\PreformattedPhotoData; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\PhotoRightsResource; use App\Models\Configs; use App\Models\Photo; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Spatie\LaravelData\Data; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -50,6 +52,9 @@ class PhotoResource extends Data public ?string $previous_photo_id; public PreformattedPhotoData $preformatted; public PreComputedPhotoData $precomputed; + public ?TimelineData $timeline = null; + + private Carbon $timeline_data_carbon; public function __construct(Photo $photo) { @@ -87,6 +92,8 @@ public function __construct(Photo $photo) $this->previous_photo_id = null; $this->preformatted = new PreformattedPhotoData($photo, $this->size_variants->original); $this->precomputed = new PreComputedPhotoData($photo); + + $this->timeline_data_carbon = $photo->taken_at ?? $photo->created_at; } public static function fromModel(Photo $photo): PhotoResource @@ -99,4 +106,14 @@ private function setLocation(Photo $photo): void $showLocation = Configs::getValueAsBool('location_show') && (Auth::check() || Configs::getValueAsBool('location_show_public')); $this->location = $showLocation ? $photo->location : null; } + + /** + * Accessors to the Carbon instances. + * + * @return Carbon + */ + public function timeline_date_carbon(): Carbon + { + return $this->timeline_data_carbon; + } } diff --git a/app/Http/Resources/Models/SmartAlbumResource.php b/app/Http/Resources/Models/SmartAlbumResource.php index e6fd43e7760..2496738434c 100644 --- a/app/Http/Resources/Models/SmartAlbumResource.php +++ b/app/Http/Resources/Models/SmartAlbumResource.php @@ -2,11 +2,14 @@ namespace App\Http\Resources\Models; +use App\Enum\TimelinePhotoGranularity; use App\Http\Resources\Models\Utils\AlbumProtectionPolicy; use App\Http\Resources\Models\Utils\PreFormattedAlbumData; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\AlbumRightsResource; use App\Http\Resources\Traits\HasHeaderUrl; use App\Http\Resources\Traits\HasPrepPhotoCollection; +use App\Models\Configs; use App\SmartAlbums\BaseSmartAlbum; use Illuminate\Support\Collection; use Spatie\LaravelData\Data; @@ -36,6 +39,14 @@ public function __construct(BaseSmartAlbum $smartAlbum) /** @phpstan-ignore-next-line */ $this->photos = $smartAlbum->relationLoaded('photos') ? PhotoResource::collect($smartAlbum->getPhotos()) : null; $this->prepPhotosCollection(); + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = Configs::getValueAsEnum('timeline_photo_granularity', TimelinePhotoGranularity::class); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } $this->thumb = ThumbResource::fromModel($smartAlbum->thumb); $this->policy = AlbumProtectionPolicy::ofSmartAlbum($smartAlbum); diff --git a/app/Http/Resources/Models/TagAlbumResource.php b/app/Http/Resources/Models/TagAlbumResource.php index 451582b9d16..cfe001fd194 100644 --- a/app/Http/Resources/Models/TagAlbumResource.php +++ b/app/Http/Resources/Models/TagAlbumResource.php @@ -5,9 +5,11 @@ use App\Http\Resources\Editable\EditableBaseAlbumResource; use App\Http\Resources\Models\Utils\AlbumProtectionPolicy; use App\Http\Resources\Models\Utils\PreFormattedAlbumData; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\AlbumRightsResource; use App\Http\Resources\Traits\HasHeaderUrl; use App\Http\Resources\Traits\HasPrepPhotoCollection; +use App\Http\Resources\Traits\HasTimelineData; use App\Models\TagAlbum; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; @@ -20,6 +22,7 @@ class TagAlbumResource extends Data { use HasPrepPhotoCollection; use HasHeaderUrl; + use HasTimelineData; public string $id; public string $title; @@ -55,7 +58,14 @@ public function __construct(TagAlbum $tagAlbum) // children $this->photos = $tagAlbum->relationLoaded('photos') ? PhotoResource::collect($tagAlbum->photos) : null; - $this->prepPhotosCollection(); + if ($this->photos !== null) { + // Prep collection with first and last link + which id is next. + $this->prepPhotosCollection(); + + // setup timeline data + $photo_granularity = $this->getPhotoTimeline($tagAlbum->photo_timeline); + $this->photos = TimelineData::setTimeLineDataForPhotos($this->photos, $photo_granularity); + } // thumb $this->thumb = ThumbResource::fromModel($tagAlbum->thumb); diff --git a/app/Http/Resources/Models/ThumbAlbumResource.php b/app/Http/Resources/Models/ThumbAlbumResource.php index b95eb7a0433..7992852f209 100644 --- a/app/Http/Resources/Models/ThumbAlbumResource.php +++ b/app/Http/Resources/Models/ThumbAlbumResource.php @@ -5,12 +5,14 @@ use App\Contracts\Models\AbstractAlbum; use App\Enum\DateOrderingType; use App\Http\Resources\Models\Utils\AlbumProtectionPolicy; +use App\Http\Resources\Models\Utils\TimelineData; use App\Http\Resources\Rights\AlbumRightsResource; use App\Models\Album; use App\Models\Configs; use App\Models\Extensions\BaseAlbum; use App\Models\TagAlbum; use App\SmartAlbums\BaseSmartAlbum; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Spatie\LaravelData\Data; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -39,7 +41,12 @@ class ThumbAlbumResource extends Data public ?string $formatted_min_max = null; public ?string $owner = null; + private Carbon $created_at_carbon; + private ?Carbon $min_taken_at_carbon = null; + private ?Carbon $max_taken_at_carbon = null; + public AlbumRightsResource $rights; + public ?TimelineData $timeline = null; public function __construct(AbstractAlbum $data) { @@ -53,12 +60,15 @@ public function __construct(AbstractAlbum $data) $policy = AlbumProtectionPolicy::ofSmartAlbum($data); } else { /** @var BaseAlbum $data */ - $this->max_taken_at = $data->max_taken_at?->format($date_format); - $this->min_taken_at = $data->min_taken_at?->format($date_format); + $this->min_taken_at_carbon = $data->min_taken_at; + $this->max_taken_at_carbon = $data->max_taken_at; + $this->max_taken_at = $this->max_taken_at_carbon?->format($date_format); + $this->min_taken_at = $this->min_taken_at_carbon?->format($date_format); $this->formatMinMaxDate(); - $this->created_at = $data->created_at->format($date_format); + $this->created_at_carbon = $data->created_at; + $this->created_at = $this->created_at_carbon->format($date_format); $policy = AlbumProtectionPolicy::ofBaseAlbum($data); $this->description = Str::limit($data->description, 100); $this->owner = $data->owner->username; @@ -103,4 +113,24 @@ private function formatMinMaxDate(): void $this->formatted_min_max = $this->min_taken_at . ' - ' . $this->max_taken_at; } } + + /** + * Accessors to the Carbon instances. + * + * @return Carbon + */ + public function created_at_carbon(): Carbon + { + return $this->created_at_carbon; + } + + public function min_taken_at_carbon(): ?Carbon + { + return $this->min_taken_at_carbon; + } + + public function max_taken_at_carbon(): ?Carbon + { + return $this->max_taken_at_carbon; + } } diff --git a/app/Http/Resources/Models/Utils/TimelineData.php b/app/Http/Resources/Models/Utils/TimelineData.php new file mode 100644 index 00000000000..066dddc9c8b --- /dev/null +++ b/app/Http/Resources/Models/Utils/TimelineData.php @@ -0,0 +1,114 @@ + $photo->timeline_date_carbon()->format($timeline_date_format_year), + TimelinePhotoGranularity::MONTH => $photo->timeline_date_carbon()->format($timeline_date_format_month), + TimelinePhotoGranularity::DAY => $photo->timeline_date_carbon()->format($timeline_date_format_day), + TimelinePhotoGranularity::HOUR => $photo->timeline_date_carbon()->format($timeline_photo_date_format_hour), + TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED => throw new LycheeLogicException('DEFAULT is not a valid granularity for photos'), + }; + + $timeDate = match ($granularity) { + TimelinePhotoGranularity::YEAR => $photo->timeline_date_carbon()->format('Y'), + TimelinePhotoGranularity::MONTH => $photo->timeline_date_carbon()->format('Y-m'), + TimelinePhotoGranularity::DAY => $photo->timeline_date_carbon()->format('Y-m-d'), + TimelinePhotoGranularity::HOUR => $photo->timeline_date_carbon()->format('Y-m-d H'), + TimelinePhotoGranularity::DEFAULT, TimelinePhotoGranularity::DISABLED => throw new LycheeLogicException('DEFAULT is not a valid granularity for photos'), + }; + + return new TimelineData(timeDate: $timeDate, format: $format); + } + + private static function fromAlbum(ThumbAlbumResource $album, ColumnSortingType $columnSorting, TimelineAlbumGranularity $granularity): ?self + { + $timeline_date_format_year = Configs::getValueAsString('timeline_album_date_format_year'); + $timeline_date_format_month = Configs::getValueAsString('timeline_album_date_format_month'); + $timeline_date_format_day = Configs::getValueAsString('timeline_album_date_format_day'); + $date = match ($columnSorting) { + ColumnSortingType::CREATED_AT => $album->created_at_carbon(), + ColumnSortingType::MAX_TAKEN_AT => $album->max_taken_at_carbon(), + ColumnSortingType::MIN_TAKEN_AT => $album->min_taken_at_carbon(), + default => null, + }; + + if ($date === null) { + return null; + } + + $format = match ($granularity) { + TimelineAlbumGranularity::YEAR => $date->format($timeline_date_format_year), + TimelineAlbumGranularity::MONTH => $date->format($timeline_date_format_month), + TimelineAlbumGranularity::DAY => $date->format($timeline_date_format_day), + TimelineAlbumGranularity::DEFAULT, TimelineAlbumGranularity::DISABLED => throw new LycheeLogicException('DEFAULT/DISABLED is not a valid granularity for albums'), + }; + + $timeDate = match ($granularity) { + TimelineAlbumGranularity::YEAR => $date->format('Y'), + TimelineAlbumGranularity::MONTH => $date->format('Y-m'), + TimelineAlbumGranularity::DAY => $date->format('Y-m-d'), + TimelineAlbumGranularity::DEFAULT, TimelineAlbumGranularity::DISABLED => throw new LycheeLogicException('DEFAULT/DISABLED is not a valid granularity for albums'), + }; + + return new TimelineData(timeDate: $timeDate, format: $format); + } + + /** + * @param Collection $albums + * @param ColumnSortingType $columnSorting + * @param TimelineAlbumGranularity $granularity + * + * @return Collection + */ + public static function setTimeLineDataForAlbums(Collection $albums, ColumnSortingType $columnSorting, TimelineAlbumGranularity $granularity): Collection + { + return $albums->map(function (ThumbAlbumResource $album) use ($columnSorting, $granularity) { + $album->timeline = TimelineData::fromAlbum($album, $columnSorting, $granularity); + + return $album; + }); + } + + /** + * @param Collection $photos + * @param TimelinePhotoGranularity $granularity + * + * @return Collection + */ + public static function setTimeLineDataForPhotos(Collection $photos, TimelinePhotoGranularity $granularity): Collection + { + return $photos->map(function (PhotoResource $photo) use ($granularity) { + $photo->timeline = TimelineData::fromPhoto($photo, $granularity); + + return $photo; + }); + } +} \ No newline at end of file diff --git a/app/Http/Resources/Search/InitResource.php b/app/Http/Resources/Search/InitResource.php index 685165efee9..767d76e088e 100644 --- a/app/Http/Resources/Search/InitResource.php +++ b/app/Http/Resources/Search/InitResource.php @@ -2,6 +2,7 @@ namespace App\Http\Resources\Search; +use App\Enum\PhotoLayoutType; use App\Models\Configs; use Spatie\LaravelData\Data; use Spatie\TypeScriptTransformer\Attributes\TypeScript; @@ -13,9 +14,11 @@ class InitResource extends Data { public int $search_minimum_length = 3; + public PhotoLayoutType $photo_layout; public function __construct() { $this->search_minimum_length = Configs::getValueAsInt('search_minimum_length_required'); + $this->photo_layout = Configs::getValueAsEnum('search_photos_layout', PhotoLayoutType::class); } } \ No newline at end of file diff --git a/app/Http/Resources/Traits/HasPrepPhotoCollection.php b/app/Http/Resources/Traits/HasPrepPhotoCollection.php index 376aab4c7f6..c28ee05bbe3 100644 --- a/app/Http/Resources/Traits/HasPrepPhotoCollection.php +++ b/app/Http/Resources/Traits/HasPrepPhotoCollection.php @@ -13,10 +13,6 @@ trait HasPrepPhotoCollection { private function prepPhotosCollection(): void { - if ($this->photos === null) { - return; - } - $previous_photo = null; $this->photos->each(function (PhotoResource &$photo) use (&$previous_photo) { if ($previous_photo !== null) { diff --git a/app/Http/Resources/Traits/HasTimelineData.php b/app/Http/Resources/Traits/HasTimelineData.php new file mode 100644 index 00000000000..2d8177105b7 --- /dev/null +++ b/app/Http/Resources/Traits/HasTimelineData.php @@ -0,0 +1,32 @@ + $children - * @property int $num_children The number of children. - * @property Collection $all_photos - * @property int $num_photos The number of photos in this album (excluding photos in subalbums). - * @property LicenseType $license - * @property string|null $cover_id - * @property Photo|null $cover - * @property string|null $header_id - * @property Photo|null $header - * @property string|null $track_short_path - * @property string|null $track_url - * @property AspectRatioType|null $album_thumb_aspect_ratio - * @property int $_lft - * @property int $_rgt - * @property BaseAlbumImpl $base_class - * @property User|null $owner + * @property string $id + * @property string|null $parent_id + * @property Album|null $parent + * @property Collection $children + * @property int $num_children The number of children. + * @property Collection $all_photos + * @property int $num_photos The number of photos in this album (excluding photos in subalbums). + * @property LicenseType $license + * @property string|null $cover_id + * @property Photo|null $cover + * @property string|null $header_id + * @property Photo|null $header + * @property string|null $track_short_path + * @property string|null $track_url + * @property AspectRatioType|null $album_thumb_aspect_ratio + * @property TimelineAlbumGranularity $album_timeline + * @property int $_lft + * @property int $_rgt + * @property BaseAlbumImpl $base_class + * @property User|null $owner * * @method static AlbumBuilder|Album query() Begin querying the model. * @method static AlbumBuilder|Album with(array|string $relations) Begin querying the model with eager loading. @@ -148,6 +150,7 @@ class Album extends BaseAlbum implements Node protected $attributes = [ 'id' => null, 'parent_id' => null, + 'album_timeline' => null, 'license' => 'none', 'cover_id' => null, 'header_id' => null, @@ -167,6 +170,7 @@ class Album extends BaseAlbum implements Node 'num_children' => 'integer', 'num_photos' => 'integer', 'album_thumb_aspect_ratio' => AspectRatioType::class, + 'album_timeline' => TimelineAlbumGranularity::class, '_lft' => 'integer', '_rgt' => 'integer', ]; @@ -385,6 +389,28 @@ protected function setAlbumThumbAspectRatioAttribute(?AspectRatioType $aspectRat $this->attributes['album_thumb_aspect_ratio'] = $aspectRatio?->value; } + /** + * Defines accessor for the Album Timeline. + * + * @return TimelineAlbumGranularity|null + */ + protected function getAlbumTimelineAttribute(): ?TimelineAlbumGranularity + { + return TimelineAlbumGranularity::tryFrom($this->attributes['album_timeline']); + } + + /** + * Defines setter for Album Timeline. + * + * @param TimelineAlbumGranularity|null $album_timeline + * + * @return void + */ + protected function setAlbumTimelineAttribute(?TimelineAlbumGranularity $album_timeline): void + { + $this->attributes['album_timeline'] = $album_timeline?->value; + } + /** * Accessor for the "virtual" attribute {@link Album::$track_url}. * diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php index e81954b43e7..f12714f1703 100644 --- a/app/Models/BaseAlbumImpl.php +++ b/app/Models/BaseAlbumImpl.php @@ -9,6 +9,7 @@ use App\Enum\ColumnSortingType; use App\Enum\OrderSortingType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelinePhotoGranularity; use App\Models\Builders\BaseAlbumImplBuilder; use App\Models\Extensions\HasAttributesPatch; use App\Models\Extensions\HasBidirectionalRelationships; @@ -96,6 +97,7 @@ * @property string $title * @property string|null $description * @property PhotoLayoutType|null $photo_layout + * @property TimelinePhotoGranularity $photo_timeline * @property int $owner_id * @property User $owner * @property bool $is_nsfw @@ -284,4 +286,48 @@ protected function setPhotoSortingAttribute(?PhotoSortingCriterion $sorting): vo $this->attributes['sorting_col'] = $sorting?->column->value; $this->attributes['sorting_order'] = $sorting?->order->value; } + + /** + * Defines accessor for the Aspect Ratio. + * + * @return PhotoLayoutType|null + */ + protected function getPhotoLayoutAttribute(): ?PhotoLayoutType + { + return PhotoLayoutType::tryFrom($this->attributes['photo_layout']); + } + + /** + * Defines setter for Aspect Ratio. + * + * @param PhotoLayoutType|null $aspectRatio + * + * @return void + */ + protected function setPhotoLayoutAttribute(?PhotoLayoutType $aspectRatio): void + { + $this->attributes['photo_layout'] = $aspectRatio?->value; + } + + /** + * Defines accessor for the Photo Timeline. + * + * @return TimelinePhotoGranularity|null + */ + protected function getPhotoTimelineAttribute(): ?TimelinePhotoGranularity + { + return TimelinePhotoGranularity::tryFrom($this->attributes['photo_timeline']); + } + + /** + * Defines setter for Photo Timeline. + * + * @param TimelinePhotoGranularity|null $photo_timeline + * + * @return void + */ + protected function setPhotoTimelineAttribute(?TimelinePhotoGranularity $photo_timeline): void + { + $this->attributes['photo_timeline'] = $photo_timeline?->value; + } } diff --git a/app/Models/Extensions/BaseAlbum.php b/app/Models/Extensions/BaseAlbum.php index fda81129de8..c2d7bbe3d9c 100644 --- a/app/Models/Extensions/BaseAlbum.php +++ b/app/Models/Extensions/BaseAlbum.php @@ -7,6 +7,7 @@ use App\Contracts\Models\HasRandomID; use App\DTO\PhotoSortingCriterion; use App\Enum\PhotoLayoutType; +use App\Enum\TimelinePhotoGranularity; use App\Models\AccessPermission; use App\Models\BaseAlbumImpl; use App\Models\User; @@ -32,6 +33,7 @@ * @property bool $is_nsfw * @property string|null $copyright * @property PhotoLayoutType|null $photo_layout + * @property TimelinePhotoGranularity $photo_timeline * @property int $owner_id * @property User $owner * @property Collection $access_permissions diff --git a/app/Rules/EnumRequireSupportRule.php b/app/Rules/EnumRequireSupportRule.php new file mode 100644 index 00000000000..319518ff93c --- /dev/null +++ b/app/Rules/EnumRequireSupportRule.php @@ -0,0 +1,78 @@ + This is usually a container of allowed values for backed enum */ + protected array $expected; + + /** + * Create a new rule instance. + * + * @param class-string $type + * @param array $expected + * @param VerifyInterface $verify + * + * @return void + */ + public function __construct(mixed $type, array $expected, VerifyInterface $verify) + { + $this->type = $type; + $this->verify = $verify; + $this->expected = $expected; + } + + /** + * {@inheritDoc} + */ + public function validate(string $attribute, mixed $value, \Closure $fail): void + { + if ($value === null || !enum_exists($this->type) || !method_exists($this->type, 'tryFrom')) { + return; + } + + try { + // Enum version + $value = $this->type::tryFrom($value); + + if ($value !== null && $this->isDesirable($value)) { + return; + } + } catch (\TypeError) { + return; + } + + if ($this->verify->is_supporter()) { + return; + } + + $fail('Error: This functionality is only available in the Supporter Edition of Lychee. See here: https://lycheeorg.github.io/get-supporter-edition/'); + } + + /** + * Determine if the given case is a valid case based on the only / except values. + * + * @param mixed $value + * + * @return bool + */ + protected function isDesirable($value) + { + return in_array(needle: $value, haystack: $this->expected, strict: true); + } +} diff --git a/resources/js/components/forms/album/AlbumProperties.vue b/resources/js/components/forms/album/AlbumProperties.vue index eff5d46e6c0..0ee3ba8fb9f 100644 --- a/resources/js/components/forms/album/AlbumProperties.vue +++ b/resources/js/components/forms/album/AlbumProperties.vue @@ -1,5 +1,5 @@