Add on this day to top of timeline (#41)

old-stable24
Varun Patil 2022-10-18 10:42:44 -07:00
parent b63a2cbfa7
commit 42fcae0f51
4 changed files with 283 additions and 22 deletions

View File

@ -32,6 +32,11 @@
v-show="!$refs.topmatter.type && list.length > 0"> v-show="!$refs.topmatter.type && list.length > 0">
{{ getViewName() }} {{ getViewName() }}
</div> </div>
<OnThisDay v-if="$route.name === 'timeline'"
:viewerManager="viewerManager"
@load="scrollerManager.adjust()">
</OnThisDay>
</template> </template>
<template v-slot="{ item }"> <template v-slot="{ item }">
@ -109,6 +114,7 @@ import Folder from "./frame/Folder.vue";
import Tag from "./frame/Tag.vue"; import Tag from "./frame/Tag.vue";
import Photo from "./frame/Photo.vue"; import Photo from "./frame/Photo.vue";
import TopMatter from "./top-matter/TopMatter.vue"; import TopMatter from "./top-matter/TopMatter.vue";
import OnThisDay from "./top-matter/OnThisDay.vue";
import SelectionManager from './SelectionManager.vue'; import SelectionManager from './SelectionManager.vue';
import ScrollerManager from './ScrollerManager.vue'; import ScrollerManager from './ScrollerManager.vue';
import UserConfig from "../mixins/UserConfig"; import UserConfig from "../mixins/UserConfig";
@ -128,6 +134,7 @@ const MOBILE_NUM_COLS = 3; // Number of columns on phone
Tag, Tag,
Photo, Photo,
TopMatter, TopMatter,
OnThisDay,
SelectionManager, SelectionManager,
ScrollerManager, ScrollerManager,
NcEmptyContent, NcEmptyContent,
@ -915,9 +922,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
* @param delPhotos photos to delete * @param delPhotos photos to delete
*/ */
async deleteFromViewWithAnimation(delPhotos: IPhoto[]) { async deleteFromViewWithAnimation(delPhotos: IPhoto[]) {
if (delPhotos.length === 0) { // Only keep photos with day
return; delPhotos = delPhotos.filter(p => p.d);
} if (delPhotos.length === 0) return;
// Get all days that need to be updatd // Get all days that need to be updatd
const updatedDays = new Set<IDay>(delPhotos.map(p => p.d)); const updatedDays = new Set<IDay>(delPhotos.map(p => p.d));

View File

@ -0,0 +1,253 @@
<template>
<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>
</div>
</div>
<div class="left-btn dir-btn memories__onthisday__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>
<div class="right-btn dir-btn memories__onthisday__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">
import { Component, Emit, Mixins, Prop } from 'vue-property-decorator';
import GlobalMixin from '../../mixins/GlobalMixin';
import { NcActions, NcActionButton } from '@nextcloud/vue';
import * as utils from "../../services/Utils";
import * as dav from '../../services/DavRequests';
import { ViewerManager } from "../../services/Viewer";
import { IPhoto } from '../../types';
import { getPreviewUrl } from "../../services/FileUtils";
import LeftMoveIcon from 'vue-material-design-icons/ChevronLeft.vue';
import RightMoveIcon from 'vue-material-design-icons/ChevronRight.vue';
interface IYear {
year: number;
url: string;
preview: IPhoto;
photos: IPhoto[];
text: string;
};
@Component({
name: 'OnThisDay',
components: {
NcActions,
NcActionButton,
LeftMoveIcon,
RightMoveIcon,
}
})
export default class OnThisDay extends Mixins(GlobalMixin) {
private getPreviewUrl = getPreviewUrl;
@Emit('load')
onload() {}
private years: IYear[] = []
private hasRight = false;
private hasLeft = false;
private scrollStack: number[] = [];
/**
* Nextcloud viewer proxy
* Can't use the timeline instance because these photos
* might not be in view, so can't delete them
*/
@Prop()
private viewerManager!: ViewerManager;
mounted() {
const inner = this.$refs.inner as HTMLElement;
inner.addEventListener('scroll', this.onScroll.bind(this));
this.refresh();
}
async refresh() {
const photos = await dav.getOnThisDayRaw();
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: photo,
photos: [],
text: utils.getFromNowStr(dateTaken),
});
currentYear = year;
}
const yearObj = this.years[this.years.length - 1];
yearObj.photos.push(photo);
}
// Randomly choose preview photo
for (const year of this.years) {
const index = Math.floor(Math.random() * year.photos.length);
year.preview = year.photos[index];
year.url = getPreviewUrl(year.preview.fileid, year.preview.etag, false, 512);
}
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.viewerManager.open(year.preview, allPhotos);
}
}
</script>
<style lang="scss" scoped>
$height: 200px;
.outer {
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: 8px;
.inner {
height: calc(100% + 20px);
white-space: nowrap;
overflow-x: scroll;
scroll-behavior: smooth;
border-radius: 10px;
}
.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 { padding: 0 8px; }
.dir-btn { display: none; }
}
}
.group {
height: $height;
aspect-ratio: 4/3;
display: inline-block;
position: relative;
cursor: pointer;
&:not(:last-of-type) { margin-right: 6px; }
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;
font-weight: bold;
padding-bottom: 5%;
text-shadow: 0 0 2px black;
cursor: inherit;
transition: background-color 0.2s ease-in-out;
}
&:hover .overlay {
background-color: transparent;
}
}
</style>
<style lang="scss">
.memories__onthisday__btn button {
transform: scale(0.6);
box-shadow: black 0 0 3px 0 !important;
background-color: var(--color-main-background) !important;
}
</style>

