big: switch to justified layout
parent
4e98e93d6e
commit
e298ef97fa
|
@ -12,6 +12,7 @@
|
||||||
"@nextcloud/l10n": "^1.6.0",
|
"@nextcloud/l10n": "^1.6.0",
|
||||||
"@nextcloud/paths": "^2.1.0",
|
"@nextcloud/paths": "^2.1.0",
|
||||||
"@nextcloud/vue": "^7.0.0",
|
"@nextcloud/vue": "^7.0.0",
|
||||||
|
"justified-layout": "^4.1.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"path-posix": "^1.0.0",
|
"path-posix": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
@ -7430,6 +7431,11 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/justified-layout": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
|
||||||
|
},
|
||||||
"node_modules/kind-of": {
|
"node_modules/kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
@ -17701,6 +17707,11 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"justified-layout": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
|
||||||
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"@nextcloud/l10n": "^1.6.0",
|
"@nextcloud/l10n": "^1.6.0",
|
||||||
"@nextcloud/paths": "^2.1.0",
|
"@nextcloud/paths": "^2.1.0",
|
||||||
"@nextcloud/vue": "^7.0.0",
|
"@nextcloud/vue": "^7.0.0",
|
||||||
|
"justified-layout": "^4.1.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"path-posix": "^1.0.0",
|
"path-posix": "^1.0.0",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|
|
@ -119,6 +119,7 @@ export default class App extends Mixins(GlobalMixin, UserConfig) {
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
|
|
||||||
// Get rid of padding on img-outer (1px on mobile)
|
// Get rid of padding on img-outer (1px on mobile)
|
||||||
|
// Also need to make sure we don't end up with a scrollbar -- see below
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
width: calc(100% + 3px); // 1px extra here because ... reasons
|
width: calc(100% + 3px); // 1px extra here because ... reasons
|
||||||
}
|
}
|
||||||
|
@ -139,6 +140,12 @@ body {
|
||||||
width: calc(100% - var(--body-container-margin)*1); // was *2
|
width: calc(100% - var(--body-container-margin)*1); // was *2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide horizontal scrollbar on mobile
|
||||||
|
// For the padding removal above
|
||||||
|
#app-content-vue {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
// Fill all available space
|
// Fill all available space
|
||||||
.fill-block {
|
.fill-block {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -72,6 +72,8 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
private scrollingRecyclerTimer = null as number | null;
|
private scrollingRecyclerTimer = null as number | null;
|
||||||
/** View size reflow timer */
|
/** View size reflow timer */
|
||||||
private reflowRequest = false;
|
private reflowRequest = false;
|
||||||
|
/** Tick adjust timer */
|
||||||
|
private adjustTimer = null as number | null;
|
||||||
|
|
||||||
/** Get the visible ticks */
|
/** Get the visible ticks */
|
||||||
get visibleTicks() {
|
get visibleTicks() {
|
||||||
|
@ -91,7 +93,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
/** Recycler scroll event, must be called by timeline */
|
/** Recycler scroll event, must be called by timeline */
|
||||||
public recyclerScrolled(event?: any) {
|
public recyclerScrolled(event?: any) {
|
||||||
this.cursorY = event ? event.target.scrollTop * this.height / this.recyclerHeight : 0;
|
this.cursorY = utils.roundHalf(event ? event.target.scrollTop * this.height / this.recyclerHeight : 0);
|
||||||
this.moveHoverCursor(this.cursorY);
|
this.moveHoverCursor(this.cursorY);
|
||||||
|
|
||||||
if (this.scrollingRecyclerTimer) window.clearTimeout(this.scrollingRecyclerTimer);
|
if (this.scrollingRecyclerTimer) window.clearTimeout(this.scrollingRecyclerTimer);
|
||||||
|
@ -114,22 +116,77 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
this.reflowRequest = false;
|
this.reflowRequest = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setTickTop(tick: ITick) {
|
||||||
|
const extraY = this.recyclerBefore?.clientHeight || 0;
|
||||||
|
tick.topF = (extraY + tick.y) * (this.height / this.recyclerHeight);
|
||||||
|
tick.top = utils.roundHalf(tick.topF);
|
||||||
|
}
|
||||||
|
|
||||||
/** Re-create tick data */
|
/** Re-create tick data */
|
||||||
private reflowNow() {
|
private reflowNow() {
|
||||||
|
// Refresh height of recycler
|
||||||
|
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
||||||
|
|
||||||
// Recreate ticks data
|
// Recreate ticks data
|
||||||
this.recreate();
|
this.recreate();
|
||||||
|
|
||||||
// Get height of recycler
|
// Recompute which ticks are visible
|
||||||
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
this.computeVisibleTicks();
|
||||||
|
|
||||||
// Static extra height at top (before slot)
|
|
||||||
const extraY = this.recyclerBefore?.clientHeight || 0;
|
|
||||||
|
|
||||||
// Compute tick positions
|
|
||||||
for (const tick of this.ticks) {
|
|
||||||
tick.top = (extraY + tick.y) * (this.height / this.recyclerHeight);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recreate from scratch */
|
||||||
|
private recreate() {
|
||||||
|
// Clear
|
||||||
|
this.ticks = [];
|
||||||
|
|
||||||
|
// Ticks
|
||||||
|
let y = 0;
|
||||||
|
let prevYear = 9999;
|
||||||
|
let prevMonth = 0;
|
||||||
|
const thisYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
// Get a new tick
|
||||||
|
const getTick = (dayId: number, text?: string | number): ITick => {
|
||||||
|
const tick = {
|
||||||
|
dayId,
|
||||||
|
y: y,
|
||||||
|
text,
|
||||||
|
topF: 0,
|
||||||
|
top: 0,
|
||||||
|
s: false,
|
||||||
|
};
|
||||||
|
this.setTickTop(tick);
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Itearte over rows
|
||||||
|
for (const row of this.rows) {
|
||||||
|
if (row.type === IRowType.HEAD) {
|
||||||
|
if (this.TagDayIDValueSet.has(row.dayId)) {
|
||||||
|
// Blank tick
|
||||||
|
this.ticks.push(getTick(row.dayId));
|
||||||
|
} else {
|
||||||
|
// Make date string
|
||||||
|
const dateTaken = utils.dayIdToDate(row.dayId);
|
||||||
|
|
||||||
|
// Create tick if month changed
|
||||||
|
const dtYear = dateTaken.getUTCFullYear();
|
||||||
|
const dtMonth = dateTaken.getUTCMonth()
|
||||||
|
if (Number.isInteger(row.dayId) && (dtMonth !== prevMonth || dtYear !== prevYear)) {
|
||||||
|
const text = (dtYear === prevYear || dtYear === thisYear) ? undefined : dtYear;
|
||||||
|
this.ticks.push(getTick(row.dayId, text));
|
||||||
|
}
|
||||||
|
prevMonth = dtMonth;
|
||||||
|
prevYear = dtYear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
y += row.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mark ticks as visible or invisible */
|
||||||
|
private computeVisibleTicks() {
|
||||||
// Do another pass to figure out which points are visible
|
// Do another pass to figure out which points are visible
|
||||||
// This is not as bad as it looks, it's actually 12*O(n)
|
// This is not as bad as it looks, it's actually 12*O(n)
|
||||||
// because there are only 12 months in a year
|
// because there are only 12 months in a year
|
||||||
|
@ -181,47 +238,45 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Recreate from scratch */
|
/**
|
||||||
private recreate() {
|
* Update tick positions without truncating the list
|
||||||
// Clear
|
* This is much cheaper than reflowing the whole thing
|
||||||
this.ticks = [];
|
*/
|
||||||
|
public adjust() {
|
||||||
|
if (this.adjustTimer) return;
|
||||||
|
this.adjustTimer = window.setTimeout(() => {
|
||||||
|
this.adjustTimer = null;
|
||||||
|
this.adjustNow();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
// Ticks
|
/** Do adjustment synchrnously */
|
||||||
|
private adjustNow() {
|
||||||
|
// Refresh height of recycler
|
||||||
|
this.recyclerHeight = this.recycler.$refs.wrapper.clientHeight;
|
||||||
|
|
||||||
|
// Start with the first tick. Walk over all rows counting the
|
||||||
|
// y position. When you hit a row with the tick, update y and
|
||||||
|
// top values and move to the next visible tick.
|
||||||
|
let tickId = 0; // regardless of whether it's visible or not
|
||||||
let y = 0;
|
let y = 0;
|
||||||
let prevYear = 9999;
|
|
||||||
let prevMonth = 0;
|
|
||||||
const thisYear = new Date().getFullYear();
|
|
||||||
|
|
||||||
// Get a new tick
|
|
||||||
const getTick = (dayId: number, text?: string | number): ITick => {
|
|
||||||
return {
|
|
||||||
dayId,
|
|
||||||
y: y,
|
|
||||||
text,
|
|
||||||
top: 0,
|
|
||||||
s: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Itearte over rows
|
|
||||||
for (const row of this.rows) {
|
for (const row of this.rows) {
|
||||||
if (row.type === IRowType.HEAD) {
|
// Check if tick is valid
|
||||||
if (this.TagDayIDValueSet.has(row.dayId)) {
|
if (tickId >= this.ticks.length) {
|
||||||
// Blank tick
|
return;
|
||||||
this.ticks.push(getTick(row.dayId));
|
|
||||||
} else {
|
|
||||||
// Make date string
|
|
||||||
const dateTaken = utils.dayIdToDate(row.dayId);
|
|
||||||
|
|
||||||
// Create tick if month changed
|
|
||||||
const dtYear = dateTaken.getUTCFullYear();
|
|
||||||
const dtMonth = dateTaken.getUTCMonth()
|
|
||||||
if (Number.isInteger(row.dayId) && (dtMonth !== prevMonth || dtYear !== prevYear)) {
|
|
||||||
const text = (dtYear === prevYear || dtYear === thisYear) ? undefined : dtYear;
|
|
||||||
this.ticks.push(getTick(row.dayId, text));
|
|
||||||
}
|
}
|
||||||
prevMonth = dtMonth;
|
|
||||||
prevYear = dtYear;
|
// Check if we hit the next tick
|
||||||
|
const tick = this.ticks[tickId];
|
||||||
|
if (tick.dayId === row.dayId) {
|
||||||
|
tick.y = y;
|
||||||
|
this.setTickTop(tick);
|
||||||
|
|
||||||
|
// Get the next visible tick
|
||||||
|
tickId++;
|
||||||
|
while (tickId < this.ticks.length && !this.ticks[tickId].s) {
|
||||||
|
tickId++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,10 +286,10 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
/** Change actual position of the hover cursor */
|
/** Change actual position of the hover cursor */
|
||||||
private moveHoverCursor(y: number) {
|
private moveHoverCursor(y: number) {
|
||||||
this.hoverCursorY = y;
|
this.hoverCursorY = utils.roundHalf(y);
|
||||||
|
|
||||||
// Get index of previous tick
|
// Get index of previous tick
|
||||||
let idx = utils.binarySearch(this.ticks, y, 'top');
|
let idx = utils.binarySearch(this.ticks, y, 'topF');
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
// use this tick
|
// use this tick
|
||||||
} else if (idx >= 1 && idx <= this.ticks.length) {
|
} else if (idx >= 1 && idx <= this.ticks.length) {
|
||||||
|
|
|
@ -51,10 +51,10 @@
|
||||||
|
|
||||||
<div v-else
|
<div v-else
|
||||||
class="photo-row"
|
class="photo-row"
|
||||||
:style="{ height: item.size + 'px' }">
|
:style="{ height: item.size + 'px', width: rowWidth + 'px' }">
|
||||||
|
|
||||||
<div class="photo" v-for="(photo, index) in item.photos" :key="index"
|
<div class="photo" v-for="(photo, index) in item.photos" :key="photo.fileid"
|
||||||
:style="{ width: rowHeight + 'px' }">
|
:style="{ width: (photo.dispWp * 100) + '%' }">
|
||||||
|
|
||||||
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER"
|
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER"
|
||||||
:data="photo"
|
:data="photo"
|
||||||
|
@ -102,6 +102,7 @@ import moment from 'moment';
|
||||||
|
|
||||||
import * as dav from "../services/DavRequests";
|
import * as dav from "../services/DavRequests";
|
||||||
import * as utils from "../services/Utils";
|
import * as utils from "../services/Utils";
|
||||||
|
import justifiedLayout from "justified-layout";
|
||||||
import axios from '@nextcloud/axios'
|
import axios from '@nextcloud/axios'
|
||||||
import Folder from "./frame/Folder.vue";
|
import Folder from "./frame/Folder.vue";
|
||||||
import Tag from "./frame/Tag.vue";
|
import Tag from "./frame/Tag.vue";
|
||||||
|
@ -117,8 +118,8 @@ import PeopleIcon from 'vue-material-design-icons/AccountMultiple.vue';
|
||||||
import ImageMultipleIcon from 'vue-material-design-icons/ImageMultiple.vue';
|
import ImageMultipleIcon from 'vue-material-design-icons/ImageMultiple.vue';
|
||||||
|
|
||||||
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
|
||||||
const MAX_PHOTO_WIDTH = 175; // Max width of a photo
|
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
|
||||||
const MIN_COLS = 3; // Min number of columns (on phone, e.g.)
|
const MOBILE_NUM_COLS = 3; // Number of columns on phone
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -144,7 +145,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
/** Counter of rows */
|
/** Counter of rows */
|
||||||
private numRows = 0;
|
private numRows = 0;
|
||||||
/** Computed number of columns */
|
/** Computed number of columns */
|
||||||
private numCols = 5;
|
private numCols = 0;
|
||||||
|
/** Keep all images square */
|
||||||
|
private squareMode = false;
|
||||||
/** Header rows for dayId key */
|
/** Header rows for dayId key */
|
||||||
private heads: { [dayid: number]: IHeadRow } = {};
|
private heads: { [dayid: number]: IHeadRow } = {};
|
||||||
/** Original days response */
|
/** Original days response */
|
||||||
|
@ -152,6 +155,8 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
|
||||||
/** Computed row height */
|
/** Computed row height */
|
||||||
private rowHeight = 100;
|
private rowHeight = 100;
|
||||||
|
/** Computed row width */
|
||||||
|
private rowWidth = 100;
|
||||||
|
|
||||||
/** Current start index */
|
/** Current start index */
|
||||||
private currentStart = 0;
|
private currentStart = 0;
|
||||||
|
@ -264,7 +269,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
// Size of outer container
|
// Size of outer container
|
||||||
const e = this.$refs.container as Element;
|
const e = this.$refs.container as Element;
|
||||||
let height = e.clientHeight;
|
let height = e.clientHeight;
|
||||||
let width = e.clientWidth;
|
this.rowWidth = e.clientWidth;
|
||||||
|
|
||||||
// Scroller spans the container height
|
// Scroller spans the container height
|
||||||
this.scrollerHeight = height;
|
this.scrollerHeight = height;
|
||||||
|
@ -277,22 +282,21 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
const recycler = this.$refs.recycler as any;
|
const recycler = this.$refs.recycler as any;
|
||||||
recycler.$el.style.height = (height - tmHeight - 4) + 'px';
|
recycler.$el.style.height = (height - tmHeight - 4) + 'px';
|
||||||
|
|
||||||
// Desktop scroller width
|
if (window.innerWidth <= 768) {
|
||||||
if (window.innerWidth > 768) {
|
// Mobile
|
||||||
width -= 40;
|
this.numCols = MOBILE_NUM_COLS;
|
||||||
|
this.rowHeight = this.rowWidth / this.numCols;
|
||||||
|
this.squareMode = true;
|
||||||
|
} else {
|
||||||
|
// Desktop
|
||||||
|
this.rowWidth -= 40;
|
||||||
|
this.rowHeight = DESKTOP_ROW_HEIGHT;
|
||||||
|
this.squareMode = false;
|
||||||
|
|
||||||
|
// As a heuristic, assume all images are 4:3 landscape
|
||||||
|
this.numCols = Math.floor(this.rowWidth / (this.rowHeight * 4 / 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.days.length === 0) {
|
|
||||||
// Don't change cols if already initialized
|
|
||||||
this.numCols = Math.max(MIN_COLS, Math.floor(width / MAX_PHOTO_WIDTH));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rowHeight = Math.floor(width / this.numCols);
|
|
||||||
|
|
||||||
// Set heights of rows
|
|
||||||
this.list.filter(r => r.type !== IRowType.HEAD).forEach(row => {
|
|
||||||
row.size = this.rowHeight;
|
|
||||||
});
|
|
||||||
this.scrollerManager.reflow();
|
this.scrollerManager.reflow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,9 +326,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
if (row.pct && !row.photos.length) {
|
if (row.pct && !row.photos.length) {
|
||||||
row.photos = new Array(row.pct);
|
row.photos = new Array(row.pct);
|
||||||
for (let j = 0; j < row.pct; j++) {
|
for (let j = 0; j < row.pct; j++) {
|
||||||
|
// Any row that has placeholders has ONLY placeholders
|
||||||
|
// so we can calculate the display width
|
||||||
row.photos[j] = {
|
row.photos[j] = {
|
||||||
flag: this.c.FLAG_PLACEHOLDER,
|
flag: this.c.FLAG_PLACEHOLDER,
|
||||||
fileid: Math.random(),
|
fileid: Math.random(),
|
||||||
|
dispWp: 1 / this.numCols,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
delete row.pct;
|
delete row.pct;
|
||||||
|
@ -374,6 +381,18 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Store the current scroll position to restore later */
|
||||||
|
private getScrollY() {
|
||||||
|
const recycler = this.$refs.recycler as any;
|
||||||
|
return recycler.$el.scrollTop
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore the stored scroll position */
|
||||||
|
private setScrollY(y: number) {
|
||||||
|
const recycler = this.$refs.recycler as any;
|
||||||
|
recycler.scrollToPosition(y);
|
||||||
|
}
|
||||||
|
|
||||||
/** Get query string for API calls */
|
/** Get query string for API calls */
|
||||||
appendQuery(url: string) {
|
appendQuery(url: string) {
|
||||||
const query = new URLSearchParams();
|
const query = new URLSearchParams();
|
||||||
|
@ -614,6 +633,19 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
const dayId = day.dayid;
|
const dayId = day.dayid;
|
||||||
const data = day.detail;
|
const data = day.detail;
|
||||||
|
|
||||||
|
// Create justified layout with correct params
|
||||||
|
const justify = justifiedLayout(day.detail.map(p => {
|
||||||
|
return {
|
||||||
|
width: (this.squareMode ? null : p.w) || this.rowHeight,
|
||||||
|
height: (this.squareMode ? null : p.h) || this.rowHeight,
|
||||||
|
};
|
||||||
|
}), {
|
||||||
|
containerWidth: this.rowWidth,
|
||||||
|
containerPadding: 0,
|
||||||
|
boxSpacing: 0,
|
||||||
|
targetRowHeight: this.rowHeight,
|
||||||
|
});
|
||||||
|
|
||||||
const head = this.heads[dayId];
|
const head = this.heads[dayId];
|
||||||
this.loadedDays.add(dayId);
|
this.loadedDays.add(dayId);
|
||||||
|
|
||||||
|
@ -625,30 +657,46 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
}
|
}
|
||||||
head.day.rows.clear();
|
head.day.rows.clear();
|
||||||
|
|
||||||
// Check if some row was added
|
// Check if some rows were added
|
||||||
let addedRow = false;
|
let addedRows: IRow[] = [];
|
||||||
|
|
||||||
|
// Check if row height changed
|
||||||
|
let rowSizeDelta = 0;
|
||||||
|
|
||||||
// Get index of header O(n)
|
// Get index of header O(n)
|
||||||
const headIdx = this.list.findIndex(item => item.id === head.id);
|
const headIdx = this.list.findIndex(item => item.id === head.id);
|
||||||
let rowIdx = headIdx + 1;
|
let rowIdx = headIdx + 1;
|
||||||
|
|
||||||
|
// Store the scroll position in case we change any rows
|
||||||
|
const scrollY = this.getScrollY();
|
||||||
|
|
||||||
|
// Previous justified row
|
||||||
|
let prevJustifyTop = justify.boxes[0]?.top || 0;
|
||||||
|
|
||||||
// Add all rows
|
// Add all rows
|
||||||
let dataIdx = 0;
|
let dataIdx = 0;
|
||||||
while (dataIdx < data.length) {
|
while (dataIdx < data.length) {
|
||||||
// Check if we ran out of rows
|
// Check if we ran out of rows
|
||||||
if (rowIdx >= this.list.length || this.list[rowIdx].type === IRowType.HEAD) {
|
if (rowIdx >= this.list.length || this.list[rowIdx].type === IRowType.HEAD) {
|
||||||
addedRow = true;
|
const newRow = this.getBlankRow(day);
|
||||||
this.list.splice(rowIdx, 0, this.getBlankRow(day));
|
addedRows.push(newRow);
|
||||||
|
rowSizeDelta += newRow.size;
|
||||||
|
this.list.splice(rowIdx, 0, newRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
const row = this.list[rowIdx];
|
|
||||||
|
|
||||||
// Go to the next row
|
// Go to the next row
|
||||||
if (row.photos.length >= this.numCols) {
|
const jbox = justify.boxes[dataIdx];
|
||||||
|
if (jbox.top !== prevJustifyTop) {
|
||||||
|
prevJustifyTop = jbox.top;
|
||||||
rowIdx++;
|
rowIdx++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set row height
|
||||||
|
const row = this.list[rowIdx];
|
||||||
|
rowSizeDelta += jbox.height - row.size;
|
||||||
|
row.size = jbox.height;
|
||||||
|
|
||||||
// Add the photo to the row
|
// Add the photo to the row
|
||||||
const photo = data[dataIdx];
|
const photo = data[dataIdx];
|
||||||
if (typeof photo.flag === "undefined") {
|
if (typeof photo.flag === "undefined") {
|
||||||
|
@ -678,6 +726,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
delete photo.istag;
|
delete photo.istag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get aspect ratio
|
||||||
|
photo.dispWp = jbox.width / this.rowWidth;
|
||||||
|
|
||||||
// Move to next index of photo
|
// Move to next index of photo
|
||||||
dataIdx++;
|
dataIdx++;
|
||||||
|
|
||||||
|
@ -695,10 +746,15 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
head.day.rows.add(row);
|
head.day.rows.add(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rows that were removed
|
||||||
|
const removedRows: IRow[] = [];
|
||||||
|
let headRemoved = false;
|
||||||
|
|
||||||
// No rows, splice everything including the header
|
// No rows, splice everything including the header
|
||||||
if (head.day.rows.size === 0) {
|
if (head.day.rows.size === 0) {
|
||||||
this.list.splice(headIdx, 1);
|
removedRows.push(...this.list.splice(headIdx, 1));
|
||||||
rowIdx = headIdx - 1;
|
rowIdx = headIdx - 1;
|
||||||
|
headRemoved = true;
|
||||||
delete this.heads[dayId];
|
delete this.heads[dayId];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -708,14 +764,34 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
spliceCount++;
|
spliceCount++;
|
||||||
}
|
}
|
||||||
if (spliceCount > 0) {
|
if (spliceCount > 0) {
|
||||||
this.list.splice(rowIdx + 1, spliceCount);
|
removedRows.push(...this.list.splice(rowIdx + 1, spliceCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update size delta for removed rows
|
||||||
|
for (const row of removedRows) {
|
||||||
|
rowSizeDelta -= row.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will be true even if the head is being spliced
|
// This will be true even if the head is being spliced
|
||||||
// because one row is always removed in that case
|
// because one row is always removed in that case
|
||||||
// So just reflow the timeline here
|
// So just reflow the timeline here
|
||||||
if (addedRow || spliceCount > 0) {
|
if (rowSizeDelta !== 0) {
|
||||||
|
if (headRemoved) {
|
||||||
|
// If the head was removed, that warrants a reflow
|
||||||
|
// since months or years might disappear!
|
||||||
this.scrollerManager.reflow();
|
this.scrollerManager.reflow();
|
||||||
|
} else {
|
||||||
|
// Otherwise just adjust the visible ticks
|
||||||
|
this.scrollerManager.adjust();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to the same actual position if the added rows
|
||||||
|
// were above the current scroll position
|
||||||
|
const recycler: any = this.$refs.recycler;
|
||||||
|
const midIndex = (recycler.$_startIndex + recycler.$_endIndex) / 2;
|
||||||
|
if (midIndex > headIdx) {
|
||||||
|
this.setScrollY(scrollY + rowSizeDelta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -815,9 +891,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
exitedLeft.forEach((photo: any) => {
|
exitedLeft.forEach((photo: any) => {
|
||||||
photo.flag &= ~this.c.FLAG_ENTER_RIGHT;
|
photo.flag &= ~this.c.FLAG_ENTER_RIGHT;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reflow timeline
|
|
||||||
this.scrollerManager.reflow();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
'p-load-fail': info.flag & c.FLAG_LOAD_FAIL,
|
'p-load-fail': info.flag & c.FLAG_LOAD_FAIL,
|
||||||
}"
|
}"
|
||||||
:key="'fpreview-' + info.fileid"
|
:key="'fpreview-' + info.fileid"
|
||||||
:src="getPreviewUrl(info.fileid, info.etag)"
|
:src="getPreviewUrl(info.fileid, info.etag, true, 256)"
|
||||||
@load="info.flag |= c.FLAG_LOADED"
|
@load="info.flag |= c.FLAG_LOADED"
|
||||||
@error="info.flag |= c.FLAG_LOAD_FAIL" />
|
@error="info.flag |= c.FLAG_LOAD_FAIL" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
'leaving': (data.flag & c.FLAG_LEAVING),
|
'leaving': (data.flag & c.FLAG_LEAVING),
|
||||||
'exit-left': (data.flag & c.FLAG_EXIT_LEFT),
|
'exit-left': (data.flag & c.FLAG_EXIT_LEFT),
|
||||||
'enter-right': (data.flag & c.FLAG_ENTER_RIGHT),
|
'enter-right': (data.flag & c.FLAG_ENTER_RIGHT),
|
||||||
|
'error': (data.flag & c.FLAG_LOAD_FAIL),
|
||||||
}">
|
}">
|
||||||
|
|
||||||
<Check :size="15" class="select"
|
<Check :size="15" class="select"
|
||||||
|
@ -81,7 +82,7 @@ export default class Photo extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
/** Get url of the photo */
|
/** Get url of the photo */
|
||||||
get url() {
|
get url() {
|
||||||
return getPreviewUrl(this.data.fileid, this.data.etag)
|
return getPreviewUrl(this.data.fileid, this.data.etag, false, 512)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Image loaded successfully */
|
/** Image loaded successfully */
|
||||||
|
@ -276,7 +277,8 @@ div.img-outer {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.selected > & { box-shadow: 0 0 3px 2px var(--color-primary); }
|
.selected > & { box-shadow: 0 0 3px 2px var(--color-primary); }
|
||||||
.p-loading > & { display: none; }
|
.p-outer.p-loading > & { display: none; }
|
||||||
|
.p-outer.error & { object-fit: contain; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -18,7 +18,7 @@
|
||||||
'p-load-fail': info.flag & c.FLAG_LOAD_FAIL,
|
'p-load-fail': info.flag & c.FLAG_LOAD_FAIL,
|
||||||
}"
|
}"
|
||||||
:key="'fpreview-' + info.fileid"
|
:key="'fpreview-' + info.fileid"
|
||||||
:src="getPreviewUrl(info.fileid, info.etag)"
|
:src="getPreviewUrl(info.fileid, info.etag, true, 256)"
|
||||||
:style="getCoverStyle(info)"
|
:style="getCoverStyle(info)"
|
||||||
@load="info.flag |= c.FLAG_LOADED"
|
@load="info.flag |= c.FLAG_LOADED"
|
||||||
@error="info.flag |= c.FLAG_LOAD_FAIL" />
|
@error="info.flag |= c.FLAG_LOAD_FAIL" />
|
||||||
|
@ -71,9 +71,9 @@ export default class Tag extends Mixins(GlobalMixin) {
|
||||||
|
|
||||||
getPreviewUrl(fileid: number, etag: string) {
|
getPreviewUrl(fileid: number, etag: string) {
|
||||||
if (this.isFace) {
|
if (this.isFace) {
|
||||||
return generateUrl(`/core/preview?fileId=${fileid}&c=${etag}&x=2048&y=2048&forceIcon=0&a=1`);
|
return getPreviewUrl(fileid, etag, false, 2048);
|
||||||
}
|
}
|
||||||
return getPreviewUrl(fileid, etag);
|
return getPreviewUrl(fileid, etag, true, 256);
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFace() {
|
get isFace() {
|
||||||
|
|
|
@ -123,8 +123,9 @@
|
||||||
return fileInfo
|
return fileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPreviewUrl = function(fileid: number, etag: string): string {
|
const getPreviewUrl = function(fileid: number, etag: string, square: boolean, size: number): string {
|
||||||
return generateUrl(`/core/preview?fileId=${fileid}&c=${etag}&x=250&y=250&forceIcon=0&a=0`);
|
const a = square ? '0' : '1'
|
||||||
|
return generateUrl(`/core/preview?fileId=${fileid}&c=${etag}&x=${size}&y=${size}&forceIcon=0&a=${a}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo, getPreviewUrl }
|
export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo, getPreviewUrl }
|
|
@ -90,6 +90,24 @@ export function binarySearch(arr: any, elem: any, key?: string) {
|
||||||
return minIndex;
|
return minIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round a number to N decimal places
|
||||||
|
* @param num Number to round
|
||||||
|
* @param places Number of decimal places
|
||||||
|
*/
|
||||||
|
export function round(num: number, places: number) {
|
||||||
|
const pow = Math.pow(10, places);
|
||||||
|
return Math.round(num * pow) / pow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round to nearest 0.5. Useful for pixels.
|
||||||
|
* @param num Number to round
|
||||||
|
*/
|
||||||
|
export function roundHalf(num: number) {
|
||||||
|
return Math.round(num * 2) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
/** Global constants */
|
/** Global constants */
|
||||||
export const constants = {
|
export const constants = {
|
||||||
c: {
|
c: {
|
||||||
|
|
|
@ -43,6 +43,8 @@ export type IPhoto = {
|
||||||
w?: number;
|
w?: number;
|
||||||
/** Height of full image */
|
/** Height of full image */
|
||||||
h?: number;
|
h?: number;
|
||||||
|
/** Grid display width percentage */
|
||||||
|
dispWp?: number;
|
||||||
/** Reference to day object */
|
/** Reference to day object */
|
||||||
d?: IDay;
|
d?: IDay;
|
||||||
/** Video flag from server */
|
/** Video flag from server */
|
||||||
|
@ -114,6 +116,8 @@ export type ITick = {
|
||||||
/** Day ID */
|
/** Day ID */
|
||||||
dayId: number;
|
dayId: number;
|
||||||
/** Display top position */
|
/** Display top position */
|
||||||
|
topF: number;
|
||||||
|
/** Display top position (truncated to 1 decimal pt) */
|
||||||
top: number;
|
top: number;
|
||||||
/** Y coordinate on recycler */
|
/** Y coordinate on recycler */
|
||||||
y: number;
|
y: number;
|
||||||
|
|
Loading…
Reference in New Issue