big: switch to justified layout

cache
Varun Patil 2022-10-15 19:55:53 -07:00
parent 4e98e93d6e
commit e298ef97fa
11 changed files with 261 additions and 89 deletions

11
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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%;

View File

@ -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) /** Recreate from scratch */
const extraY = this.recyclerBefore?.clientHeight || 0; private recreate() {
// Clear
this.ticks = [];
// Compute tick positions // Ticks
for (const tick of this.ticks) { let y = 0;
tick.top = (extraY + tick.y) * (this.height / this.recyclerHeight); 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 // Check if we hit the next tick
const dtYear = dateTaken.getUTCFullYear(); const tick = this.ticks[tickId];
const dtMonth = dateTaken.getUTCMonth() if (tick.dayId === row.dayId) {
if (Number.isInteger(row.dayId) && (dtMonth !== prevMonth || dtYear !== prevYear)) { tick.y = y;
const text = (dtYear === prevYear || dtYear === thisYear) ? undefined : dtYear; this.setTickTop(tick);
this.ticks.push(getTick(row.dayId, text));
} // Get the next visible tick
prevMonth = dtMonth; tickId++;
prevYear = dtYear; 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) {

View File

@ -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) {
this.scrollerManager.reflow(); if (headRemoved) {
// If the head was removed, that warrants a reflow
// since months or years might disappear!
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>

View File

@ -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>

View File

@ -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>

View File

@ -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() {

View File

@ -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 }

View File

@ -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: {

View File

@ -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;