View File

@ -391,11 +391,10 @@ export async function downloadFilesByIds(fileIds: number[]) {
} }
/** /**
* Get the onThisDay data * Get original onThisDay response.
* Query for last 120 years; should be enough
*/ */
export async function getOnThisDayData(): Promise<IDay[]> { export async function getOnThisDayRaw() {
const diffs: { [dayId: number]: number } = {}; const dayIds: number[] = [];
const now = new Date(); const now = new Date();
const nowUTC = new Date(now.getTime() - now.getTimezoneOffset() * 60000); const nowUTC = new Date(now.getTime() - now.getTimezoneOffset() * 60000);
@ -407,20 +406,22 @@ export async function getOnThisDayData(): Promise<IDay[]> {
d.setFullYear(d.getFullYear() - i); d.setFullYear(d.getFullYear() - i);
d.setDate(d.getDate() + j); d.setDate(d.getDate() + j);
const dayId = Math.floor(d.getTime() / 1000 / 86400) const dayId = Math.floor(d.getTime() / 1000 / 86400)
diffs[dayId] = i; dayIds.push(dayId);
} }
} }
return (await axios.post<IPhoto[]>(generateUrl('/apps/memories/api/days'), {
body_ids: dayIds.join(','),
})).data;
}
/**
* Get the onThisDay data
* Query for last 120 years; should be enough
*/
export async function getOnThisDayData(): Promise<IDay[]> {
// Query for photos // Query for photos
let data: IPhoto[] = []; let data = await getOnThisDayRaw();
try {
const res = await axios.post<IPhoto[]>(generateUrl('/apps/memories/api/days'), {
body_ids: Object.keys(diffs).join(','),
});
data = res.data;
} catch (e) {
throw e;
}
// Group photos by day // Group photos by day
const ans: IDay[] = []; const ans: IDay[] = [];

View File

@ -21,19 +21,19 @@ export class ViewerManager {
}); });
} }
public async open(photo: IPhoto) { public async open(photo: IPhoto, list?: IPhoto[]) {
const day = photo.d; list = list || photo.d?.detail;
if (!day) return; if (!list) return;
// Repopulate map // Repopulate map
this.photoMap.clear(); this.photoMap.clear();
for (const p of day.detail) { for (const p of list) {
this.photoMap.set(p.fileid, p); this.photoMap.set(p.fileid, p);
} }
// Get file infos // Get file infos
let fileInfos: IFileInfo[]; let fileInfos: IFileInfo[];
const ids = day.detail.map(p => p.fileid); const ids = list.map(p => p.fileid);
try { try {
this.updateLoading(1); this.updateLoading(1);
fileInfos = await dav.getFiles(ids); fileInfos = await dav.getFiles(ids);