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
|
*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
|
||||||
)
|
)
|
||||||
)';
|
)';
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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))');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
Loading…
Reference in New Issue