Merge branch 'master' into stable24

old_stable24
Varun Patil 2022-10-31 23:27:11 -07:00
commit 40e0a92850
7 changed files with 95 additions and 26 deletions

View File

@ -23,7 +23,7 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
*PREFIX*filecache f *PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders c INNER JOIN *PREFIX*cte_folders c
ON (f.parent = c.fileid ON (f.parent = c.fileid
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = "httpd/unix-directory") AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
AND f.fileid <> :excludedFolderId AND f.fileid <> :excludedFolderId
) )
)'; )';

View File

@ -61,7 +61,7 @@ trait TimelineQueryTags
$query = $this->joinFilecache($query, $folder, true, false); $query = $this->joinFilecache($query, $folder, true, false);
// GROUP and ORDER by tag name // GROUP and ORDER by tag name
$query->groupBy('st.name'); $query->groupBy('st.id');
$query->orderBy('st.name', 'ASC'); $query->orderBy('st.name', 'ASC');
$query->addOrderBy('st.id'); // tie-breaker $query->addOrderBy('st.id'); // tie-breaker

View File

@ -81,6 +81,6 @@ class Version400503Date20221101033144 extends SimpleMigrationStep
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
{ {
// Update oc_memories to set objectid equal to fileid for all rows // Update oc_memories to set objectid equal to fileid for all rows
$this->dbc->executeQuery('UPDATE *PREFIX*memories SET objectid = CAST(fileid AS CHAR)'); $this->dbc->executeQuery('UPDATE *PREFIX*memories SET objectid = CAST(fileid AS CHAR(64))');
} }
} }

View File

