big: remove filename from IPhoto
Signed-off-by: Varun Patil <varunpatil@ucla.edu>pull/504/head
parent
1851c463c5
commit
b1df9215f9
|
@ -209,6 +209,11 @@ class ImageController extends ApiBase
|
||||||
// Inject permissions and convert to string
|
// Inject permissions and convert to string
|
||||||
$info['permissions'] = \OCA\Memories\Util::permissionsToStr($file->getPermissions());
|
$info['permissions'] = \OCA\Memories\Util::permissionsToStr($file->getPermissions());
|
||||||
|
|
||||||
|
// Inject other file parameters that are cheap to get now
|
||||||
|
$info['mimetype'] = $file->getMimeType();
|
||||||
|
$info['size'] = $file->getSize();
|
||||||
|
$info['basename'] = $file->getName();
|
||||||
|
|
||||||
return new JSONResponse($info, Http::STATUS_OK);
|
return new JSONResponse($info, Http::STATUS_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class TimelineQuery
|
||||||
|
|
||||||
public const TIMELINE_SELECT = [
|
public const TIMELINE_SELECT = [
|
||||||
'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid',
|
'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid',
|
||||||
'f.etag', 'f.path', 'f.name AS basename', 'mimetypes.mimetype',
|
'f.etag', 'f.name AS basename', 'mimetypes.mimetype',
|
||||||
];
|
];
|
||||||
|
|
||||||
protected IDBConnection $connection;
|
protected IDBConnection $connection;
|
||||||
|
|
|
@ -4,23 +4,20 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace OCA\Memories\Db;
|
namespace OCA\Memories\Db;
|
||||||
|
|
||||||
use OCA\Memories\Exif;
|
|
||||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||||
use OCP\IDBConnection;
|
use OCP\IDBConnection;
|
||||||
|
|
||||||
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive
|
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive
|
||||||
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, rootid) AS (
|
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid) AS (
|
||||||
SELECT
|
SELECT
|
||||||
f.fileid,
|
f.fileid
|
||||||
f.fileid AS rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*filecache f
|
*PREFIX*filecache f
|
||||||
WHERE
|
WHERE
|
||||||
f.fileid IN (:topFolderIds)
|
f.fileid IN (:topFolderIds)
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
f.fileid,
|
f.fileid
|
||||||
c.rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*filecache f
|
*PREFIX*filecache f
|
||||||
INNER JOIN *PREFIX*cte_folders_all c
|
INNER JOIN *PREFIX*cte_folders_all c
|
||||||
|
@ -30,8 +27,7 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
|
||||||
)
|
)
|
||||||
), *PREFIX*cte_folders AS (
|
), *PREFIX*cte_folders AS (
|
||||||
SELECT
|
SELECT
|
||||||
fileid,
|
fileid
|
||||||
MIN(rootid) AS rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*cte_folders_all
|
*PREFIX*cte_folders_all
|
||||||
GROUP BY
|
GROUP BY
|
||||||
|
@ -39,11 +35,10 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
|
||||||
)';
|
)';
|
||||||
|
|
||||||
const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the given top folders
|
const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the given top folders
|
||||||
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name, rootid) AS (
|
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name) AS (
|
||||||
SELECT
|
SELECT
|
||||||
f.fileid,
|
f.fileid,
|
||||||
f.name,
|
f.name
|
||||||
f.fileid AS rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*filecache f
|
*PREFIX*filecache f
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -51,18 +46,16 @@ const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
f.fileid,
|
f.fileid,
|
||||||
f.name,
|
f.name
|
||||||
c.rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*filecache f
|
*PREFIX*filecache f
|
||||||
INNER JOIN *PREFIX*cte_folders_all c
|
INNER JOIN *PREFIX*cte_folders_all 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\')
|
||||||
)
|
)
|
||||||
), *PREFIX*cte_folders(fileid, rootid) AS (
|
), *PREFIX*cte_folders(fileid) AS (
|
||||||
SELECT
|
SELECT
|
||||||
cfa.fileid,
|
cfa.fileid
|
||||||
MIN(cfa.rootid) AS rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*cte_folders_all cfa
|
*PREFIX*cte_folders_all cfa
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -71,8 +64,7 @@ const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the
|
||||||
cfa.fileid
|
cfa.fileid
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
f.fileid,
|
f.fileid
|
||||||
c.rootid
|
|
||||||
FROM
|
FROM
|
||||||
*PREFIX*filecache f
|
*PREFIX*filecache f
|
||||||
INNER JOIN *PREFIX*cte_folders c
|
INNER JOIN *PREFIX*cte_folders c
|
||||||
|
@ -160,11 +152,6 @@ trait TimelineQueryDays
|
||||||
// JOIN with filecache for existing files
|
// JOIN with filecache for existing files
|
||||||
$query = $this->joinFilecache($query, $root, $recursive, $archive);
|
$query = $this->joinFilecache($query, $root, $recursive, $archive);
|
||||||
|
|
||||||
// SELECT rootid if not a single folder
|
|
||||||
if ($recursive && !$root->isEmpty()) {
|
|
||||||
$query->addSelect('cte_f.rootid');
|
|
||||||
}
|
|
||||||
|
|
||||||
// JOIN with mimetypes to get the mimetype
|
// JOIN with mimetypes to get the mimetype
|
||||||
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
|
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
|
||||||
|
|
||||||
|
@ -213,56 +200,6 @@ trait TimelineQueryDays
|
||||||
*/
|
*/
|
||||||
private function processDay(array &$day, string $uid, TimelineRoot &$root)
|
private function processDay(array &$day, string $uid, TimelineRoot &$root)
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Path entry in database for folder.
|
|
||||||
* We need to splice this from the start of the file path.
|
|
||||||
*/
|
|
||||||
$internalPaths = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DAV paths for the folders.
|
|
||||||
* We need to prefix this to the start of the file path.
|
|
||||||
*/
|
|
||||||
$davPaths = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The root folder id for the folder.
|
|
||||||
* We fallback to this if rootid is not found.
|
|
||||||
*/
|
|
||||||
$defaultRootId = 0;
|
|
||||||
|
|
||||||
if (!$root->isEmpty()) {
|
|
||||||
// Get root id of the top folder
|
|
||||||
$defaultRootId = $root->getOneId();
|
|
||||||
|
|
||||||
// No way to get the internal path from the folder
|
|
||||||
$query = $this->connection->getQueryBuilder();
|
|
||||||
$query->select('fileid', 'path')
|
|
||||||
->from('filecache')
|
|
||||||
->where($query->expr()->in('fileid', $query->createNamedParameter($root->getIds(), IQueryBuilder::PARAM_INT_ARRAY)))
|
|
||||||
;
|
|
||||||
$paths = $query->executeQuery()->fetchAll();
|
|
||||||
foreach ($paths as &$path) {
|
|
||||||
$fileid = (int) $path['fileid'];
|
|
||||||
$internalPaths[$fileid] = $path['path'];
|
|
||||||
|
|
||||||
// Get DAV path.
|
|
||||||
// getPath looks like /user/files/... but we want /files/user/...
|
|
||||||
// Split at / and swap these
|
|
||||||
// For public shares, we just give the relative path
|
|
||||||
if (!empty($uid) && ($actualPath = $root->getFolderPath($fileid))) {
|
|
||||||
$actualPath = explode('/', $actualPath);
|
|
||||||
if (\count($actualPath) >= 3) {
|
|
||||||
$tmp = $actualPath[1];
|
|
||||||
$actualPath[1] = $actualPath[2];
|
|
||||||
$actualPath[2] = $tmp;
|
|
||||||
$davPath = implode('/', $actualPath);
|
|
||||||
$davPaths[$fileid] = Exif::removeExtraSlash('/'.$davPath.'/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($day as &$row) {
|
foreach ($day as &$row) {
|
||||||
// Convert field types
|
// Convert field types
|
||||||
$row['fileid'] = (int) $row['fileid'];
|
$row['fileid'] = (int) $row['fileid'];
|
||||||
|
@ -282,26 +219,12 @@ trait TimelineQueryDays
|
||||||
unset($row['liveid']);
|
unset($row['liveid']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if path exists and starts with basePath and remove
|
|
||||||
if (isset($row['path']) && !empty($row['path'])) {
|
|
||||||
$rootId = \array_key_exists('rootid', $row) ? $row['rootid'] : $defaultRootId;
|
|
||||||
$basePath = $internalPaths[$rootId] ?? '#__#';
|
|
||||||
$davPath = (\array_key_exists($rootId, $davPaths) ? $davPaths[$rootId] : null) ?: '';
|
|
||||||
|
|
||||||
if (0 === strpos($row['path'], $basePath)) {
|
|
||||||
$rpath = substr($row['path'], \strlen($basePath));
|
|
||||||
$row['filename'] = Exif::removeExtraSlash($davPath.$rpath);
|
|
||||||
}
|
|
||||||
|
|
||||||
unset($row['path']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All transform processing
|
// All transform processing
|
||||||
$this->processPeopleRecognizeDetection($row);
|
$this->processPeopleRecognizeDetection($row);
|
||||||
$this->processFaceRecognitionDetection($row);
|
$this->processFaceRecognitionDetection($row);
|
||||||
|
|
||||||
// We don't need these fields
|
// We don't need these fields
|
||||||
unset($row['datetaken'], $row['rootid']);
|
unset($row['datetaken']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $day;
|
return $day;
|
||||||
|
|
|
@ -25,8 +25,6 @@ trait TimelineQuerySingleItem
|
||||||
// JOIN with mimetypes to get the mimetype
|
// JOIN with mimetypes to get the mimetype
|
||||||
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
|
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
|
||||||
|
|
||||||
unset($row['datetaken'], $row['path']);
|
|
||||||
|
|
||||||
return $query->executeQuery()->fetch();
|
return $query->executeQuery()->fetch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,11 +200,6 @@ export default defineComponent({
|
||||||
// Register sidebar metadata tab
|
// Register sidebar metadata tab
|
||||||
const OCA = globalThis.OCA;
|
const OCA = globalThis.OCA;
|
||||||
if (OCA.Files && OCA.Files.Sidebar) {
|
if (OCA.Files && OCA.Files.Sidebar) {
|
||||||
const pf = (fileInfo) => {
|
|
||||||
fileInfo.fileid = Number(fileInfo.id);
|
|
||||||
return fileInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
OCA.Files.Sidebar.registerTab(
|
OCA.Files.Sidebar.registerTab(
|
||||||
new OCA.Files.Sidebar.Tab({
|
new OCA.Files.Sidebar.Tab({
|
||||||
id: "memories-metadata",
|
id: "memories-metadata",
|
||||||
|
@ -215,10 +210,10 @@ export default defineComponent({
|
||||||
this.metadataComponent?.$destroy?.();
|
this.metadataComponent?.$destroy?.();
|
||||||
this.metadataComponent = new Vue(Metadata as any);
|
this.metadataComponent = new Vue(Metadata as any);
|
||||||
this.metadataComponent.$mount(el);
|
this.metadataComponent.$mount(el);
|
||||||
this.metadataComponent.update(pf(fileInfo));
|
this.metadataComponent.update(Number(fileInfo.id));
|
||||||
},
|
},
|
||||||
update(fileInfo) {
|
update(fileInfo) {
|
||||||
this.metadataComponent.update(pf(fileInfo));
|
this.metadataComponent.update(Number(fileInfo.id));
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
this.metadataComponent?.$destroy?.();
|
this.metadataComponent?.$destroy?.();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="outer" v-if="fileInfo">
|
<div class="outer" v-if="fileid">
|
||||||
<div class="top-field" v-for="field of topFields" :key="field.title">
|
<div class="top-field" v-for="field of topFields" :key="field.title">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<component :is="field.icon" :size="24" />
|
<component :is="field.icon" :size="24" />
|
||||||
|
@ -62,8 +62,6 @@ import moment from "moment";
|
||||||
|
|
||||||
import * as utils from "../services/Utils";
|
import * as utils from "../services/Utils";
|
||||||
|
|
||||||
import { IFileInfo } from "../types";
|
|
||||||
|
|
||||||
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
import EditIcon from "vue-material-design-icons/Pencil.vue";
|
||||||
import CalendarIcon from "vue-material-design-icons/Calendar.vue";
|
import CalendarIcon from "vue-material-design-icons/Calendar.vue";
|
||||||
import CameraIrisIcon from "vue-material-design-icons/CameraIris.vue";
|
import CameraIrisIcon from "vue-material-design-icons/CameraIris.vue";
|
||||||
|
@ -72,6 +70,7 @@ import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
|
||||||
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
|
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
|
||||||
import TagIcon from "vue-material-design-icons/Tag.vue";
|
import TagIcon from "vue-material-design-icons/Tag.vue";
|
||||||
import { API } from "../services/API";
|
import { API } from "../services/API";
|
||||||
|
import { IImageInfo } from "../types";
|
||||||
|
|
||||||
interface TopField {
|
interface TopField {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -91,9 +90,9 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
fileInfo: null as IFileInfo,
|
fileid: null as number | null,
|
||||||
exif: {} as { [prop: string]: any },
|
exif: {} as { [prop: string]: any },
|
||||||
baseInfo: {} as any,
|
baseInfo: {} as IImageInfo,
|
||||||
state: 0,
|
state: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -246,11 +245,7 @@ export default defineComponent({
|
||||||
|
|
||||||
/** Image info */
|
/** Image info */
|
||||||
imageInfo(): string | null {
|
imageInfo(): string | null {
|
||||||
return (
|
return this.baseInfo.basename;
|
||||||
this.fileInfo?.originalBasename ||
|
|
||||||
this.fileInfo?.basename ||
|
|
||||||
(<any>this.fileInfo)?.name
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
imageInfoSub(): string[] | null {
|
imageInfoSub(): string[] | null {
|
||||||
|
@ -310,24 +305,25 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async update(fileInfo: IFileInfo) {
|
async update(fileid: number): Promise<IImageInfo> {
|
||||||
this.state = Math.random();
|
this.state = Math.random();
|
||||||
this.fileInfo = null;
|
this.fileid = null;
|
||||||
this.exif = {};
|
this.exif = {};
|
||||||
|
|
||||||
const state = this.state;
|
const state = this.state;
|
||||||
const url = API.Q(API.IMAGE_INFO(fileInfo.fileid), { tags: 1 });
|
const url = API.Q(API.IMAGE_INFO(fileid), { tags: 1 });
|
||||||
const res = await axios.get<any>(url);
|
const res = await axios.get<any>(url);
|
||||||
if (state !== this.state) return;
|
if (state !== this.state) return res.data;
|
||||||
|
|
||||||
this.fileInfo = fileInfo;
|
this.fileid = fileid;
|
||||||
this.exif = res.data.exif || {};
|
this.exif = res.data.exif || {};
|
||||||
this.baseInfo = res.data;
|
this.baseInfo = res.data;
|
||||||
|
return this.baseInfo;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleFileUpdated({ fileid }) {
|
handleFileUpdated({ fileid }) {
|
||||||
if (fileid && this.fileInfo?.id === fileid) {
|
if (fileid && this.fileid === fileid) {
|
||||||
this.update(this.fileInfo);
|
this.update(this.fileid);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||||
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
|
||||||
|
|
||||||
import Metadata from "./Metadata.vue";
|
import Metadata from "./Metadata.vue";
|
||||||
import { IFileInfo } from "../types";
|
import { IImageInfo } from "../types";
|
||||||
|
|
||||||
import CloseIcon from "vue-material-design-icons/Close.vue";
|
import CloseIcon from "vue-material-design-icons/Close.vue";
|
||||||
|
|
||||||
|
@ -61,20 +61,17 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async open(file: IFileInfo) {
|
async open(fileid: number, filename?: string, forceNative = false) {
|
||||||
if (
|
if (!this.reducedOpen && this.native() && (!fileid || forceNative)) {
|
||||||
!this.reducedOpen &&
|
|
||||||
this.native() &&
|
|
||||||
(!file.fileid || file.originalFilename?.startsWith("/files/"))
|
|
||||||
) {
|
|
||||||
this.native()?.setFullScreenMode?.(true);
|
this.native()?.setFullScreenMode?.(true);
|
||||||
this.native()?.open(file.filename);
|
this.native()?.open(filename);
|
||||||
} else {
|
} else {
|
||||||
this.reducedOpen = true;
|
this.reducedOpen = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.basename = file.originalBasename || file.basename;
|
const info: IImageInfo = await (<any>this.$refs.metadata)?.update(
|
||||||
|
fileid
|
||||||
(<any>this.$refs.metadata)?.update(file);
|
);
|
||||||
|
this.basename = info.basename;
|
||||||
emit("memories:sidebar:opened", null);
|
emit("memories:sidebar:opened", null);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -996,9 +996,7 @@ export default defineComponent({
|
||||||
head.day.detail.length === photos.length &&
|
head.day.detail.length === photos.length &&
|
||||||
head.day.detail.every(
|
head.day.detail.every(
|
||||||
(p, i) =>
|
(p, i) =>
|
||||||
p.fileid === photos[i].fileid &&
|
p.fileid === photos[i].fileid && p.etag === photos[i].etag
|
||||||
p.etag === photos[i].etag &&
|
|
||||||
p.filename === photos[i].filename
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -193,7 +193,7 @@ img {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
line-height: 1.2em;
|
line-height: 1.1em;
|
||||||
|
|
||||||
> .subtitle {
|
> .subtitle {
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
|
|
|
@ -114,10 +114,8 @@ export default defineComponent({
|
||||||
photoMap.set(photo.fileid, photo);
|
photoMap.set(photo.fileid, photo);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = await dav.getFiles(this.photos);
|
|
||||||
|
|
||||||
// Create move calls
|
// Create move calls
|
||||||
const calls = data.map((p) => async () => {
|
const calls = this.photos.map((p) => async () => {
|
||||||
try {
|
try {
|
||||||
await client.moveFile(
|
await client.moveFile(
|
||||||
`/recognize/${user}/faces/${name}/${p.fileid}-${p.basename}`,
|
`/recognize/${user}/faces/${name}/${p.fileid}-${p.basename}`,
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default defineComponent({
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.sidebar) {
|
if (this.sidebar) {
|
||||||
globalThis.mSidebar.open({ filename: this.sidebar } as any);
|
globalThis.mSidebar.open(0, this.sidebar, true);
|
||||||
|
|
||||||
// Adjust width anyway in case the sidebar is already open
|
// Adjust width anyway in case the sidebar is already open
|
||||||
this.handleAppSidebarOpen();
|
this.handleAppSidebarOpen();
|
||||||
|
|
|
@ -250,7 +250,7 @@ export default defineComponent({
|
||||||
refreshSidebar() {
|
refreshSidebar() {
|
||||||
if (this.isMobile) return;
|
if (this.isMobile) return;
|
||||||
globalThis.mSidebar.close();
|
globalThis.mSidebar.close();
|
||||||
globalThis.mSidebar.open({ filename: this.filename } as any);
|
globalThis.mSidebar.open(0, this.filename, true);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -83,7 +83,7 @@ import NcListItem from "@nextcloud/vue/dist/Components/NcListItem";
|
||||||
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
||||||
import Modal from "./Modal.vue";
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
import { IFileInfo, IPhoto } from "../../types";
|
import { IPhoto } from "../../types";
|
||||||
import { getPreviewUrl } from "../../services/FileUtils";
|
import { getPreviewUrl } from "../../services/FileUtils";
|
||||||
import { API } from "../../services/API";
|
import { API } from "../../services/API";
|
||||||
import * as dav from "../../services/DavRequests";
|
import * as dav from "../../services/DavRequests";
|
||||||
|
@ -159,14 +159,6 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFileInfo() {
|
|
||||||
if (this.$route.name.endsWith("-share")) {
|
|
||||||
return this.photo as IFileInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await dav.getFiles([this.photo]))[0];
|
|
||||||
},
|
|
||||||
|
|
||||||
async sharePreview() {
|
async sharePreview() {
|
||||||
const src = getPreviewUrl(this.photo, false, 2048);
|
const src = getPreviewUrl(this.photo, false, 2048);
|
||||||
this.shareWithHref(src, true);
|
this.shareWithHref(src, true);
|
||||||
|
@ -185,26 +177,26 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareLink() {
|
async shareLink() {
|
||||||
this.l(async () =>
|
this.l(async () => {
|
||||||
globalThis.shareNodeLink((await this.getFileInfo()).filename, true)
|
const fileInfo = (await dav.getFiles([this.photo]))[0];
|
||||||
);
|
globalThis.shareNodeLink(fileInfo.filename, true);
|
||||||
|
});
|
||||||
this.close();
|
this.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareWithHref(href: string, replaceExt = false) {
|
async shareWithHref(href: string, replaceExt = false) {
|
||||||
let fileInfo: IFileInfo, blob: Blob;
|
let blob: Blob;
|
||||||
await this.l(async () => {
|
await this.l(async () => {
|
||||||
const res = await axios.get(href, { responseType: "blob" });
|
const res = await axios.get(href, { responseType: "blob" });
|
||||||
blob = res.data;
|
blob = res.data;
|
||||||
fileInfo = await this.getFileInfo();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!blob || !fileInfo) {
|
if (!blob) {
|
||||||
showError(this.t("memories", "Failed to download and share file"));
|
showError(this.t("memories", "Failed to download file"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let basename = fileInfo.originalBasename || fileInfo.basename;
|
let basename = this.photo.basename;
|
||||||
|
|
||||||
if (replaceExt) {
|
if (replaceExt) {
|
||||||
// Fix basename extension
|
// Fix basename extension
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<ImageEditor
|
<ImageEditor
|
||||||
v-if="editorOpen"
|
v-if="editorOpen"
|
||||||
:etag="currentPhoto.etag"
|
:etag="currentPhoto.etag"
|
||||||
:src="editorDownloadLink"
|
:src="editorSrc"
|
||||||
:fileid="currentPhoto.fileid"
|
:fileid="currentPhoto.fileid"
|
||||||
@close="editorOpen = false"
|
@close="editorOpen = false"
|
||||||
/>
|
/>
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue";
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../../types";
|
import { IDay, IPhoto, IRow, IRowType } from "../../types";
|
||||||
|
|
||||||
import UserConfig from "../../mixins/UserConfig";
|
import UserConfig from "../../mixins/UserConfig";
|
||||||
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
|
||||||
|
@ -241,6 +241,7 @@ export default defineComponent({
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
originalTitle: null,
|
originalTitle: null,
|
||||||
editorOpen: false,
|
editorOpen: false,
|
||||||
|
editorSrc: "",
|
||||||
|
|
||||||
show: false,
|
show: false,
|
||||||
showControls: false,
|
showControls: false,
|
||||||
|
@ -351,14 +352,6 @@ export default defineComponent({
|
||||||
return utils.getLongDateStr(new Date(date * 1000), false, true);
|
return utils.getLongDateStr(new Date(date * 1000), false, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Get DAV download link for current photo */
|
|
||||||
editorDownloadLink(): string | null {
|
|
||||||
const filename = this.currentPhoto?.filename;
|
|
||||||
return filename
|
|
||||||
? window.location.origin + getRootUrl() + `/remote.php/dav${filename}`
|
|
||||||
: null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Show edit buttons */
|
/** Show edit buttons */
|
||||||
canEdit(): boolean {
|
canEdit(): boolean {
|
||||||
return this.currentPhoto?.imageInfo?.permissions?.includes("U");
|
return this.currentPhoto?.imageInfo?.permissions?.includes("U");
|
||||||
|
@ -897,7 +890,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openEditor() {
|
async openEditor() {
|
||||||
// Only for JPEG for now
|
// Only for JPEG for now
|
||||||
if (!this.canEdit) return;
|
if (!this.canEdit) return;
|
||||||
|
|
||||||
|
@ -909,6 +902,18 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get DAV path
|
||||||
|
const fileInfo = (await dav.getFiles([this.currentPhoto]))[0];
|
||||||
|
if (!fileInfo) {
|
||||||
|
alert(this.t("memories", "Cannot edit this file"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorSrc =
|
||||||
|
window.location.origin +
|
||||||
|
getRootUrl() +
|
||||||
|
"/remote.php/dav" +
|
||||||
|
fileInfo.originalFilename;
|
||||||
this.editorOpen = true;
|
this.editorOpen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1019,14 +1024,15 @@ export default defineComponent({
|
||||||
/** Open the sidebar */
|
/** Open the sidebar */
|
||||||
async openSidebar(photo?: IPhoto) {
|
async openSidebar(photo?: IPhoto) {
|
||||||
globalThis.mSidebar.setTab("memories-metadata");
|
globalThis.mSidebar.setTab("memories-metadata");
|
||||||
globalThis.mSidebar.open(await this.getFileInfo(photo));
|
photo ||= this.currentPhoto;
|
||||||
},
|
|
||||||
|
|
||||||
/** Get fileInfo for a photo */
|
if (this.routeIsPublic) {
|
||||||
async getFileInfo(photo?: IPhoto): Promise<IFileInfo> {
|
globalThis.mSidebar.open(photo.fileid);
|
||||||
photo = photo || this.currentPhoto;
|
} else {
|
||||||
if (this.routeIsPublic) return photo as IFileInfo;
|
const fileInfo = (await dav.getFiles([photo]))[0];
|
||||||
return (await dav.getFiles([photo]))[0];
|
const forceNative = fileInfo?.originalFilename?.startsWith("/files/");
|
||||||
|
globalThis.mSidebar.open(photo.fileid, fileInfo?.filename, forceNative);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateSizeWithoutAnim() {
|
async updateSizeWithoutAnim() {
|
||||||
|
|
|
@ -12,7 +12,7 @@ import router from "./router";
|
||||||
import { Route } from "vue-router";
|
import { Route } from "vue-router";
|
||||||
import { generateFilePath } from "@nextcloud/router";
|
import { generateFilePath } from "@nextcloud/router";
|
||||||
import { getRequestToken } from "@nextcloud/auth";
|
import { getRequestToken } from "@nextcloud/auth";
|
||||||
import { IFileInfo, IPhoto } from "./types";
|
import { IPhoto } from "./types";
|
||||||
|
|
||||||
import "./global.scss";
|
import "./global.scss";
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ declare global {
|
||||||
var shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
|
var shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
|
||||||
|
|
||||||
var mSidebar: {
|
var mSidebar: {
|
||||||
open: (filename: IFileInfo) => void;
|
open: (fileid: number, filename?: string, forceNative?: boolean) => void;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
setTab: (tab: string) => void;
|
setTab: (tab: string) => void;
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import camelcase from "camelcase";
|
import camelcase from "camelcase";
|
||||||
import { IFileInfo, IPhoto } from "../types";
|
import { IPhoto } from "../types";
|
||||||
import { API } from "./API";
|
import { API } from "./API";
|
||||||
import { isNumber } from "./NumberUtils";
|
import { isNumber } from "./NumberUtils";
|
||||||
|
|
||||||
|
@ -136,7 +136,7 @@ const genFileInfo = function (obj) {
|
||||||
|
|
||||||
/** Get preview URL from photo object */
|
/** Get preview URL from photo object */
|
||||||
const getPreviewUrl = function (
|
const getPreviewUrl = function (
|
||||||
photo: IPhoto | IFileInfo,
|
photo: IPhoto,
|
||||||
square: boolean,
|
square: boolean,
|
||||||
size: number | [number, number]
|
size: number | [number, number]
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -118,11 +118,8 @@ export async function* removeFromAlbum(
|
||||||
name: string,
|
name: string,
|
||||||
photos: IPhoto[]
|
photos: IPhoto[]
|
||||||
) {
|
) {
|
||||||
// Get files data
|
|
||||||
let fileInfos = await base.getFiles(photos);
|
|
||||||
|
|
||||||
// Add each file
|
// Add each file
|
||||||
const calls = fileInfos.map((f) => async () => {
|
const calls = photos.map((f) => async () => {
|
||||||
try {
|
try {
|
||||||
await client.deleteFile(
|
await client.deleteFile(
|
||||||
`/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`
|
`/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`
|
||||||
|
@ -131,7 +128,7 @@ export async function* removeFromAlbum(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showError(
|
showError(
|
||||||
t("memories", "Failed to remove {filename}.", {
|
t("memories", "Failed to remove {filename}.", {
|
||||||
filename: f.filename,
|
filename: f.basename,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -285,10 +282,6 @@ export function getAlbumFileInfos(
|
||||||
filename: `${collection}/${basename}`,
|
filename: `${collection}/${basename}`,
|
||||||
originalFilename: `${collection}/${basename}`,
|
originalFilename: `${collection}/${basename}`,
|
||||||
basename: basename,
|
basename: basename,
|
||||||
originalBasename: photo.basename,
|
|
||||||
mime: photo.mimetype,
|
|
||||||
hasPreview: true,
|
|
||||||
etag: photo.etag,
|
|
||||||
} as IFileInfo;
|
} as IFileInfo;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,33 +56,8 @@ export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
|
||||||
// Get file infos
|
// Get file infos
|
||||||
let fileInfos: IFileInfo[] = [];
|
let fileInfos: IFileInfo[] = [];
|
||||||
|
|
||||||
// Get all photos that already have and don't have a filename
|
|
||||||
const photosWithFilename = photos.filter((photo) => photo.filename);
|
|
||||||
fileInfos = fileInfos.concat(
|
|
||||||
photosWithFilename.map((photo) => {
|
|
||||||
const prefixPath = `/files/${getCurrentUser()?.uid}`;
|
|
||||||
return {
|
|
||||||
id: photo.fileid,
|
|
||||||
fileid: photo.fileid,
|
|
||||||
filename: photo.filename.replace(prefixPath, ""),
|
|
||||||
originalFilename: photo.filename,
|
|
||||||
basename: photo.basename,
|
|
||||||
mime: photo.mimetype,
|
|
||||||
hasPreview: true,
|
|
||||||
etag: photo.etag,
|
|
||||||
permissions: "RWD",
|
|
||||||
} as IFileInfo;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Next: get all photos that have no filename using ID
|
|
||||||
if (photosWithFilename.length === photos.length) {
|
|
||||||
return fileInfos;
|
|
||||||
}
|
|
||||||
const photosWithoutFilename = photos.filter((photo) => !photo.filename);
|
|
||||||
|
|
||||||
// Get file IDs array
|
// Get file IDs array
|
||||||
const fileIds = photosWithoutFilename.map((photo) => photo.fileid);
|
const fileIds = photos.map((photo) => photo.fileid);
|
||||||
|
|
||||||
// Divide fileIds into chunks of GET_FILE_CHUNK_SIZE
|
// Divide fileIds into chunks of GET_FILE_CHUNK_SIZE
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
|
|
|
@ -93,11 +93,8 @@ export async function* removeFaceImages(
|
||||||
name: string,
|
name: string,
|
||||||
photos: IPhoto[]
|
photos: IPhoto[]
|
||||||
) {
|
) {
|
||||||
// Get files data
|
|
||||||
let fileInfos = await base.getFiles(photos);
|
|
||||||
|
|
||||||
// Remove each file
|
// Remove each file
|
||||||
const calls = fileInfos.map((f) => async () => {
|
const calls = photos.map((f) => async () => {
|
||||||
try {
|
try {
|
||||||
await client.deleteFile(
|
await client.deleteFile(
|
||||||
`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`
|
`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`
|
||||||
|
@ -107,7 +104,7 @@ export async function* removeFaceImages(
|
||||||
console.error(e);
|
console.error(e);
|
||||||
showError(
|
showError(
|
||||||
t("memories", "Failed to remove {filename} from face.", {
|
t("memories", "Failed to remove {filename} from face.", {
|
||||||
filename: f.filename,
|
filename: f.basename,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { showError } from "@nextcloud/dialogs";
|
import { showError } from "@nextcloud/dialogs";
|
||||||
import { translate as t } from "@nextcloud/l10n";
|
import { translate as t } from "@nextcloud/l10n";
|
||||||
import { IPhoto } from "../../types";
|
import { IFileInfo, IPhoto } from "../../types";
|
||||||
import client from "../DavClient";
|
import client from "../DavClient";
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
import * as utils from "../Utils";
|
import * as utils from "../Utils";
|
||||||
|
@ -45,7 +45,7 @@ export async function* favoritePhotos(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get files data
|
// Get files data
|
||||||
let fileInfos: any[] = [];
|
let fileInfos: IFileInfo[] = [];
|
||||||
try {
|
try {
|
||||||
fileInfos = await base.getFiles(photos);
|
fileInfos = await base.getFiles(photos);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
57
src/types.ts
57
src/types.ts
|
@ -9,20 +9,6 @@ export type IFileInfo = {
|
||||||
originalFilename?: string;
|
originalFilename?: string;
|
||||||
/** Base name of file e.g. Qx0dq7dvEXA.jpg */
|
/** Base name of file e.g. Qx0dq7dvEXA.jpg */
|
||||||
basename: string;
|
basename: string;
|
||||||
/** Original base name, e.g. in albums without the file id */
|
|
||||||
originalBasename?: string;
|
|
||||||
/** Etag identifier */
|
|
||||||
etag: string;
|
|
||||||
/** File has preview available */
|
|
||||||
hasPreview: boolean;
|
|
||||||
/** File is marked favorite */
|
|
||||||
favorite?: boolean;
|
|
||||||
/** Vue flags */
|
|
||||||
flag?: number;
|
|
||||||
/** MIME type of file */
|
|
||||||
mime?: string;
|
|
||||||
/** WebDAV permissions string */
|
|
||||||
permissions?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IDay = {
|
export type IDay = {
|
||||||
|
@ -43,8 +29,6 @@ export type IPhoto = {
|
||||||
key?: string;
|
key?: string;
|
||||||
/** Etag from server */
|
/** Etag from server */
|
||||||
etag?: string;
|
etag?: string;
|
||||||
/** Path to file */
|
|
||||||
filename?: string;
|
|
||||||
/** Base name of file */
|
/** Base name of file */
|
||||||
basename?: string;
|
basename?: string;
|
||||||
/** Mime type of file */
|
/** Mime type of file */
|
||||||
|
@ -74,23 +58,7 @@ export type IPhoto = {
|
||||||
/** Reference to day object */
|
/** Reference to day object */
|
||||||
d?: IDay;
|
d?: IDay;
|
||||||
/** Reference to exif object */
|
/** Reference to exif object */
|
||||||
imageInfo?: {
|
imageInfo?: IImageInfo;
|
||||||
h: number;
|
|
||||||
w: number;
|
|
||||||
datetaken: number;
|
|
||||||
address?: string;
|
|
||||||
tags: { [id: string]: string };
|
|
||||||
permissions: string;
|
|
||||||
exif?: {
|
|
||||||
Rotation?: number;
|
|
||||||
Orientation?: number;
|
|
||||||
ImageWidth?: number;
|
|
||||||
ImageHeight?: number;
|
|
||||||
Title?: string;
|
|
||||||
Description?: string;
|
|
||||||
[other: string]: unknown;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Face detection ID */
|
/** Face detection ID */
|
||||||
faceid?: number;
|
faceid?: number;
|
||||||
|
@ -117,6 +85,29 @@ export type IPhoto = {
|
||||||
datetaken?: number;
|
datetaken?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface IImageInfo {
|
||||||
|
h: number;
|
||||||
|
w: number;
|
||||||
|
datetaken: number;
|
||||||
|
address?: string;
|
||||||
|
tags: { [id: string]: string };
|
||||||
|
|
||||||
|
permissions: string;
|
||||||
|
basename: string;
|
||||||
|
mimetype: string;
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
exif?: {
|
||||||
|
Rotation?: number;
|
||||||
|
Orientation?: number;
|
||||||
|
ImageWidth?: number;
|
||||||
|
ImageHeight?: number;
|
||||||
|
Title?: string;
|
||||||
|
Description?: string;
|
||||||
|
[other: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface IFolder extends IPhoto {
|
export interface IFolder extends IPhoto {
|
||||||
/** Path to folder */
|
/** Path to folder */
|
||||||
path: string;
|
path: string;
|
||||||
|
|
Loading…
Reference in New Issue