memories/src/components/top-matter/OnThisDay.vue

326 lines
7.5 KiB
Vue
Raw Normal View History

<template>
2022-10-28 19:08:34 +00:00
<div class="outer" v-show="years.length > 0">
<div class="inner" ref="inner">
<div
v-for="year of years"
class="group"
:key="year.year"
@click="click(year)"
>
<img class="fill-block" :src="year.url" />
<div class="overlay">
{{ year.text }}
</div>
2022-10-28 19:08:34 +00:00
</div>
</div>
2022-10-28 19:08:34 +00:00
<div class="left-btn dir-btn" v-if="hasLeft">
<NcActions>
<NcActionButton
:aria-label="t('memories', 'Move left')"
@click="moveLeft"
>
{{ t("memories", "Move left") }}
<template #icon> <LeftMoveIcon :size="28" /> </template>
</NcActionButton>
</NcActions>
</div>
2022-10-28 19:08:34 +00:00
<div class="right-btn dir-btn" v-if="hasRight">
<NcActions>
<NcActionButton
:aria-label="t('memories', 'Move right')"
@click="moveRight"
>
{{ t("memories", "Move right") }}
<template #icon> <RightMoveIcon :size="28" /> </template>
</NcActionButton>
</NcActions>
</div>
</div>
</template>
<script lang="ts">
2022-12-10 10:04:07 +00:00
import { defineComponent } from "vue";
2022-11-24 19:54:29 +00:00
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import * as utils from "../../services/Utils";
2022-10-28 19:08:34 +00:00
import * as dav from "../../services/DavRequests";
import { IPhoto } from "../../types";
import { getPreviewUrl } from "../../services/FileUtils";
2022-10-28 19:08:34 +00:00
import LeftMoveIcon from "vue-material-design-icons/ChevronLeft.vue";
import RightMoveIcon from "vue-material-design-icons/ChevronRight.vue";
interface IYear {
2022-10-28 19:08:34 +00:00
year: number;
url: string;
preview: IPhoto;
photos: IPhoto[];
text: string;
}
2022-12-10 10:04:07 +00:00
export default defineComponent({
2022-10-28 19:08:34 +00:00
name: "OnThisDay",
components: {
NcActions,
NcActionButton,
LeftMoveIcon,
RightMoveIcon,
},
2022-12-10 10:04:07 +00:00
props: {
viewer: {
type: Object,
required: false,
},
},
2022-10-28 19:08:34 +00:00
2022-12-10 18:59:36 +00:00
data: () => ({
getPreviewUrl,
years: [] as IYear[],
hasRight: false,
hasLeft: false,
scrollStack: [] as number[],
}),
2022-10-28 19:08:34 +00:00
mounted() {
const inner = this.$refs.inner as HTMLElement;
2022-10-30 00:17:34 +00:00
inner.addEventListener("scroll", this.onScroll.bind(this), {
passive: true,
});
2022-10-28 19:08:34 +00:00
this.refresh();
2022-12-10 10:04:07 +00:00
},
2022-10-28 19:08:34 +00:00
2022-12-10 10:04:07 +00:00
methods: {
onload() {
this.$emit("load");
},
async refresh() {
// Look for cache
const dayIdToday = utils.dateToDayId(new Date());
const cacheUrl = `/onthisday/${dayIdToday}`;
const cache = await utils.getCachedData<IPhoto[]>(cacheUrl);
if (cache) this.process(cache);
// Network request
const photos = await dav.getOnThisDayRaw();
utils.cacheData(cacheUrl, photos);
// Check if exactly same as cache
if (
cache?.length === photos.length &&
cache.every((p, i) => p.fileid === photos[i].fileid)
)
return;
this.process(photos);
},
async process(photos: IPhoto[]) {
this.years = [];
let currentYear = 9999;
let currentText = "";
2022-12-10 10:04:07 +00:00
for (const photo of photos) {
const dateTaken = utils.dayIdToDate(photo.dayid);
const year = dateTaken.getUTCFullYear();
photo.key = `${photo.fileid}`;
// DateTime calls are expensive, so check if the year
// itself is different first, then also check the text
2022-12-10 10:04:07 +00:00
if (year !== currentYear) {
const text = utils.getFromNowStr(dateTaken);
if (text !== currentText) {
this.years.push({
year,
text,
url: "",
preview: null,
photos: [],
});
currentText = text;
}
2022-12-10 10:04:07 +00:00
currentYear = year;
}
const yearObj = this.years[this.years.length - 1];
yearObj.photos.push(photo);
2022-10-28 19:08:34 +00:00
}
2022-12-10 10:04:07 +00:00
// For each year, randomly choose 10 photos to display
for (const year of this.years) {
year.photos = utils.randomSubarray(year.photos, 10);
2022-10-28 19:08:34 +00:00
}
2022-12-10 10:04:07 +00:00
// Choose preview photo
for (const year of this.years) {
// Try to prioritize landscape photos on desktop
if (globalThis.windowInnerWidth <= 600) {
const landscape = year.photos.filter((p) => p.w > p.h);
year.preview = utils.randomChoice(landscape);
}
// Get random photo
year.preview ||= utils.randomChoice(year.photos);
year.url = getPreviewUrl(year.preview, false, 512);
}
2022-10-28 19:08:34 +00:00
2022-12-10 10:04:07 +00:00
await this.$nextTick();
this.onScroll();
this.onload();
},
moveLeft() {
const inner = this.$refs.inner as HTMLElement;
inner.scrollBy(-(this.scrollStack.pop() || inner.clientWidth), 0);
},
moveRight() {
const inner = this.$refs.inner as HTMLElement;
const innerRect = inner.getBoundingClientRect();
const nextChild = Array.from(inner.children)
.map((c) => c.getBoundingClientRect())
.find((rect) => rect.right > innerRect.right);
let scroll = nextChild
? nextChild.left - innerRect.left
: inner.clientWidth;
scroll = Math.min(
inner.scrollWidth - inner.scrollLeft - inner.clientWidth,
scroll
);
this.scrollStack.push(scroll);
inner.scrollBy(scroll, 0);
},
onScroll() {
const inner = this.$refs.inner as HTMLElement;
if (!inner) return;
this.hasLeft = inner.scrollLeft > 0;
this.hasRight =
inner.clientWidth + inner.scrollLeft < inner.scrollWidth - 20;
},
click(year: IYear) {
const allPhotos = this.years.flatMap((y) => y.photos);
this.viewer.openStatic(year.preview, allPhotos, 512);
},
},
});
</script>
<style lang="scss" scoped>
$height: 200px;
2022-10-18 18:11:04 +00:00
$mobHeight: 150px;
.outer {
2022-10-28 19:08:34 +00:00
width: calc(100% - 50px);
height: $height;
overflow: hidden;
position: relative;
padding: 0 calc(28px * 0.6);
// Sloppy: ideally this should be done in Timeline
// to put a gap between the title and this
margin-top: 10px;
.inner {
height: calc(100% + 20px);
white-space: nowrap;
overflow-x: scroll;
scroll-behavior: smooth;
border-radius: 10px;
}
:deep .dir-btn button {
transform: scale(0.6);
box-shadow: var(--color-main-text) 0 0 3px 0 !important;
background-color: var(--color-main-background) !important;
}
.left-btn {
position: absolute;
top: 50%;
left: 0;
transform: translate(-10%, -50%);
}
.right-btn {
position: absolute;
top: 50%;
right: 0;
transform: translate(10%, -50%);
}
@media (max-width: 768px) {
width: 98%;
padding: 0;
.inner {
2022-10-28 19:08:34 +00:00
padding: 0 8px;
}
2022-10-28 19:08:34 +00:00
.dir-btn {
display: none;
2022-10-18 18:11:04 +00:00
}
2022-10-28 19:08:34 +00:00
}
@media (max-width: 600px) {
height: $mobHeight;
}
}
.group {
2022-10-28 19:08:34 +00:00
height: $height;
aspect-ratio: 4/3;
display: inline-block;
position: relative;
cursor: pointer;
&:not(:last-of-type) {
margin-right: 8px;
}
img {
cursor: inherit;
object-fit: cover;
border-radius: 10px;
background-color: var(--color-background-dark);
background-clip: padding-box, content-box;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 10px;
display: flex;
align-items: end;
justify-content: center;
color: white;
font-size: 1.2em;
padding: 5%;
white-space: normal;
cursor: inherit;
transition: background-color 0.2s ease-in-out;
}
&:hover .overlay {
background-color: transparent;
}
@media (max-width: 600px) {
aspect-ratio: 3/4;
height: $mobHeight;
.overlay {
2022-10-28 19:08:34 +00:00
font-size: 1.1em;
2022-10-18 18:11:04 +00:00
}
2022-10-28 19:08:34 +00:00
}
}
</style>