Merge branch 'master' into stable24
commit
40e0a92850
|
@ -23,7 +23,7 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
|
|||
*PREFIX*filecache f
|
||||
INNER JOIN *PREFIX*cte_folders c
|
||||
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
|
||||
)
|
||||
)';
|
||||
|
|
|
@ -61,7 +61,7 @@ trait TimelineQueryTags
|
|||
$query = $this->joinFilecache($query, $folder, true, false);
|
||||
|
||||
// GROUP and ORDER by tag name
|
||||
$query->groupBy('st.name');
|
||||
$query->groupBy('st.id');
|
||||
$query->orderBy('st.name', 'ASC');
|
||||
$query->addOrderBy('st.id'); // tie-breaker
|
||||
|
||||
|
|
|
@ -81,6 +81,6 @@ class Version400503Date20221101033144 extends SimpleMigrationStep
|
|||
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
|
||||
{
|
||||
// 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))');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -270,6 +270,13 @@ body {
|
|||
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
|
||||
// make the image fill the entire screen
|
||||
.viewer {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="selection.size > 0" class="top-bar">
|
||||
<NcActions>
|
||||
<div v-if="show" class="top-bar">
|
||||
<NcActions :inline="1">
|
||||
<NcActionButton
|
||||
:aria-label="t('memories', 'Cancel')"
|
||||
@click="clearSelection()"
|
||||
|
@ -47,7 +47,7 @@
|
|||
</template>
|
||||
|
||||
<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 UserConfig from "../mixins/UserConfig";
|
||||
|
||||
|
@ -88,10 +88,11 @@ type Selection = Map<number, IPhoto>;
|
|||
CloseIcon,
|
||||
},
|
||||
})
|
||||
export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
||||
@Prop() public selection: Selection;
|
||||
export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
|
||||
@Prop() public heads: { [dayid: number]: IHeadRow };
|
||||
|
||||
private show = false;
|
||||
private readonly selection!: Selection;
|
||||
private readonly defaultActions: ISelectionAction[];
|
||||
|
||||
@Emit("refresh")
|
||||
|
@ -106,6 +107,8 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
constructor() {
|
||||
super();
|
||||
|
||||
this.selection = new Map<number, IPhoto>();
|
||||
|
||||
// Make default actions
|
||||
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 */
|
||||
private async click(action: ISelectionAction) {
|
||||
try {
|
||||
|
@ -191,7 +217,9 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
|
||||
/** Get the actions list */
|
||||
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 */
|
||||
|
@ -202,6 +230,7 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
photo.flag &= ~this.c.FLAG_SELECTED;
|
||||
heads.add(this.heads[photo.d.dayid]);
|
||||
this.selection.delete(photo.fileid);
|
||||
this.selectionChanged();
|
||||
});
|
||||
heads.forEach(this.updateHeadSelected);
|
||||
this.$forceUpdate();
|
||||
|
@ -239,9 +268,17 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
if (nval) {
|
||||
photo.flag |= this.c.FLAG_SELECTED;
|
||||
this.selection.set(photo.fileid, photo);
|
||||
this.selectionChanged();
|
||||
} else {
|
||||
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) {
|
||||
|
@ -509,11 +546,11 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
right: 60px;
|
||||
padding: 8px;
|
||||
width: 400px;
|
||||
max-width: calc(100vw - 30px);
|
||||
max-width: 100vw;
|
||||
background-color: var(--color-main-background);
|
||||
box-shadow: 0 0 2px gray;
|
||||
border-radius: 10px;
|
||||
opacity: 0.95;
|
||||
opacity: 0.97;
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
z-index: 100;
|
||||
|
@ -524,9 +561,17 @@ export default class SelectionHandler extends Mixins(GlobalMixin, UserConfig) {
|
|||
padding-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
top: 35px;
|
||||
right: 15px;
|
||||
@media (max-width: 1024px) {
|
||||
// sidebar is hidden below this point
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: unset;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
border-radius: 0px;
|
||||
opacity: 1;
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
<div
|
||||
class="photo"
|
||||
v-for="photo of item.photos"
|
||||
:key="photo.fileid"
|
||||
:key="photo.key || photo.fileid"
|
||||
:style="{
|
||||
height: photo.dispH + 'px',
|
||||
width: photo.dispW + 'px',
|
||||
|
@ -116,7 +116,6 @@
|
|||
|
||||
<SelectionManager
|
||||
ref="selectionManager"
|
||||
:selection="selection"
|
||||
:heads="heads"
|
||||
@refresh="softRefresh"
|
||||
@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 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({
|
||||
components: {
|
||||
|
@ -204,8 +203,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
private fetchDayQueue = [] as number[];
|
||||
/** Timer to load day call */
|
||||
private fetchDayTimer = null as number | null;
|
||||
/** Set of selected file ids */
|
||||
private selection = new Map<number, IPhoto>();
|
||||
|
||||
/** State for request cancellations */
|
||||
private state = Math.random();
|
||||
|
@ -255,8 +252,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
return window.innerWidth <= 768;
|
||||
}
|
||||
|
||||
isMobileLayout() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
allowBreakout() {
|
||||
return this.isMobile() && !this.config_squareThumbs;
|
||||
return this.isMobileLayout() && !this.config_squareThumbs;
|
||||
}
|
||||
|
||||
/** Create new state */
|
||||
|
@ -353,14 +354,13 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.isMobile()) {
|
||||
if (this.isMobileLayout()) {
|
||||
// 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);
|
||||
} else {
|
||||
// Desktop
|
||||
if (this.config_squareThumbs) {
|
||||
// Set columns first, then height
|
||||
this.numCols = Math.max(
|
||||
3,
|
||||
Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT)
|
||||
|
@ -883,7 +883,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
}
|
||||
|
||||
// Force all to square
|
||||
const squareMode = this.isMobile() || this.config_squareThumbs;
|
||||
const squareMode = this.isMobileLayout() || this.config_squareThumbs;
|
||||
|
||||
// Create justified layout with correct params
|
||||
const justify = getLayout(
|
||||
|
@ -924,6 +924,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
let rowIdx = headIdx + 1;
|
||||
let rowY = headY + head.size;
|
||||
|
||||
// Duplicate detection, e.g. for face rects
|
||||
const seen = new Map<number, number>();
|
||||
|
||||
// Previous justified row
|
||||
let prevJustifyTop = justify[0]?.top || 0;
|
||||
|
||||
|
@ -1010,6 +1013,18 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
// Move to next index of photo
|
||||
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
|
||||
row.photos.push(photo);
|
||||
}
|
||||
|
@ -1097,7 +1112,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
|||
clickPhoto(photo: IPhoto) {
|
||||
if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
|
||||
|
||||
if (this.selection.size > 0) {
|
||||
if (this.selectionManager.has()) {
|
||||
// selection mode
|
||||
this.selectionManager.selectPhoto(photo);
|
||||
} else {
|
||||
|
|
|
@ -35,6 +35,8 @@ export type IDay = {
|
|||
export type IPhoto = {
|
||||
/** Nextcloud ID of file */
|
||||
fileid: number;
|
||||
/** Vue key if duplicates present (otherwise use fileid) */
|
||||
key?: string;
|
||||
/** Etag from server */
|
||||
etag?: string;
|
||||
/** Path to file */
|
||||
|
|
Loading…
Reference in New Issue