diff --git a/app/Enum/TimelineAlbumGranularity.php b/app/Enum/TimelineAlbumGranularity.php new file mode 100644 index 00000000000..a7fdca7241a --- /dev/null +++ b/app/Enum/TimelineAlbumGranularity.php @@ -0,0 +1,13 @@ +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_album_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/GalleryConfigs/AlbumConfig.php b/app/Http/Resources/GalleryConfigs/AlbumConfig.php index ceb7e6b0d22..d6f293a8537 100644 --- a/app/Http/Resources/GalleryConfigs/AlbumConfig.php +++ b/app/Http/Resources/GalleryConfigs/AlbumConfig.php @@ -6,6 +6,8 @@ use App\Enum\AspectRatioCSSType; use App\Enum\AspectRatioType; use App\Enum\PhotoLayoutType; +use App\Enum\TimelineAlbumGranularity; +use App\Enum\TimelinePhotoGranularity; use App\Models\Album; use App\Models\Configs; use App\Models\Extensions\BaseAlbum; @@ -28,6 +30,8 @@ class AlbumConfig extends Data public bool $is_nsfw_warning_visible; public AspectRatioCSSType $album_thumb_css_aspect_ratio; public PhotoLayoutType $photo_layout; + public TimelineAlbumGranularity $timeline_album_granularity; + public TimelinePhotoGranularity $timeline_photo_granularity; public function __construct(AbstractAlbum $album) { @@ -42,6 +46,8 @@ public function __construct(AbstractAlbum $album) $album instanceof BaseAlbum && $album->is_nsfw && (Auth::check() ? Configs::getValueAsBool('nsfw_warning_admin') : Configs::getValueAsBool('nsfw_warning')); + $this->timeline_album_granularity = Configs::getValueAsEnum('timeline_album_granularity', TimelineAlbumGranularity::class); + $this->timeline_photo_granularity = Configs::getValueAsEnum('timeline_photo_granularity', TimelinePhotoGranularity::class); $this->setIsMapAccessible(); $this->setIsSearchAccessible($this->is_base_album); diff --git a/app/Http/Resources/GalleryConfigs/InitConfig.php b/app/Http/Resources/GalleryConfigs/InitConfig.php index 164fbab4290..f7dc4468919 100644 --- a/app/Http/Resources/GalleryConfigs/InitConfig.php +++ b/app/Http/Resources/GalleryConfigs/InitConfig.php @@ -17,25 +17,42 @@ #[TypeScript()] class InitConfig extends Data { + // ! This will make the error on http requests or front-end being displayed very visibly. public bool $is_debug_enabled; + + // NSFW settings public bool $are_nsfw_visible; - public bool $is_nsfw_warning_visible; public bool $is_nsfw_background_blurred; public string $nsfw_banner_override; public bool $is_nsfw_banner_backdrop_blurred; + + // Keybinding help popup public bool $show_keybinding_help_popup; + + // Image overlay settings public ImageOverlayType $image_overlay_type; + public bool $can_rotate; + public bool $can_autoplay; + + // Thumbs configuration public ThumbOverlayVisibilityType $display_thumb_album_overlay; public ThumbOverlayVisibilityType $display_thumb_photo_overlay; - public ?string $clockwork_url; public ThumbAlbumSubtitleType $album_subtitle_type; - public bool $can_rotate; - public bool $can_autoplay; public AlbumDecorationType $album_decoration; public AlbumDecorationOrientation $album_decoration_orientation; + + // Clockwork + public ?string $clockwork_url; + + // 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; - public int $slideshow_timeout; // Lychee SE is available. public bool $is_se_enabled; @@ -47,48 +64,82 @@ class InitConfig extends Data public function __construct() { + // Debug mode $this->is_debug_enabled = config('app.debug'); + + // NSFW settings $this->are_nsfw_visible = Configs::getValueAsBool('nsfw_visible'); - $this->is_nsfw_background_blurred = Configs::getValueAsBool('nsfw_blur'); - $this->nsfw_banner_override = Configs::getValueAsString('nsfw_banner_override'); - $this->is_nsfw_banner_backdrop_blurred = Configs::getValueAsBool('nsfw_banner_blur_backdrop'); - $this->image_overlay_type = Configs::getValueAsEnum('image_overlay_type', ImageOverlayType::class); - $this->display_thumb_album_overlay = Configs::getValueAsEnum('display_thumb_album_overlay', ThumbOverlayVisibilityType::class); - $this->display_thumb_photo_overlay = Configs::getValueAsEnum('display_thumb_photo_overlay', ThumbOverlayVisibilityType::class); + $this->is_nsfw_background_blurred = Configs::getValueAsBool('nsfw_blur'); // blur the thumbnails + $this->nsfw_banner_override = Configs::getValueAsString('nsfw_banner_override'); // override the banner text. + $this->is_nsfw_banner_backdrop_blurred = Configs::getValueAsBool('nsfw_banner_blur_backdrop'); // blur the backdrop of the warning banner. + + // keybinding help popup $this->show_keybinding_help_popup = Configs::getValueAsBool('show_keybinding_help_popup'); - $this->clockwork_url = $this->has_clockwork_in_menu(); - $this->album_subtitle_type = Configs::getValueAsEnum('album_subtitle_type', ThumbAlbumSubtitleType::class); + // Image overlay settings + $this->image_overlay_type = Configs::getValueAsEnum('image_overlay_type', ImageOverlayType::class); $this->can_rotate = Configs::getValueAsBool('editor_enabled'); $this->can_autoplay = Configs::getValueAsBool('autoplay_enabled'); - $this->slideshow_timeout = Configs::getValueAsInt('slideshow_timeout'); + // Thumbs configuration + $this->display_thumb_album_overlay = Configs::getValueAsEnum('display_thumb_album_overlay', ThumbOverlayVisibilityType::class); + $this->display_thumb_photo_overlay = Configs::getValueAsEnum('display_thumb_photo_overlay', ThumbOverlayVisibilityType::class); + $this->album_subtitle_type = Configs::getValueAsEnum('album_subtitle_type', ThumbAlbumSubtitleType::class); $this->album_decoration = Configs::getValueAsEnum('album_decoration', AlbumDecorationType::class); $this->album_decoration_orientation = Configs::getValueAsEnum('album_decoration_orientation', AlbumDecorationOrientation::class); - $this->title = Configs::getValueAsString('site_title'); + // Clockwork + $this->has_clockwork_in_menu(); - $verify = resolve(Verify::class); - $is_supporter = $verify->is_supporter(); - $this->is_se_enabled = $verify->validate() && $is_supporter; - $this->is_se_preview_enabled = !$is_supporter && Configs::getValueAsBool('enable_se_preview'); - $this->is_se_info_hidden = $is_supporter || Configs::getValueAsBool('disable_se_call_for_actions'); + // Slideshow settings + $this->slideshow_timeout = Configs::getValueAsInt('slideshow_timeout'); + // Timeline settings + $this->is_timeline_left_border_visible = Configs::getValueAsBool('timeline_left_border_enable'); + + // 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'; + + $this->set_supporter_properties(); } - private function has_clockwork_in_menu(): string|null + /** + * For clockwork we need to check that it is enabled or that we are in debug mode. + * Furthermore we need to check if the web interface is enabled. + * + * @return void + */ + private function has_clockwork_in_menu(): void { // Defining clockwork URL $clockWorkEnabled = config('clockwork.enable') === true || (config('app.debug') === true && config('clockwork.enable') === null); $clockWorkWeb = config('clockwork.web'); - if ($clockWorkEnabled && $clockWorkWeb === true) { - return URL::asset('clockwork/app'); - } - if (is_string($clockWorkWeb)) { - return $clockWorkWeb . '/app'; - } - - return null; + + $this->clockwork_url = match (true) { + $clockWorkEnabled && ($clockWorkWeb === true) => URL::asset('clockwork/app'), + is_string($clockWorkWeb) => $clockWorkWeb . '/app', + default => null, + }; + } + + /** + * We set the properties related to Lychee SE. + * + * @return void + */ + private function set_supporter_properties() + { + $verify = resolve(Verify::class); + $is_supporter = $verify->is_supporter(); + + // We enable Lychee SE if the user is a supporter. + $this->is_se_enabled = $verify->validate() && $is_supporter; + + // We disable preview if we are already a supporter. + $this->is_se_preview_enabled = !$is_supporter && Configs::getValueAsBool('enable_se_preview'); + + // We hide the info if we are already a supporter (or the user requests it). + $this->is_se_info_hidden = $is_supporter || Configs::getValueAsBool('disable_se_call_for_actions'); } } \ No newline at end of file diff --git a/app/Http/Resources/GalleryConfigs/RootConfig.php b/app/Http/Resources/GalleryConfigs/RootConfig.php index 7036e0a05a8..acfffbf5adc 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_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_enabled = Configs::getValueAsBool('timeline_enable'); + $timeline_public = Configs::getValueAsBool('timeline_public'); + $this->is_timeline_enabled = $timeline_enabled && ($is_logged_in || $timeline_public); + + $timeline_albums_enabled = Configs::getValueAsBool('timeline_albums_enable'); + $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_album_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 86a257e8dd6..843ad02d8ca 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\Enum\TimelineAlbumGranularity; 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\Models\Album; +use App\Models\Configs; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Spatie\LaravelData\Data; @@ -74,6 +78,12 @@ public function __construct(Album $album) $this->photos = $album->relationLoaded('photos') ? PhotoResource::collect($album->photos) : null; $this->prepPhotosCollection(); + if ($this->albums->count() > 0) { + $sorting = $album->album_sorting?->column ?? Configs::getValueAsEnum('sorting_albums_col', ColumnSortingType::class); + $album_granularity = Configs::getValueAsEnum('timeline_album_granularity', TimelineAlbumGranularity::class); + $this->albums = TimelineData::setTimeLineDataForAlbums($this->albums, $sorting, $album_granularity); + } + // thumb $this->cover_id = $album->cover_id; $this->thumb = ThumbResource::make($album->thumb?->id, $album->thumb?->type, $album->thumb?->thumbUrl, $album->thumb?->thumb2xUrl); diff --git a/app/Http/Resources/Models/ThumbAlbumResource.php b/app/Http/Resources/Models/ThumbAlbumResource.php index 0c12a1da0a5..d24095d625d 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/PreformattedPhotoData.php b/app/Http/Resources/Models/Utils/PreformattedPhotoData.php index 3899b3a1143..eaf35da55a1 100644 --- a/app/Http/Resources/Models/Utils/PreformattedPhotoData.php +++ b/app/Http/Resources/Models/Utils/PreformattedPhotoData.php @@ -3,6 +3,7 @@ namespace App\Http\Resources\Models\Utils; use App\Enum\LicenseType; +use App\Enum\TimelinePhotoGranularity; use App\Facades\Helpers; use App\Http\Resources\Models\SizeVariantResource; use App\Models\Configs; @@ -30,6 +31,7 @@ class PreformattedPhotoData extends Data public ?string $altitude; public string $license; public string $description; + public TimelineData $timeline; public function __construct(Photo $photo, ?SizeVariantResource $original = null) { @@ -40,6 +42,7 @@ public function __construct(Photo $photo, ?SizeVariantResource $original = null) $this->created_at = $photo->created_at->format($date_format_uploaded); $this->taken_at = $photo->taken_at?->format($date_format_taken_at); $this->date_overlay = ($photo->taken_at ?? $photo->created_at)->format($overlay_date_format) ?? ''; + $this->timeline = TimelineData::fromPhoto($photo, Configs::getValueAsEnum('timeline_photo_granularity', TimelinePhotoGranularity::class)); $this->shutter = str_replace('s', 'sec', $photo->shutter ?? ''); $this->aperture = str_replace('f/', '', $photo->aperture ?? ''); diff --git a/app/Http/Resources/Models/Utils/TimelineData.php b/app/Http/Resources/Models/Utils/TimelineData.php new file mode 100644 index 00000000000..7365736354e --- /dev/null +++ b/app/Http/Resources/Models/Utils/TimelineData.php @@ -0,0 +1,93 @@ + ($photo->taken_at ?? $photo->created_at)->format($timeline_date_format_year), + TimelinePhotoGranularity::MONTH => ($photo->taken_at ?? $photo->created_at)->format($timeline_date_format_month), + TimelinePhotoGranularity::DAY => ($photo->taken_at ?? $photo->created_at)->format($timeline_date_format_day), + TimelinePhotoGranularity::HOUR => ($photo->taken_at ?? $photo->created_at)->format($timeline_photo_date_format_hour), + }; + + $timeDate = match ($granularity) { + TimelinePhotoGranularity::YEAR => ($photo->taken_at ?? $photo->created_at)->format('Y'), + TimelinePhotoGranularity::MONTH => ($photo->taken_at ?? $photo->created_at)->format('Y-m'), + TimelinePhotoGranularity::DAY => ($photo->taken_at ?? $photo->created_at)->format('Y-m-d'), + TimelinePhotoGranularity::HOUR => ($photo->taken_at ?? $photo->created_at)->format('Y-m-d H'), + }; + + 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), + }; + + $timeDate = match ($granularity) { + TimelineAlbumGranularity::YEAR => $date->format('Y'), + TimelineAlbumGranularity::MONTH => $date->format('Y-m'), + TimelineAlbumGranularity::DAY => $date->format('Y-m-d'), + }; + + return new TimelineData(timeDate: $timeDate, format: $format); + } + + /** + * @param Collection $albums + * @param ColumnSortingType $columnSorting + * + * @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; + }); + } +} \ No newline at end of file diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php index e81954b43e7..fbd101dce41 100644 --- a/app/Models/BaseAlbumImpl.php +++ b/app/Models/BaseAlbumImpl.php @@ -284,4 +284,26 @@ 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 AspectRatioType|null $aspectRatio + // * + // * @return void + // */ + // protected function setPhotoLayoutAttribute(?PhotoLayoutType $aspectRatio): void + // { + // $this->attributes['photo_layout'] = $aspectRatio?->value; + // } } diff --git a/database/migrations/2024_10_25_064336_timeline_options.php b/database/migrations/2024_10_25_064336_timeline_options.php new file mode 100644 index 00000000000..54467c85875 --- /dev/null +++ b/database/migrations/2024_10_25_064336_timeline_options.php @@ -0,0 +1,154 @@ + 'timeline_enable', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable Timeline', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_public', + 'value' => '0', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Allow anonymous users to access the timeline', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_albums_enable', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable Timeline for albums', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_albums_public', + 'value' => '0', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Display the albums as timeline for anonymous users', + 'details' => '', + 'is_secret' => false, + 'level' => 0, + ], + [ + 'key' => 'timeline_left_border_enable', + 'value' => '1', + 'cat' => self::TIMELINE, + 'type_range' => self::BOOL, + 'description' => 'Enable the left border line on timelines', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_granularity', + 'value' => 'day', + 'cat' => self::TIMELINE, + 'type_range' => 'year|month|day|hour', + 'description' => 'Timeline granularity for photos', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_granularity', + 'value' => 'year', + 'cat' => self::TIMELINE, + 'type_range' => 'year|month|day', + 'description' => 'Timeline granularity for albums', + 'details' => '', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_year', + 'value' => 'Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at year granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_month', + 'value' => 'M Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at month granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_day', + 'value' => 'Y-M-j', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at day granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_photo_date_format_hour', + 'value' => 'g:i', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at hour granularity for photos', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_year', + 'value' => 'Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at year granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_month', + 'value' => 'M Y', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at month granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + [ + 'key' => 'timeline_album_date_format_day', + 'value' => 'M-j', + 'cat' => self::TIMELINE, + 'type_range' => self::STRING_REQ, + 'description' => 'Format the date at day granularity for albums', + 'details' => 'See datetime.format.php', + 'is_secret' => false, + 'level' => 1, + ], + ]; + } +}; diff --git a/resources/js/components/gallery/AlbumThumbPanel.vue b/resources/js/components/gallery/AlbumThumbPanel.vue index a07e529df88..2d8395a4f60 100644 --- a/resources/js/components/gallery/AlbumThumbPanel.vue +++ b/resources/js/components/gallery/AlbumThumbPanel.vue @@ -8,7 +8,7 @@ :album="album" :cover_id="null" :config="props.config" - v-if="!album.is_nsfw || props.areNsfwVisible" + v-if="!album.is_nsfw || are_nsfw_visible" :is-selected="props.selectedAlbums.includes(album.id)" /> @@ -19,9 +19,13 @@ import Panel from "primevue/panel"; import AlbumThumb, { AlbumThumbConfig } from "@/components/gallery/thumbs/AlbumThumb.vue"; import { computed } from "vue"; +import { useLycheeStateStore } from "@/stores/LycheeState"; +import { storeToRefs } from "pinia"; + +const lycheeStore = useLycheeStateStore(); +const { are_nsfw_visible } = storeToRefs(lycheeStore); const props = defineProps<{ - areNsfwVisible: boolean; header: string; album: App.Http.Resources.Models.AlbumResource | undefined | null; albums: { [key: number]: App.Http.Resources.Models.ThumbAlbumResource }; @@ -36,12 +40,14 @@ const emits = defineEmits<{ clicked: [idx: number, event: MouseEvent]; contexted: [idx: number, event: MouseEvent]; }>(); + const maySelect = (idx: number, e: MouseEvent) => { if (props.idxShift < 0) { return; } emits("clicked", idx, e); }; + const menuOpen = (idx: number, e: MouseEvent) => { if (props.idxShift < 0) { return; diff --git a/resources/js/components/gallery/AlbumThumbTimeline.vue b/resources/js/components/gallery/AlbumThumbTimeline.vue new file mode 100644 index 00000000000..7c354e05e9c --- /dev/null +++ b/resources/js/components/gallery/AlbumThumbTimeline.vue @@ -0,0 +1,99 @@ + + diff --git a/resources/js/composables/album/albumsRefresher.ts b/resources/js/composables/album/albumsRefresher.ts index 0a663f9e4fc..e78fa4f5228 100644 --- a/resources/js/composables/album/albumsRefresher.ts +++ b/resources/js/composables/album/albumsRefresher.ts @@ -2,22 +2,18 @@ import AlbumService from "@/services/album-service"; import { AuthStore } from "@/stores/Auth"; import { LycheeStateStore } from "@/stores/LycheeState"; import { computed, ref, Ref } from "vue"; - -export type SharedAlbums = { - owner: string; - albums: App.Http.Resources.Models.ThumbAlbumResource[]; - iter: number; -}; +import { SplitData, useSplitter } from "./splitter"; export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStore, isLoginOpen: Ref) { + const { spliter } = useSplitter(); const user = ref(undefined) as Ref; const isKeybindingsHelpOpen = ref(false); const smartAlbums = ref([]) as Ref; const albums = ref([]) as Ref; - const sharedAlbums = ref([]) as Ref; + const sharedAlbums = ref([]) as Ref[]>; const rootConfig = ref(undefined) as Ref; const rootRights = ref(undefined) as Ref; - const selectableAlbums = computed(() => albums.value.concat(sharedAlbums.value.map((album) => album.albums).flat())); + const selectableAlbums = computed(() => albums.value.concat(sharedAlbums.value.map((album) => album.data).flat())); function refresh() { auth.getUser().then((data) => { @@ -35,9 +31,12 @@ export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStor smartAlbums.value = (data.data.smart_albums as App.Http.Resources.Models.ThumbAlbumResource[]) ?? []; albums.value = data.data.albums as App.Http.Resources.Models.ThumbAlbumResource[]; smartAlbums.value = smartAlbums.value.concat(data.data.tag_albums as App.Http.Resources.Models.ThumbAlbumResource[]); - sharedAlbums.value = []; - - prepSharedAlbum((data.data.shared_albums as App.Http.Resources.Models.ThumbAlbumResource[]) ?? []); + sharedAlbums.value = spliter( + (data.data.shared_albums as App.Http.Resources.Models.ThumbAlbumResource[]) ?? [], + (d) => d.owner as string, // mapper + (d) => d.owner as string, // formatter + albums.value.length, + ); rootConfig.value = data.data.config; rootRights.value = data.data.rights; @@ -56,23 +55,6 @@ export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStor }); } - function prepSharedAlbum(sharedAlbumsData: App.Http.Resources.Models.ThumbAlbumResource[]) { - // In this specific case, album owner is not null. - const sharedOwners: string[] = [...new Set(sharedAlbumsData.map((album) => album.owner as string))]; - sharedOwners.forEach((owner) => { - const albums = sharedAlbumsData.filter((album) => album.owner === owner); - sharedAlbums.value.push({ owner, albums, iter: 0 }); - }); - - // loop over all the shared albums to prep the indexes. - let idx = 0; - let sum = albums.value.length; - for (idx = 0; idx < sharedAlbums.value.length; idx++) { - sharedAlbums.value[idx].iter = sum; - sum += sharedAlbums.value[idx].albums.length; - } - } - return { user, isKeybindingsHelpOpen, diff --git a/resources/js/composables/album/splitter.ts b/resources/js/composables/album/splitter.ts new file mode 100644 index 00000000000..369418b3481 --- /dev/null +++ b/resources/js/composables/album/splitter.ts @@ -0,0 +1,31 @@ +export type SplitData = { + header: string; + data: T[]; + iter: number; +}; + +export function useSplitter() { + function spliter(data: T[], mapper: (d: T) => string, formatter: (d: T) => string, start: number = 0): SplitData[] { + const ret = [] as SplitData[]; + + const headers: string[] = [...new Set(data.map(mapper))]; + headers.forEach((h) => { + const headerData = data.filter((d) => mapper(d) === h); + ret.push({ header: formatter(headerData[0]), data: headerData, iter: 0 }); + }); + + // loop over all the shared albums to prep the indexes. + let idx = 0; + let sum = start; + for (idx = 0; idx < ret.length; idx++) { + ret[idx].iter = sum; + sum += ret[idx].data.length; + } + + return ret; + } + + return { + spliter, + }; +} diff --git a/resources/js/config/constants.ts b/resources/js/config/constants.ts index 3786525a288..5993cfcbc7e 100644 --- a/resources/js/config/constants.ts +++ b/resources/js/config/constants.ts @@ -125,7 +125,7 @@ export const SelectBuilders = { return licenseOptions.find((option) => option.value === value) || undefined; }, - buildPhotoLayout(value: string | App.Enum.PhotoLayoutType | undefined): SelectOption | undefined { + buildPhotoLayout(value: string | App.Enum.PhotoLayoutType | null): SelectOption | undefined { return photoLayoutOptions.find((option) => option.value === value) || undefined; }, diff --git a/resources/js/lychee.d.ts b/resources/js/lychee.d.ts index 6561bb4628a..ba452713908 100644 --- a/resources/js/lychee.d.ts +++ b/resources/js/lychee.d.ts @@ -81,6 +81,8 @@ declare namespace App.Enum { export type StorageDiskType = "images" | "s3"; export type ThumbAlbumSubtitleType = "description" | "takedate" | "creation" | "oldstyle"; export type ThumbOverlayVisibilityType = "never" | "always" | "hover"; + export type TimelineAlbumGranularity = "year" | "month" | "day"; + export type TimelinePhotoGranularity = "year" | "month" | "day" | "hour"; export type UpdateStatus = 0 | 1 | 2 | 3; export type VersionChannelType = "release" | "git" | "tag"; } @@ -143,7 +145,7 @@ declare namespace App.Http.Resources.Editable { photo_sorting: App.DTO.PhotoSortingCriterion | null; album_sorting: App.DTO.AlbumSortingCriterion | null; aspect_ratio: App.Enum.AspectRatioType | null; - photo_layout: any | null; + photo_layout: App.Enum.PhotoLayoutType | null; header_id: string | null; cover_id: string | null; tags: Array; @@ -181,6 +183,8 @@ declare namespace App.Http.Resources.GalleryConfigs { is_nsfw_warning_visible: boolean; album_thumb_css_aspect_ratio: App.Enum.AspectRatioCSSType; photo_layout: App.Enum.PhotoLayoutType; + timeline_album_granularity: App.Enum.TimelineAlbumGranularity; + timeline_photo_granularity: App.Enum.TimelinePhotoGranularity; }; export type FooterConfig = { footer_additional_text: string; @@ -196,23 +200,23 @@ declare namespace App.Http.Resources.GalleryConfigs { export type InitConfig = { is_debug_enabled: boolean; are_nsfw_visible: boolean; - is_nsfw_warning_visible: boolean; is_nsfw_background_blurred: boolean; nsfw_banner_override: string; is_nsfw_banner_backdrop_blurred: boolean; show_keybinding_help_popup: boolean; image_overlay_type: App.Enum.ImageOverlayType; + can_rotate: boolean; + can_autoplay: boolean; display_thumb_album_overlay: App.Enum.ThumbOverlayVisibilityType; display_thumb_photo_overlay: App.Enum.ThumbOverlayVisibilityType; - clockwork_url: string | null; album_subtitle_type: App.Enum.ThumbAlbumSubtitleType; - can_rotate: boolean; - can_autoplay: boolean; album_decoration: App.Enum.AlbumDecorationType; album_decoration_orientation: App.Enum.AlbumDecorationOrientation; + clockwork_url: string | null; + slideshow_timeout: number; + is_timeline_left_border_visible: boolean; title: string; dropbox_api_key: string; - slideshow_timeout: number; is_se_enabled: boolean; is_se_preview_enabled: boolean; is_se_info_hidden: boolean; @@ -243,6 +247,8 @@ declare namespace App.Http.Resources.GalleryConfigs { export type RootConfig = { is_map_accessible: boolean; is_mod_frame_enabled: boolean; + is_timeline_enabled: boolean; + is_album_timeline_enabled: boolean; is_search_accessible: boolean; show_keybinding_help_button: boolean; album_thumb_css_aspect_ratio: App.Enum.AspectRatioType; @@ -250,6 +256,7 @@ declare namespace App.Http.Resources.GalleryConfigs { back_button_enabled: boolean; back_button_text: string; back_button_url: string; + timeline_album_granularity: App.Enum.TimelineAlbumGranularity; }; export type UploadConfig = { upload_processing_limit: number; @@ -415,6 +422,7 @@ declare namespace App.Http.Resources.Models { formatted_min_max: string | null; owner: string | null; rights: App.Http.Resources.Rights.AlbumRightsResource; + timeline: App.Http.Resources.Models.Utils.TimelineData | null; }; export type ThumbResource = { id: string; @@ -491,6 +499,11 @@ declare namespace App.Http.Resources.Models.Utils { altitude: string | null; license: string; description: string; + timeline: App.Http.Resources.Models.Utils.TimelineData; + }; + export type TimelineData = { + timeDate: string; + format: string; }; export type UserToken = { token: string; diff --git a/resources/js/stores/LycheeState.ts b/resources/js/stores/LycheeState.ts index 5fa561270dd..eaf38c580cc 100644 --- a/resources/js/stores/LycheeState.ts +++ b/resources/js/stores/LycheeState.ts @@ -6,6 +6,11 @@ export type LycheeStateStore = ReturnType; export const useLycheeStateStore = defineStore("lychee-store", { state: () => ({ + // flag to fetch data + is_init: false, + is_loading: false, + + // Debug mode (is default to true to see the first crash) is_debug_enabled: true, // togglables @@ -31,46 +36,42 @@ export const useLycheeStateStore = defineStore("lychee-store", { search_page: 1, // configs for nsfw + are_nsfw_visible: false, is_nsfw_background_blurred: false, is_nsfw_banner_backdrop_blurred: false, nsfw_banner_override: "", + nsfw_consented: [] as string[], + + // Image overlay settings + image_overlay_type: "exif" as App.Enum.ImageOverlayType, + can_rotate: false, + can_autoplay: false, + // keybinding help show_keybinding_help_popup: false, - // Lychee Supporter Edition - is_se_enabled: false, - is_se_preview_enabled: false, - is_se_info_hidden: false, - // album stuff - album_decoration: "LAYERS" as App.Enum.AlbumDecorationType, - album_decoration_orientation: "ROW" as App.Enum.AlbumDecorationOrientation, - album_subtitle_type: "OLDSTYLE" as App.Enum.ThumbAlbumSubtitleType, display_thumb_album_overlay: "always" as App.Enum.ThumbOverlayVisibilityType, display_thumb_photo_overlay: "always" as App.Enum.ThumbOverlayVisibilityType, - - can_rotate: false, - can_autoplay: false, + album_subtitle_type: "OLDSTYLE" as App.Enum.ThumbAlbumSubtitleType, + album_decoration: "LAYERS" as App.Enum.AlbumDecorationType, + album_decoration_orientation: "ROW" as App.Enum.AlbumDecorationOrientation, // menu stuff clockwork_url: "" as null | string, - // togglable with defaults - are_nsfw_visible: false, - image_overlay_type: "exif" as App.Enum.ImageOverlayType, + // Timeline settings + is_timeline_left_border_visible: true, - // Site title + // Site title & Dropbox API key title: "lychee.GALLERY", - - // flag to fetch data - is_init: false, - is_loading: false, - - nsfw_consented: [] as string[], - - // Dropbox API key dropbox_api_key: "disabled", + + // Lychee Supporter Edition + is_se_enabled: false, + is_se_preview_enabled: false, + is_se_info_hidden: false, }), getters: { isSearchActive(): boolean { @@ -95,30 +96,42 @@ export const useLycheeStateStore = defineStore("lychee-store", { InitService.fetchInitData() .then((response) => { + this.is_init = true; + this.is_loading = false; + const data = response.data; + + this.is_debug_enabled = data.is_debug_enabled; + this.are_nsfw_visible = data.are_nsfw_visible; this.is_nsfw_background_blurred = data.is_nsfw_background_blurred; this.nsfw_banner_override = data.nsfw_banner_override; this.is_nsfw_banner_backdrop_blurred = data.is_nsfw_banner_backdrop_blurred; - this.image_overlay_type = data.image_overlay_type; - this.display_thumb_album_overlay = data.display_thumb_album_overlay; - this.display_thumb_photo_overlay = data.display_thumb_photo_overlay; - this.is_init = true; - this.is_loading = false; + this.show_keybinding_help_popup = data.show_keybinding_help_popup; - this.clockwork_url = data.clockwork_url; + + this.image_overlay_type = data.image_overlay_type; this.can_rotate = data.can_rotate; this.can_autoplay = data.can_autoplay; + + this.display_thumb_album_overlay = data.display_thumb_album_overlay; + this.display_thumb_photo_overlay = data.display_thumb_photo_overlay; + this.album_subtitle_type = data.album_subtitle_type; this.album_decoration = data.album_decoration; this.album_decoration_orientation = data.album_decoration_orientation; - this.album_subtitle_type = data.album_subtitle_type; + + this.clockwork_url = data.clockwork_url; + + this.slideshow_timeout = data.slideshow_timeout; + + this.is_timeline_left_border_visible = data.is_timeline_left_border_visible; + this.title = data.title; - this.is_debug_enabled = data.is_debug_enabled; + this.dropbox_api_key = data.dropbox_api_key; + this.is_se_enabled = data.is_se_enabled; this.is_se_preview_enabled = data.is_se_preview_enabled; this.is_se_info_hidden = data.is_se_info_hidden; - this.dropbox_api_key = data.dropbox_api_key; - this.slideshow_timeout = data.slideshow_timeout; }) .catch((error) => { // In this specific case, even though it has been possibly disabled, we really need to see the error. diff --git a/resources/js/style/preset.ts b/resources/js/style/preset.ts index a96c558c804..cef204ca8a9 100644 --- a/resources/js/style/preset.ts +++ b/resources/js/style/preset.ts @@ -474,6 +474,26 @@ const LycheePrimeVueConfig = { }, }, }, + timeline: { + colorScheme: { + dark: { + event: { + marker: { + background: "{surface.800}", + border: { + color: "{surface.700}", + }, + content: { + background: "{primary.600}", + }, + }, + connector: { + color: "{surface.700}", + }, + }, + }, + }, + }, paginator: { colorScheme: { light: { diff --git a/resources/js/views/gallery-panels/Albums.vue b/resources/js/views/gallery-panels/Albums.vue index e5b9f3d8de2..2cb20f63589 100644 --- a/resources/js/views/gallery-panels/Albums.vue +++ b/resources/js/views/gallery-panels/Albums.vue @@ -20,34 +20,46 @@ :user="user" :config="albumPanelConfig" :is-alone="!albums.length" - :are-nsfw-visible="false" :idx-shift="-1" :selected-albums="[]" /> - +