Skip to content

Commit 812fe55

Browse files
committed
add timeline front-end
1 parent 97fcc72 commit 812fe55

File tree

28 files changed

+1282
-57
lines changed

28 files changed

+1282
-57
lines changed

resources/js/components/gallery/albumModule/AlbumThumbPanel.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<Panel v-if="props.isTimeline === false" :header="$t(props.header)" :toggleable="!isAlone" :pt:header:class="headerClass" class="border-0 w-full">
2+
<Panel v-if="isTimeline === false" :header="$t(props.header)" :toggleable="!isAlone" :pt:header:class="headerClass" class="border-0 w-full">
33
<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pt-4">
44
<AlbumThumbPanelList
55
:albums="props.albums"
@@ -101,6 +101,8 @@ const albumsTimeLine = computed<SplitData<App.Http.Resources.Models.ThumbAlbumRe
101101
),
102102
);
103103
104+
const isTimeline = computed(() => props.isTimeline && albumsTimeLine.value.length > 0);
105+
104106
const headerClass = computed(() => {
105107
return props.isAlone ? "hidden" : "";
106108
});

resources/js/components/gallery/albumModule/PhotoThumbPanel.vue

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@
1111
:galleryConfig="props.galleryConfig"
1212
:selectedPhotos="props.selectedPhotos"
1313
:iter="0"
14+
:idx="0"
1415
@clicked="propagateClicked"
1516
@selected="propagateSelected"
1617
@contexted="propagateMenuOpen"
1718
:isTimeline="isTimeline"
1819
/>
1920
<template v-else>
20-
<Timeline v-if="is_timeline_left_border_visible" :value="photosTimeLine" :pt:eventopposite:class="'hidden'" class="mt-4">
21+
<Timeline v-if="isLeftBorderVisible" :value="photosTimeLine" :pt:eventopposite:class="'hidden'" class="mt-4">
2122
<template #content="slotProps">
22-
<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8">
23+
<div
24+
data-type="timelineBlock"
25+
:data-date="slotProps.item.data[0].timeline.timeDate"
26+
class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8"
27+
>
2328
<div class="w-full text-left font-semibold text-muted-color-emphasis text-lg">{{ slotProps.item.header }}</div>
2429
<PhotoThumbPanelList
2530
:photos="slotProps.item.data"
@@ -28,6 +33,7 @@
2833
:galleryConfig="props.galleryConfig"
2934
:selectedPhotos="props.selectedPhotos"
3035
:iter="slotProps.item.iter"
36+
:idx="slotProps.index"
3137
:isTimeline="isTimeline"
3238
@contexted="propagateMenuOpen"
3339
@selected="propagateSelected"
@@ -37,8 +43,12 @@
3743
</template>
3844
</Timeline>
3945
<div v-else>
40-
<template v-for="photoTimeline in photosTimeLine">
41-
<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8">
46+
<template v-for="(photoTimeline, idx) in photosTimeLine">
47+
<div
48+
data-type="timelineBlock"
49+
:data-date="photoTimeline.data[0].timeline?.time_date"
50+
class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8"
51+
>
4252
<div class="w-full text-left font-semibold text-muted-color-emphasis text-lg">{{ photoTimeline.header }}</div>
4353
<PhotoThumbPanelList
4454
:photos="photoTimeline.data"
@@ -47,6 +57,7 @@
4757
:galleryConfig="props.galleryConfig"
4858
:selectedPhotos="props.selectedPhotos"
4959
:iter="photoTimeline.iter"
60+
:idx="idx"
5061
:isTimeline="isTimeline"
5162
@contexted="propagateMenuOpen"
5263
@selected="propagateSelected"
@@ -67,6 +78,7 @@ import { SplitData, useSplitter } from "@/composables/album/splitter";
6778
import Timeline from "primevue/timeline";
6879
import PhotoThumbPanelList from "./PhotoThumbPanelList.vue";
6980
import PhotoThumbPanelControl from "./PhotoThumbPanelControl.vue";
81+
import { isTouchDevice } from "@/utils/keybindings-utils";
7082
7183
const lycheeStore = useLycheeStateStore();
7284
const { is_timeline_left_border_visible } = storeToRefs(lycheeStore);
@@ -87,7 +99,9 @@ const props = defineProps<{
8799
}>();
88100
89101
const layout = ref(props.photoLayout);
90-
const isTimeline = ref(props.isTimeline);
102+
103+
// We do not show the left border on touch devices (mostly phones) due to limited real estate.
104+
const isLeftBorderVisible = computed(() => is_timeline_left_border_visible && !isTouchDevice());
91105
92106
// bubble up.
93107
const emits = defineEmits<{
@@ -117,4 +131,6 @@ const photosTimeLine = computed<SplitData<App.Http.Resources.Models.PhotoResourc
117131
(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.format ?? "Others",
118132
),
119133
);
134+
135+
const isTimeline = computed(() => props.isTimeline && photosTimeLine.value.length > 1);
120136
</script>