@ -270,6 +270,13 @@ body {
max-height: 100vh; max-height: 100vh;
} }
// Top bar is above everything else on mobile
#content-vue.has-top-bar {
@media (max-width: 1024px) {
z-index: 3000;
}
}
// Patch viewer to remove the title and // Patch viewer to remove the title and
// make the image fill the entire screen // make the image fill the entire screen
.viewer { .viewer {

View File

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div v-if="selection.size > 0" class="top-bar"> <div v-if="show" class="top-bar">
<NcActions> <NcActions :inline="1">
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Cancel')" :aria-label="t('memories', 'Cancel')"
@click="clearSelection()" @click="clearSelection()"
@ -47,7 +47,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Emit, Mixins, Prop } from "vue-property-decorator"; import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin"; import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig"; import UserConfig from "../mixins/UserConfig";
@ -88,10 +88,11 @@ type Selection = Map<number, IPhoto>;
CloseIcon, CloseIcon,
}, },
}) })
export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) { export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
@Prop() public selection: Selection;
@Prop() public heads: { [dayid: number]: IHeadRow }; @Prop() public heads: { [dayid: number]: IHeadRow };
private show = false;
private readonly selection!: Selection;
private readonly defaultActions: ISelectionAction[]; private readonly defaultActions: ISelectionAction[];
@Emit("refresh") @Emit("refresh")
@ -106,6 +107,8 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
constructor() { constructor() {
super(); super();
this.selection = new Map<number, IPhoto>();
// Make default actions // Make default actions
this.defaultActions = [ this.defaultActions = [
{ {
@ -177,6 +180,29 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
]; ];
} }
@Watch("show")
onShowChange() {
const elem = document.getElementById("content-vue");
const klass = "has-top-bar";
if (this.show) {
elem.classList.add(klass);
} else {
elem.classList.remove(klass);
}
}
private selectionChanged() {
this.show = this.selection.size > 0;
}
/** Is this fileid (or anything if not specified) selected */
public has(fileid?: number) {
if (fileid === undefined) {
return this.selection.size > 0;
}
return this.selection.has(fileid);
}
/** Click on an action */ /** Click on an action */
private async click(action: ISelectionAction) { private async click(action: ISelectionAction) {
try { try {
@ -191,7 +217,9 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
/** Get the actions list */ /** Get the actions list */
private getActions(): ISelectionAction[] { private getActions(): ISelectionAction[] {
return this.defaultActions.filter((a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)); return this.defaultActions.filter(
(a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
);
} }
/** Clear all selected photos */ /** Clear all selected photos */
@ -202,6 +230,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
photo.flag &= ~this.c.FLAG_SELECTED; photo.flag &= ~this.c.FLAG_SELECTED;
heads.add(this.heads[photo.d.dayid]); heads.add(this.heads[photo.d.dayid]);
this.selection.delete(photo.fileid); this.selection.delete(photo.fileid);
this.selectionChanged();
}); });
heads.forEach(this.updateHeadSelected); heads.forEach(this.updateHeadSelected);
this.$forceUpdate(); this.$forceUpdate();
@ -239,9 +268,17 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
if (nval) { if (nval) {
photo.flag |= this.c.FLAG_SELECTED; photo.flag |= this.c.FLAG_SELECTED;
this.selection.set(photo.fileid, photo); this.selection.set(photo.fileid, photo);
this.selectionChanged();
} else { } else {
photo.flag &= ~this.c.FLAG_SELECTED; photo.flag &= ~this.c.FLAG_SELECTED;
this.selection.delete(photo.fileid);
// Only do this if the photo in the selection set is this one.
// The problem arises when there are duplicates (e.g. face rect)
// in the list, which creates an inconsistent state if we do this.
if (this.selection.get(photo.fileid) === photo) {
this.selection.delete(photo.fileid);
this.selectionChanged();
}
} }
if (!noUpdate) { if (!noUpdate) {
@ -509,11 +546,11 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
right: 60px; right: 60px;
padding: 8px; padding: 8px;
width: 400px; width: 400px;
max-width: calc(100vw - 30px); max-width: 100vw;
background-color: var(--color-main-background); background-color: var(--color-main-background);
box-shadow: 0 0 2px gray; box-shadow: 0 0 2px gray;
border-radius: 10px; border-radius: 10px;
opacity: 0.95; opacity: 0.97;
display: flex; display: flex;
vertical-align: middle; vertical-align: middle;
z-index: 100; z-index: 100;
@ -524,9 +561,17 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
padding-left: 8px; padding-left: 8px;
} }
@media (max-width: 768px) { @media (max-width: 1024px) {
top: 35px; // sidebar is hidden below this point
right: 15px; top: 0;
left: 0;
right: unset;
position: fixed;
width: 100vw;
border-radius: 0px;
opacity: 1;
padding-top: 3px;
padding-bottom: 3px;
} }
} }
</style> </style>

View File

@ -73,7 +73,7 @@
<div <div
class="photo" class="photo"
v-for="photo of item.photos" v-for="photo of item.photos"
:key="photo.fileid" :key="photo.key || photo.fileid"
:style="{ :style="{
height: photo.dispH + 'px', height: photo.dispH + 'px',
width: photo.dispW + 'px', width: photo.dispW + 'px',
@ -116,7 +116,6 @@
<SelectionManager <SelectionManager
ref="selectionManager" ref="selectionManager"
:selection="selection"
:heads="heads" :heads="heads"
@refresh="softRefresh" @refresh="softRefresh"
@delete="deleteFromViewWithAnimation" @delete="deleteFromViewWithAnimation"
@ -153,7 +152,7 @@ import TopMatter from "./top-matter/TopMatter.vue";
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
const MOBILE_NUM_COLS = 3; // Number of columns on phone const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
@Component({ @Component({
components: { components: {
@ -204,8 +203,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
private fetchDayQueue = [] as number[]; private fetchDayQueue = [] as number[];
/** Timer to load day call */ /** Timer to load day call */
private fetchDayTimer = null as number | null; private fetchDayTimer = null as number | null;
/** Set of selected file ids */
private selection = new Map<number, IPhoto>();
/** State for request cancellations */ /** State for request cancellations */
private state = Math.random(); private state = Math.random();
@ -255,8 +252,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
return window.innerWidth <= 768; return window.innerWidth <= 768;
} }
isMobileLayout() {
return window.innerWidth <= 600;
}
allowBreakout() { allowBreakout() {
return this.isMobile() && !this.config_squareThumbs; return this.isMobileLayout() && !this.config_squareThumbs;
} }
/** Create new state */ /** Create new state */
@ -353,14 +354,13 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
return; return;
} }
if (this.isMobile()) { if (this.isMobileLayout()) {
// Mobile // Mobile
this.numCols = MOBILE_NUM_COLS; this.numCols = Math.max(3, Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT));
this.rowHeight = Math.floor(this.rowWidth / this.numCols); this.rowHeight = Math.floor(this.rowWidth / this.numCols);
} else { } else {
// Desktop // Desktop
if (this.config_squareThumbs) { if (this.config_squareThumbs) {
// Set columns first, then height
this.numCols = Math.max( this.numCols = Math.max(
3, 3,
Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT) Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT)
@ -883,7 +883,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
} }
// Force all to square // Force all to square
const squareMode = this.isMobile() || this.config_squareThumbs; const squareMode = this.isMobileLayout() || this.config_squareThumbs;
// Create justified layout with correct params // Create justified layout with correct params
const justify = getLayout( const justify = getLayout(
@ -924,6 +924,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
let rowIdx = headIdx + 1; let rowIdx = headIdx + 1;
let rowY = headY + head.size; let rowY = headY + head.size;
// Duplicate detection, e.g. for face rects
const seen = new Map<number, number>();
// Previous justified row // Previous justified row
let prevJustifyTop = justify[0]?.top || 0; let prevJustifyTop = justify[0]?.top || 0;
@ -1010,6 +1013,18 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Move to next index of photo // Move to next index of photo
dataIdx++; dataIdx++;
// Duplicate detection.
// These may be valid, e.g. in face rects. All we need to have
// is a unique Vue key for the v-for loop.
if (seen.has(photo.fileid)) {
const val = seen.get(photo.fileid);
photo.key = `${photo.fileid}-${val}`;
seen.set(photo.fileid, val + 1);
} else {
photo.key = null;
seen.set(photo.fileid, 1);
}
// Add photo to row // Add photo to row
row.photos.push(photo); row.photos.push(photo);
} }
@ -1097,7 +1112,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
clickPhoto(photo: IPhoto) { clickPhoto(photo: IPhoto) {
if (photo.flag & this.c.FLAG_PLACEHOLDER) return; if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
if (this.selection.size > 0) { if (this.selectionManager.has()) {
// selection mode // selection mode
this.selectionManager.selectPhoto(photo); this.selectionManager.selectPhoto(photo);
} else { } else {

View File

@ -35,6 +35,8 @@ export type IDay = {
export type IPhoto = { export type IPhoto = {
/** Nextcloud ID of file */ /** Nextcloud ID of file */
fileid: number; fileid: number;
/** Vue key if duplicates present (otherwise use fileid) */
key?: string;
/** Etag from server */ /** Etag from server */
etag?: string; etag?: string;
/** Path to file */ /** Path to file */