Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
v-if="layoutConfig !== null && photos !== null && photos.length > 0"
header="gallery.album.header_photos"
:photos="photos"
:photos-timeline="photosTimeline"
:album="album"
:gallery-config="layoutConfig"
:photo-layout="config.photo_layout"
Expand Down Expand Up @@ -125,6 +126,7 @@ import AlbumStatistics from "@/components/drawers/AlbumStatistics.vue";
import { useTogglablesStateStore } from "@/stores/ModalsState";
import { usePhotoRoute } from "@/composables/photo/photoRoute";
import { useRouter } from "vue-router";
import { type SplitData } from "@/composables/album/splitter";

const router = useRouter();

Expand All @@ -135,6 +137,8 @@ const props = defineProps<{
| App.Http.Resources.Models.TagAlbumResource
| App.Http.Resources.Models.SmartAlbumResource
| undefined;
photos: App.Http.Resources.Models.PhotoResource[];
photosTimeline: SplitData<App.Http.Resources.Models.PhotoResource>[] | undefined;
config: App.Http.Resources.GalleryConfigs.AlbumConfig | undefined;
user: App.Http.Resources.Models.UserResource | undefined;
layoutConfig: App.Http.Resources.GalleryConfigs.PhotoLayoutConfig;
Expand All @@ -144,7 +148,8 @@ const props = defineProps<{
const modelAlbum = computed(() => props.modelAlbum);
const album = computed(() => props.album);
const hasHidden = computed(() => modelAlbum.value !== undefined && modelAlbum.value.albums.filter((album) => album.is_nsfw).length > 0);
const photos = computed<App.Http.Resources.Models.PhotoResource[]>(() => album.value?.photos ?? []);
const photos = computed<App.Http.Resources.Models.PhotoResource[]>(() => props.photos);
const photosTimeline = computed(() => props.photosTimeline);

const config = ref(props.config);

Expand Down
42 changes: 33 additions & 9 deletions resources/js/components/gallery/albumModule/AlbumThumbPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@
<script setup lang="ts">
import Panel from "primevue/panel";
import { AlbumThumbConfig } from "@/components/gallery/albumModule/thumbs/AlbumThumb.vue";
import { computed } from "vue";
import { SplitData, useSplitter } from "@/composables/album/splitter";
import { computed, onMounted, watch } from "vue";
import { type SplitData, useSplitter } from "@/composables/album/splitter";
import Timeline from "primevue/timeline";
import { useLycheeStateStore } from "@/stores/LycheeState";
import { storeToRefs } from "pinia";
import AlbumThumbPanelList from "./AlbumThumbPanelList.vue";

const lycheeStore = useLycheeStateStore();
const { are_nsfw_visible, is_timeline_left_border_visible } = storeToRefs(lycheeStore);
const { are_nsfw_visible, is_timeline_left_border_visible, is_debug_enabled } = storeToRefs(lycheeStore);

const props = defineProps<{
header: string;
Expand All @@ -83,7 +83,7 @@ const emits = defineEmits<{
contexted: [idx: number, event: MouseEvent];
}>();

const { spliter } = useSplitter();
const { spliter, verifyOrder } = useSplitter();

const propagateClicked = (idx: number, e: MouseEvent) => {
emits("clicked", idx, e);
Expand All @@ -94,16 +94,40 @@ const propagateMenuOpen = (idx: number, e: MouseEvent) => {
};

const albumsTimeLine = computed<SplitData<App.Http.Resources.Models.ThumbAlbumResource>[]>(() =>
spliter(
props.albums as App.Http.Resources.Models.ThumbAlbumResource[],
(a: App.Http.Resources.Models.ThumbAlbumResource) => a.timeline?.time_date ?? "",
(a: App.Http.Resources.Models.ThumbAlbumResource) => a.timeline?.format ?? "Others",
),
split(props.albums as App.Http.Resources.Models.ThumbAlbumResource[]),
);

const isTimeline = computed(() => props.isTimeline && albumsTimeLine.value.length > 1);

const headerClass = computed(() => {
return props.isAlone ? "hidden" : "";
});

function split(albums: App.Http.Resources.Models.ThumbAlbumResource[]) {
return spliter(
albums,
(a: App.Http.Resources.Models.ThumbAlbumResource) => a.timeline?.time_date ?? "",
(a: App.Http.Resources.Models.ThumbAlbumResource) => a.timeline?.format ?? "Others",
);
}

onMounted(() => {
validate(props.albums as App.Http.Resources.Models.ThumbAlbumResource[]);
});

function validate(albums: App.Http.Resources.Models.ThumbAlbumResource[]) {
if (props.isTimeline) {
const splitted = split(albums);
verifyOrder(is_debug_enabled.value, albums, splitted);
}
}

watch(
() => props.album?.id,
() => {
if (props.isTimeline) {
validate(props.albums as App.Http.Resources.Models.ThumbAlbumResource[]);
}
},
);
</script>
34 changes: 17 additions & 17 deletions resources/js/components/gallery/albumModule/PhotoThumbPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
:galleryConfig="props.galleryConfig"
:selectedPhotos="props.selectedPhotos"
:iter="0"
:idx="0"
:group-idx="0"
@clicked="propagateClicked"
@selected="propagateSelected"
@contexted="propagateMenuOpen"
:isTimeline="isTimeline"
/>
<template v-else>
<Timeline v-if="isLeftBorderVisible" :value="photosTimeLine" :pt:eventopposite:class="'hidden'" class="mt-4">
<Timeline v-if="isLeftBorderVisible" :value="props.photosTimeline" :pt:eventopposite:class="'hidden'" class="mt-4">
<template #content="slotProps">
<div
data-type="timelineBlock"
Expand All @@ -33,7 +33,7 @@
:galleryConfig="props.galleryConfig"
:selectedPhotos="props.selectedPhotos"
:iter="slotProps.item.iter"
:idx="slotProps.index"
:group-idx="slotProps.index"
:isTimeline="isTimeline"
@contexted="propagateMenuOpen"
@selected="propagateSelected"
Expand All @@ -43,7 +43,7 @@
</template>
</Timeline>
<div v-else>
<template v-for="(photoTimeline, idx) in photosTimeLine" :key="'photoTimeline' + idx">
<template v-for="(photoTimeline, idx) in props.photosTimeline" :key="'photoTimeline' + idx">
<div
data-type="timelineBlock"
:data-date="photoTimeline.data[0].timeline?.time_date"
Expand All @@ -57,7 +57,7 @@
:galleryConfig="props.galleryConfig"
:selectedPhotos="props.selectedPhotos"
:iter="photoTimeline.iter"
:idx="idx"
:group-idx="idx"
:isTimeline="isTimeline"
@contexted="propagateMenuOpen"
@selected="propagateSelected"
Expand All @@ -74,18 +74,20 @@ import { computed, ref } from "vue";
import Panel from "primevue/panel";
import { useLycheeStateStore } from "@/stores/LycheeState";
import { storeToRefs } from "pinia";
import { SplitData, useSplitter } from "@/composables/album/splitter";
import { type SplitData, useSplitter } from "@/composables/album/splitter";
import Timeline from "primevue/timeline";
import PhotoThumbPanelList from "./PhotoThumbPanelList.vue";
import PhotoThumbPanelControl from "./PhotoThumbPanelControl.vue";
import { isTouchDevice } from "@/utils/keybindings-utils";
import { onMounted } from "vue";

const lycheeStore = useLycheeStateStore();
const { is_timeline_left_border_visible } = storeToRefs(lycheeStore);
const { is_timeline_left_border_visible, is_debug_enabled } = storeToRefs(lycheeStore);

const props = defineProps<{
header: string;
photos: { [key: number]: App.Http.Resources.Models.PhotoResource };
photos: App.Http.Resources.Models.PhotoResource[];
photosTimeline: SplitData<App.Http.Resources.Models.PhotoResource>[] | undefined;
photoLayout: App.Enum.PhotoLayoutType;
album:
| App.Http.Resources.Models.AlbumResource
Expand Down Expand Up @@ -122,15 +124,13 @@ const propagateMenuOpen = (idx: number, e: MouseEvent) => {
emits("contexted", idx, e);
};

const { spliter } = useSplitter();
const { verifyOrder } = useSplitter();

const photosTimeLine = computed<SplitData<App.Http.Resources.Models.PhotoResource>[]>(() =>
spliter(
props.photos as App.Http.Resources.Models.PhotoResource[],
(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.time_date ?? "",
(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.format ?? "Others",
),
);
const isTimeline = computed(() => props.isTimeline && props.photosTimeline !== undefined && props.photosTimeline.length > 1);

const isTimeline = computed(() => props.isTimeline && photosTimeLine.value.length > 1);
onMounted(() => {
if (isTimeline.value) {
verifyOrder(is_debug_enabled.value, props.photos, props.photosTimeline as SplitData<App.Http.Resources.Models.PhotoResource>[]);
}
});
</script>
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<template>
<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.idx">
<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.groupIdx">
<template v-for="(photo, idx) in props.photos" :key="photo.id">
<PhotoThumb
@click="maySelect(idx + iter, $event)"
@contextmenu.prevent="menuOpen(idx + iter, $event)"
@click="maySelect(idx + props.iter, $event)"
@contextmenu.prevent="menuOpen(idx + props.iter, $event)"
:is-selected="props.selectedPhotos.includes(photo.id)"
:photo="photo"
:album="props.album"
:is-lazy="idx + iter > 10"
:is-lazy="idx + props.iter > 10"
/>
</template>
</div>
Expand All @@ -30,7 +30,7 @@ const props = defineProps<{
galleryConfig: App.Http.Resources.GalleryConfigs.PhotoLayoutConfig;
selectedPhotos: string[];
iter: number;
idx: number;
groupIdx: number;
}>();

const lycheeStore = useLycheeStateStore();
Expand All @@ -48,17 +48,19 @@ const emits = defineEmits<{
selected: [idx: number, event: MouseEvent];
contexted: [idx: number, event: MouseEvent];
}>();
const maySelect = (idx: number, e: MouseEvent) => {
function maySelect(idx: number, e: MouseEvent) {
if (ctrlKeyState.value || metaKeyState.value || shiftKeyState.value) {
emits("selected", idx, e);
return;
}
emits("clicked", idx, e);
};
const menuOpen = (idx: number, e: MouseEvent) => emits("contexted", idx, e);
}
function menuOpen(idx: number, e: MouseEvent) {
emits("contexted", idx, e);
}

// Layouts stuff
const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.idx);
const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.groupIdx);
onMounted(() => activateLayout());
onUpdated(() => activateLayout());
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@
:photo-layout="props.layout"
:header="props.photoHeader"
:photos="props.photos"
:photos-timeline="undefined"
:album="undefined"
:gallery-config="props.layoutConfig"
:selected-photos="selectedPhotosIds"
@clicked="photoClick"
@selected="photoSelect"
@contexted="photoMenuOpen"
:is-timeline="props.isPhotoTimelineEnabled"
:is-timeline="false"
:with-control="true"
/>
<div class="flex justify-center w-full" v-if="photos.length > 0">
Expand Down
36 changes: 35 additions & 1 deletion resources/js/composables/album/albumRefresher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { ALL } from "@/config/constants";
import AlbumService from "@/services/album-service";
import { AuthStore } from "@/stores/Auth";
import { computed, Ref, ref } from "vue";
import { type SplitData, useSplitter } from "./splitter";

const { spliter, merge } = useSplitter();

export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | undefined>, auth: AuthStore, isLoginOpen: Ref<boolean>) {
const isPasswordProtected = ref(false);
Expand All @@ -15,6 +18,7 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un

const photo = ref<App.Http.Resources.Models.PhotoResource | undefined>(undefined);
const photos = ref<App.Http.Resources.Models.PhotoResource[]>([]);
const photosTimeline = ref<SplitData<App.Http.Resources.Models.PhotoResource>[] | undefined>(undefined);

const config = ref<App.Http.Resources.GalleryConfigs.AlbumConfig | undefined>(undefined);
const rights = computed(() => album.value?.rights ?? undefined);
Expand All @@ -39,14 +43,43 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un
modelAlbum.value = undefined;
tagAlbum.value = undefined;
smartAlbum.value = undefined;
photosTimeline.value = undefined;
if (data.data.config.is_model_album) {
modelAlbum.value = data.data.resource as App.Http.Resources.Models.AlbumResource;
} else if (data.data.config.is_base_album) {
tagAlbum.value = data.data.resource as App.Http.Resources.Models.TagAlbumResource;
} else {
smartAlbum.value = data.data.resource as App.Http.Resources.Models.SmartAlbumResource;
}
photos.value = album.value?.photos ?? [];

// So what is going on here?
// The problem is that the ordering of the photos from the API is not necessarily the same
// as the ordering of the photos in the timeline. The timeline is constructed from the photos
// taken_at data and if not provided, created_at data.
// This is split into different chunks based on the granularity.
// If the ordering is done by created_at, and the order of the photos match, we do not have problems.
// But if one of the photos has a different taken_at date, that does not match the order, then all the following
// photos are "moved" to different place which does not match the original index ordering.
//
// When we click on a photo, the index returned refers to the original ordering of the photos.
// As a result, if the timeline is enabled, we first do the split and then merge the photos so that the
// ordering is updated to reflect the timeline.
//
// Note that this is not something that can be fixed in the backend as we would need to assume that all the dates are
// set properly. Furthermore, this would make the functionality unavailable if sorting by title is done.
// By doing it in the front-end, we are able to display the photos by blocks of time,
// and within the block, the ordering is done as expected.
if (data.data.config.is_photo_timeline_enabled) {
photosTimeline.value = spliter(
data.data.resource?.photos ?? [],
(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.time_date ?? "",
(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.format ?? "Others",
);
photos.value = merge(photosTimeline.value);
} else {
// We are not using the timeline, so we can just use the photos as is.
photos.value = album.value?.photos ?? [];
}
})
.catch((error) => {
if (error.response && error.response.status === 401 && error.response.data.message === "Password required") {
Expand Down Expand Up @@ -84,6 +117,7 @@ export function useAlbumRefresher(albumId: Ref<string>, photoId: Ref<string | un
rights,
photo,
photos,
photosTimeline,
config,
loadUser,
loadAlbum,
Expand Down
2 changes: 1 addition & 1 deletion resources/js/composables/album/albumsRefresher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import AlbumService from "@/services/album-service";
import { AuthStore } from "@/stores/Auth";
import { LycheeStateStore } from "@/stores/LycheeState";
import { computed, ref, Ref } from "vue";
import { SplitData, useSplitter } from "./splitter";
import { type SplitData, useSplitter } from "./splitter";

export function useAlbumsRefresher(auth: AuthStore, lycheeStore: LycheeStateStore, isLoginOpen: Ref<boolean>) {
const { spliter } = useSplitter();
Expand Down
39 changes: 39 additions & 0 deletions resources/js/composables/album/splitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,46 @@ export function useSplitter() {
return ret;
}

function merge<T>(data: SplitData<T>[]): T[] {
const ret: T[] = [];
data.forEach((d) => ret.push(...d.data));
return ret;
}

function verifyOrder(is_debug: boolean, data: { id: string }[], splitData: SplitData<{ id: string }>[]) {
if (!is_debug) {
return;
}

const dataMap = new Map<string, number>();
data.forEach((d, i) => {
dataMap.set(d.id, i);
});

let check = false;
splitData.forEach((chunk) => {
chunk.data.forEach((d, idx) => {
const expected = dataMap.get(d.id);
if (expected === undefined) {
console.error(`Data not found in original data for id ${d.id} (WTF??)`);
check = true;
}
const candidate = chunk.iter + idx;
if (expected !== candidate) {
console.error(`Data mismatch for id ${d.id} (expected ${expected}, got ${candidate})`);
check = true;
}
});
});

if (check) {
alert("Data mismatch found in splitter, please check the console logs and contact the developer.");
}
}

return {
spliter,
merge,
verifyOrder,
};
}
Loading