resources/js/components/gallery/albumModule/PhotoThumbPanelList.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.iter">
2+
<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.idx">
33
<template v-for="(photo, idx) in props.photos">
44
<PhotoThumb
55
@click="maySelect(idx + iter, $event)"
@@ -30,6 +30,7 @@ const props = defineProps<{
3030
galleryConfig: App.Http.Resources.GalleryConfigs.PhotoLayoutConfig;
3131
selectedPhotos: string[];
3232
iter: number;
33+
idx: number;
3334
}>();
3435
3536
const lycheeStore = useLycheeStateStore();
@@ -57,7 +58,7 @@ const maySelect = (idx: number, e: MouseEvent) => {
5758
const menuOpen = (idx: number, e: MouseEvent) => emits("contexted", idx, e);
5859
5960
// Layouts stuff
60-
const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.iter);
61+
const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.idx);
6162
onMounted(() => activateLayout());
6263
onUpdated(() => activateLayout());
6364
</script>

resources/js/components/gallery/photoModule/Dock.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
v-on:click="emits('toggleStar')"
1919
/>
2020
<DockButton
21+
v-if="!isTimeline"
2122
pi="image"
2223
class="lg:hover:text-primary-500 text-white"
2324
v-tooltip.bottom="$t('gallery.photo.actions.set_album_header')"
@@ -47,6 +48,8 @@ import DockButton from "./DockButton.vue";
4748
import { useLycheeStateStore } from "@/stores/LycheeState";
4849
import { useTogglablesStateStore } from "@/stores/ModalsState";
4950
import { storeToRefs } from "pinia";
51+
import { computed } from "vue";
52+
import { useRoute } from "vue-router";
5053
5154
const lycheeStore = useLycheeStateStore();
5255
const togglableStore = useTogglablesStateStore();
@@ -65,4 +68,7 @@ const emits = defineEmits<{
6568
toggleMove: [];
6669
toggleDelete: [];
6770
}>();
71+
72+
const route = useRoute();
73+
const isTimeline = computed(() => (route.name as string).includes("timeline"));
6874
</script>

resources/js/components/gallery/searchModule/ResultPanel.vue

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
:album="undefined"
2525
:gallery-config="props.layoutConfig"
2626
:selected-photos="selectedPhotosIds"
27+
@clicked="photoClick"
2728
@selected="photoSelect"
2829
@contexted="photoMenuOpen"
2930
:is-timeline="props.isPhotoTimelineEnabled"
@@ -59,11 +60,13 @@ import { AlbumThumbConfig } from "../albumModule/thumbs/AlbumThumb.vue";
5960
import ContextMenu from "primevue/contextmenu";
6061
import Divider from "primevue/divider";
6162
import Paginator from "primevue/paginator";
63+
import { useRouter } from "vue-router";
64+
import { usePhotoRoute } from "@/composables/photo/photoRoute";
6265
6366
const lycheeStore = useLycheeStateStore();
6467
const { are_nsfw_visible } = storeToRefs(lycheeStore);
6568
const togglableStore = useTogglablesStateStore();
66-
const { is_full_screen } = storeToRefs(togglableStore);
69+
const router = useRouter();
6770
6871
const props = defineProps<{
6972
// results
@@ -91,6 +94,12 @@ const emits = defineEmits<{
9194
refresh: [];
9295
}>();
9396
97+
const { photoRoute } = usePhotoRoute(router);
98+
99+
function photoClick(idx: number, e: MouseEvent) {
100+
router.push(photoRoute(photos.value[idx].id));
101+
}
102+
94103
const photos = ref(props.photos);
95104
const children = ref(props.albums);
96105
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<template>
2+
<div
3+
:class="{
4+
'absolute flex flex-col text-muted-color text-right top-0 h-full overflow-y-scroll no-scrollbar': true,
5+
'right-6': !isTouch,
6+
'right-2': isTouch,
7+
'bg-gradient-to-l from-(--p-surface-0) pt-14 dark:from-(--p-surface-900) text-shadow-sm group pb-24': true,
8+
}"
9+
@mouseleave="scrollToView"
10+
>
11+
<div v-for="yearChunk in dates" :key="yearChunk.header" class="">
12+
<span
13+
:class="{
14+
'sticky inline-block top-0 font-semibold z-10 shadow-surface-950 drop-shadow-md text-3xl scale-75 text-muted-color-emphasis': true,
15+
'group-hover:scale-100 transition-all duration-150 origin-right': true,
16+
'scale-100': currentYear === parseInt(yearChunk.header, 10),
17+
}"
18+
>
19+
{{ yearChunk.header }}
20+
</span>
21+
<!-- We only apply the hover property for items bellow the current scrolling year,
22+
this avoids the upper part of the element to scroll up and down and some jerky behaviours. -->
23+
<div :class="{ 'date-wrapper group-hover:grid-rows-[1]': currentYear > parseInt(yearChunk.header, 10) }">
24+
<div class="overflow-hidden flex flex-col">
25+
<span
26+
v-for="monthChunk in yearChunk.data"
27+
:key="monthChunk.time_date"
28+
:data-date-pointer="monthChunk.time_date"
29+
:class="{
30+
'cursor-pointer transition-all duration-150 scale-75 ease-in-out origin-right': true,
31+
'hover:text-primary-emphasis hover:font-bold hover:scale-100': !isTouch,
32+
'scale-110 text-primary-emphasis font-bold': currentDate === monthChunk.time_date,
33+
}"
34+
@click="emits('load', monthChunk.time_date)"
35+
>{{ monthChunk.format }}
36+
</span>
37+
</div>
38+
</div>
39+
</div>
40+
</div>
41+
</template>
42+
<script setup lang="ts">
43+
import { useSplitter } from "@/composables/album/splitter";
44+
import { isTouchDevice } from "@/utils/keybindings-utils";
45+
import { useDebounceFn } from "@vueuse/core";
46+
import { ref } from "vue";
47+
import { onMounted } from "vue";
48+
import { watch } from "vue";
49+
import { computed } from "vue";
50+
import { useRoute } from "vue-router";
51+
52+
const route = useRoute();
53+
const props = defineProps<{
54+
dates: App.Http.Resources.Models.Utils.TimelineData[];
55+
}>();
56+
57+
const { spliter } = useSplitter();
58+
59+
const dates = computed(() => {
60+
return spliter(
61+
props.dates,
62+
(d) => d.time_date.split("-")[0],
63+
(d) => d.time_date.split("-")[0],
64+
);
65+
});
66+
67+
const currentDate = computed(() => (route.params.date as string | undefined) ?? "");
68+
const currentYear = computed(() => parseInt(currentDate.value.split("-")[0], 10));
69+
70+
const isTouch = ref(isTouchDevice());
71+
72+
const emits = defineEmits<{
73+
load: [date: string];
74+
}>();
75+
76+
// Scroll magic side
77+
// Select the current date and center it in the view
78+
const scrollToView = useDebounceFn(() => {
79+
if (!currentDate.value) {
80+
return;
81+
}
82+
83+
const el = document.querySelector(`[data-date-pointer="${currentDate.value}"]`);
84+
if (el) {
85+
el.scrollIntoView({ behavior: "smooth", block: "center" });
86+
}
87+
}, 100);
88+
89+
// If the date change, we update!
90+
watch(() => route.params.date, scrollToView, { immediate: true });
91+
92+
// Also do that at when loading the component
93+
onMounted(() =>
94+
// But wait! if we do it too early the dom is not rendered and this does not work.
95+
// Let's wait 500ms for the rendering, then select the element.
96+
setTimeout(scrollToView, 500),
97+
);
98+
</script>
99+
<style>
100+
.date-wrapper {
101+
display: grid;
102+
grid-template-rows: 0fr;
103+
transition: grid-template-rows 0.25s ease-out;
104+
105+
&:is(:where(.group):hover *) {
106+
grid-template-rows: 1fr;
107+
}
108+
}
109+
</style>

