2022-10-18 17:42:44 +00:00
|
|
|
<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 }}
|
2022-10-18 17:42:44 +00:00
|
|
|
</div>
|
2022-10-28 19:08:34 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
2022-10-18 17:42:44 +00:00
|
|
|
|
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>
|
2022-10-18 17:42:44 +00:00
|
|
|
</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>
|
2022-10-18 17:42:44 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts">
|
2022-10-28 19:08:34 +00:00
|
|
|
import { Component, Emit, Mixins, Prop } from "vue-property-decorator";
|
|
|
|
import GlobalMixin from "../../mixins/GlobalMixin";
|
|
|
|
import { NcActions, NcActionButton } from "@nextcloud/vue";
|
2022-10-18 17:42:44 +00:00
|
|
|
|
|
|
|
import * as utils from "../../services/Utils";
|
2022-10-28 19:08:34 +00:00
|
|
|
import * as dav from "../../services/DavRequests";
|
|
|
|
import { IPhoto } from "../../types";
|
2022-10-18 17:42:44 +00:00
|
|
|
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";
|
2022-10-18 17:42:44 +00:00
|
|
|
|
|
|
|
interface IYear {
|
2022-10-28 19:08:34 +00:00
|
|
|
year: number;
|
|
|
|
url: string;
|
|
|
|
preview: IPhoto;
|
|
|
|
photos: IPhoto[];
|
|
|
|
text: string;
|
|
|
|
}
|
2022-10-18 17:42:44 +00:00
|
|
|
|
|
|
|
@Component({
|
2022-10-28 19:08:34 +00:00
|
|
|
name: "OnThisDay",
|
|
|
|
components: {
|
|
|
|
NcActions,
|
|
|
|
NcActionButton,
|
|
|
|
LeftMoveIcon,
|
|
|
|
RightMoveIcon,
|
|
|
|
},
|
2022-10-18 17:42:44 +00:00
|
|
|
})
|
|
|
|
export default class OnThisDay extends Mixins(GlobalMixin) {
|
2022-10-28 19:08:34 +00:00
|
|
|
private getPreviewUrl = getPreviewUrl;
|
|
|
|
|
2022-11-06 06:32:25 +00:00
|
|
|
@Prop()
|
|
|
|
private viewer: any;
|
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
@Emit("load")
|
|
|
|
onload() {}
|
|
|
|
|
|
|
|
private years: IYear[] = [];
|
|
|
|
|
|
|
|
private hasRight = false;
|
|
|
|
private hasLeft = false;
|
|
|
|
private scrollStack: number[] = [];
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
for (const photo of photos) {
|
|
|
|
const dateTaken = utils.dayIdToDate(photo.dayid);
|
|
|
|
const year = dateTaken.getUTCFullYear();
|
|
|
|
|
|
|
|
if (year !== currentYear) {
|
|
|
|
this.years.push({
|
|
|
|
year,
|
|
|
|
url: "",
|
|
|
|
preview: null,
|
|
|
|
photos: [],
|
|
|
|
text: utils.getFromNowStr(dateTaken),
|
|
|
|
});
|
|
|
|
currentYear = year;
|
|
|
|
}
|
|
|
|
|
|
|
|
const yearObj = this.years[this.years.length - 1];
|
|
|
|
yearObj.photos.push(photo);
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 19:08:34 +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-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
// Choose preview photo
|
|
|
|
for (const year of this.years) {
|
|
|
|
// Try to prioritize landscape photos on desktop
|
|
|
|
if (window.innerWidth <= 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);
|
2022-10-29 00:25:39 +00:00
|
|
|
year.url = getPreviewUrl(year.preview, false, 512);
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-28 19:08:34 +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);
|
2022-11-06 06:32:25 +00:00
|
|
|
this.viewer.openStatic(year.preview, allPhotos);
|
2022-10-28 19:08:34 +00:00
|
|
|
}
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
$height: 200px;
|
2022-10-18 18:11:04 +00:00
|
|
|
$mobHeight: 150px;
|
2022-10-18 17:42:44 +00:00
|
|
|
|
|
|
|
.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;
|
2022-10-18 17:42:44 +00:00
|
|
|
.inner {
|
2022-10-28 19:08:34 +00:00
|
|
|
padding: 0 8px;
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
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;
|
|
|
|
}
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.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;
|
2022-10-18 17:42:44 +00:00
|
|
|
.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
|
|
|
}
|
2022-10-18 17:42:44 +00:00
|
|
|
}
|
|
|
|
</style>
|