resources/js/components/headers/AlbumHeader.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<template #end>
1414
<router-link
1515
:to="{ name: 'favourites' }"
16-
v-if="(favourites.photos?.length ?? 0) > 0"
16+
v-if="is_favourite_enabled && (favourites.photos?.length ?? 0) > 0"
1717
class="hidden sm:block"
1818
v-tooltip.bottom="'Favourites'"
1919
>
@@ -80,7 +80,7 @@ const lycheeStore = useLycheeStateStore();
8080
lycheeStore.init();
8181
const favourites = useFavouriteStore();
8282
83-
const { dropbox_api_key } = storeToRefs(lycheeStore);
83+
const { dropbox_api_key, is_favourite_enabled } = storeToRefs(lycheeStore);
8484
const { is_album_edit_open } = storeToRefs(togglableStore);
8585
8686
const { toggleCreateAlbum, is_import_from_link_open, toggleImportFromLink, is_import_from_dropbox_open, toggleImportFromDropbox, toggleUpload } =

resources/js/components/headers/AlbumsHeader.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const lycheeStore = useLycheeStateStore();
124124
const togglableStore = useTogglablesStateStore();
125125
const favourites = useFavouriteStore();
126126
127-
const { dropbox_api_key } = storeToRefs(lycheeStore);
127+
const { dropbox_api_key, is_favourite_enabled } = storeToRefs(lycheeStore);
128128
const { is_login_open, is_upload_visible, is_create_album_visible, is_create_tag_album_visible } = storeToRefs(togglableStore);
129129
130130
const router = useRouter();
@@ -222,7 +222,13 @@ const menu = computed(() =>
222222
to: { name: "favourites" },
223223
type: "link",
224224
icon: "pi pi-heart",
225-
if: (favourites.photos?.length ?? 0) > 0,
225+
if: is_favourite_enabled.value && (favourites.photos?.length ?? 0) > 0,
226+
},
227+
{
228+
to: { name: "timeline" },
229+
type: "link",
230+
icon: "pi pi-clock",
231+
if: lycheeStore.is_favourite_enabled,
226232
},
227233
{
228234
icon: "pi pi-search",

0 commit comments

Comments
 (0)