Implement archive (close #38)
parent
eb60b9fb91
commit
dc2615e107
|
@ -13,12 +13,14 @@ return [
|
||||||
],
|
],
|
||||||
['name' => 'page#favorites', 'url' => '/favorites', 'verb' => 'GET'],
|
['name' => 'page#favorites', 'url' => '/favorites', 'verb' => 'GET'],
|
||||||
['name' => 'page#videos', 'url' => '/videos', 'verb' => 'GET'],
|
['name' => 'page#videos', 'url' => '/videos', 'verb' => 'GET'],
|
||||||
|
['name' => 'page#archive', 'url' => '/archive', 'verb' => 'GET'],
|
||||||
|
|
||||||
// API
|
// API
|
||||||
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
['name' => 'api#days', 'url' => '/api/days', 'verb' => 'GET'],
|
||||||
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'],
|
['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'],
|
||||||
['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'],
|
['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'],
|
||||||
|
['name' => 'api#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
|
||||||
|
|
||||||
// Config API
|
// Config API
|
||||||
['name' => 'api#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
|
['name' => 'api#setUserConfig', 'url' => '/api/config/{key}', 'verb' => 'PUT'],
|
||||||
|
|
|
@ -85,7 +85,7 @@ class ApiController extends Controller {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preload a few "day" at the start of "days" response */
|
/** Preload a few "day" at the start of "days" response */
|
||||||
private function preloadDays(array &$days, Folder &$folder, bool $recursive) {
|
private function preloadDays(array &$days, Folder &$folder, bool $recursive, bool $archive) {
|
||||||
$uid = $this->userSession->getUser()->getUID();
|
$uid = $this->userSession->getUser()->getUID();
|
||||||
$transforms = $this->getTransformations();
|
$transforms = $this->getTransformations();
|
||||||
$preloaded = 0;
|
$preloaded = 0;
|
||||||
|
@ -95,6 +95,7 @@ class ApiController extends Controller {
|
||||||
$uid,
|
$uid,
|
||||||
$day["dayid"],
|
$day["dayid"],
|
||||||
$recursive,
|
$recursive,
|
||||||
|
$archive,
|
||||||
$transforms,
|
$transforms,
|
||||||
);
|
);
|
||||||
$day["count"] = count($day["detail"]); // make sure count is accurate
|
$day["count"] = count($day["detail"]); // make sure count is accurate
|
||||||
|
@ -145,6 +146,7 @@ class ApiController extends Controller {
|
||||||
// Get the folder to show
|
// Get the folder to show
|
||||||
$folder = $this->getRequestFolder();
|
$folder = $this->getRequestFolder();
|
||||||
$recursive = is_null($this->request->getParam('folder'));
|
$recursive = is_null($this->request->getParam('folder'));
|
||||||
|
$archive = !is_null($this->request->getParam('archive'));
|
||||||
if (is_null($folder)) {
|
if (is_null($folder)) {
|
||||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
@ -154,11 +156,12 @@ class ApiController extends Controller {
|
||||||
$folder,
|
$folder,
|
||||||
$uid,
|
$uid,
|
||||||
$recursive,
|
$recursive,
|
||||||
|
$archive,
|
||||||
$this->getTransformations(),
|
$this->getTransformations(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Preload some day responses
|
// Preload some day responses
|
||||||
$this->preloadDays($list, $folder, $recursive);
|
$this->preloadDays($list, $folder, $recursive, $archive);
|
||||||
|
|
||||||
// Add subfolder info if querying non-recursively
|
// Add subfolder info if querying non-recursively
|
||||||
if (!$recursive) {
|
if (!$recursive) {
|
||||||
|
@ -183,6 +186,7 @@ class ApiController extends Controller {
|
||||||
// Get the folder to show
|
// Get the folder to show
|
||||||
$folder = $this->getRequestFolder();
|
$folder = $this->getRequestFolder();
|
||||||
$recursive = is_null($this->request->getParam('folder'));
|
$recursive = is_null($this->request->getParam('folder'));
|
||||||
|
$archive = !is_null($this->request->getParam('archive'));
|
||||||
if (is_null($folder)) {
|
if (is_null($folder)) {
|
||||||
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
return new JSONResponse([], Http::STATUS_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
@ -193,6 +197,7 @@ class ApiController extends Controller {
|
||||||
$uid,
|
$uid,
|
||||||
intval($id),
|
intval($id),
|
||||||
$recursive,
|
$recursive,
|
||||||
|
$archive,
|
||||||
$this->getTransformations(),
|
$this->getTransformations(),
|
||||||
);
|
);
|
||||||
return new JSONResponse($list, Http::STATUS_OK);
|
return new JSONResponse($list, Http::STATUS_OK);
|
||||||
|
@ -312,6 +317,114 @@ class ApiController extends Controller {
|
||||||
return $this->imageInfo($id);
|
return $this->imageInfo($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
*
|
||||||
|
* Move one file to the archive folder
|
||||||
|
* @param string fileid
|
||||||
|
*/
|
||||||
|
public function archive(string $id): JSONResponse {
|
||||||
|
$user = $this->userSession->getUser();
|
||||||
|
if (is_null($user)) {
|
||||||
|
return new JSONResponse(["message" => "Not logged in"], Http::STATUS_PRECONDITION_FAILED);
|
||||||
|
}
|
||||||
|
$uid = $user->getUID();
|
||||||
|
$userFolder = $this->rootFolder->getUserFolder($uid);
|
||||||
|
|
||||||
|
// Check for permissions and get numeric Id
|
||||||
|
$file = $userFolder->getById(intval($id));
|
||||||
|
if (count($file) === 0) {
|
||||||
|
return new JSONResponse(["message" => "No such file"], Http::STATUS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
$file = $file[0];
|
||||||
|
|
||||||
|
// Check if user has permissions
|
||||||
|
if (!$file->isUpdateable()) {
|
||||||
|
return new JSONResponse(["message" => "Cannot update this file"], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create archive folder in the root of the user's configured timeline
|
||||||
|
$timelinePath = Exif::removeExtraSlash(Exif::getPhotosPath($this->config, $uid));
|
||||||
|
$timelineFolder = $userFolder->get($timelinePath);
|
||||||
|
if (is_null($timelineFolder) || !$timelineFolder instanceof Folder) {
|
||||||
|
return new JSONResponse(["message" => "Cannot get timeline"], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
if (!$timelineFolder->isCreatable()) {
|
||||||
|
return new JSONResponse(["message" => "Cannot create archive folder"], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get path of current file relative to the timeline folder
|
||||||
|
// remove timelineFolder path from start of file path
|
||||||
|
$timelinePath = $timelineFolder->getPath(); // no trailing slash
|
||||||
|
if (substr($file->getPath(), 0, strlen($timelinePath)) !== $timelinePath) {
|
||||||
|
return new JSONResponse(["message" => "Files outside timeline cannot be archived"], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
$relativePath = substr($file->getPath(), strlen($timelinePath)); // has a leading slash
|
||||||
|
|
||||||
|
// Final path of the file including the file name
|
||||||
|
$destinationPath = '';
|
||||||
|
|
||||||
|
// Check if we want to archive or unarchive
|
||||||
|
$body = $this->request->getParams();
|
||||||
|
$unarchive = isset($body['archive']) && $body['archive'] === false;
|
||||||
|
|
||||||
|
// Get if the file is already in the archive (relativePath starts with archive)
|
||||||
|
$archiveFolderWithLeadingSlash = '/' . \OCA\Memories\Util::$ARCHIVE_FOLDER;
|
||||||
|
if (substr($relativePath, 0, strlen($archiveFolderWithLeadingSlash)) === $archiveFolderWithLeadingSlash) {
|
||||||
|
// file already in archive, remove it instead
|
||||||
|
$destinationPath = substr($relativePath, strlen($archiveFolderWithLeadingSlash));
|
||||||
|
if (!$unarchive) {
|
||||||
|
return new JSONResponse(["message" => "File already archived"], Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// file not in archive, put it in there
|
||||||
|
$destinationPath = Exif::removeExtraSlash(\OCA\Memories\Util::$ARCHIVE_FOLDER . $relativePath);
|
||||||
|
if ($unarchive) {
|
||||||
|
return new JSONResponse(["message" => "File not archived"], Http::STATUS_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the filename
|
||||||
|
$destinationFolders = explode('/', $destinationPath);
|
||||||
|
array_pop($destinationFolders);
|
||||||
|
|
||||||
|
// Create folder tree
|
||||||
|
$folder = $timelineFolder;
|
||||||
|
foreach ($destinationFolders as $folderName) {
|
||||||
|
if ($folderName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$existingFolder = $folder->get($folderName . '/');
|
||||||
|
if (!$existingFolder instanceof Folder) {
|
||||||
|
throw new \OCP\Files\NotFoundException('Not a folder');
|
||||||
|
}
|
||||||
|
$folder = $existingFolder;
|
||||||
|
} catch (\OCP\Files\NotFoundException $e) {
|
||||||
|
try {
|
||||||
|
$folder = $folder->newFolder($folderName);
|
||||||
|
} catch (\OCP\Files\NotPermittedException $e) {
|
||||||
|
return new JSONResponse(["message" => "Failed to create folder"], Http::STATUS_FORBIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move file to archive folder
|
||||||
|
try {
|
||||||
|
$file->move($folder->getPath() . '/' . $file->getName());
|
||||||
|
} catch (\OCP\Files\NotPermittedException $e) {
|
||||||
|
return new JSONResponse(["message" => "Failed to move file"], Http::STATUS_FORBIDDEN);
|
||||||
|
} catch (\OCP\Files\NotFoundException $e) {
|
||||||
|
return new JSONResponse(["message" => "File not found"], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
} catch (\OCP\Files\InvalidPathException $e) {
|
||||||
|
return new JSONResponse(["message" => "Invalid path"], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
} catch (\OCP\Lock\LockedException $e) {
|
||||||
|
return new JSONResponse(["message" => "File is locked"], Http::STATUS_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JSONResponse([], Http::STATUS_OK);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @NoAdminRequired
|
* @NoAdminRequired
|
||||||
*
|
*
|
||||||
|
|
|
@ -88,4 +88,12 @@ class PageController extends Controller {
|
||||||
public function videos() {
|
public function videos() {
|
||||||
return $this->main();
|
return $this->main();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @NoCSRFRequired
|
||||||
|
*/
|
||||||
|
public function archive() {
|
||||||
|
return $this->main();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,12 @@ trait TimelineQueryDays {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the query for oc_filecache join */
|
/** Get the query for oc_filecache join */
|
||||||
private function getFilecacheJoinQuery(IQueryBuilder &$query, Folder &$folder, bool $recursive) {
|
private function getFilecacheJoinQuery(
|
||||||
|
IQueryBuilder &$query,
|
||||||
|
Folder &$folder,
|
||||||
|
bool $recursive,
|
||||||
|
bool $archive
|
||||||
|
) {
|
||||||
// Subquery to get storage and path
|
// Subquery to get storage and path
|
||||||
$subQuery = $query->getConnection()->getQueryBuilder();
|
$subQuery = $query->getConnection()->getQueryBuilder();
|
||||||
$cursor = $subQuery->select('path', 'storage')->from('filecache')->where(
|
$cursor = $subQuery->select('path', 'storage')->from('filecache')->where(
|
||||||
|
@ -67,6 +72,19 @@ trait TimelineQueryDays {
|
||||||
}
|
}
|
||||||
$likePath = $likePath . '%';
|
$likePath = $likePath . '%';
|
||||||
$pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($likePath));
|
$pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($likePath));
|
||||||
|
|
||||||
|
// Exclude/show archive folder
|
||||||
|
$archiveLikePath = $finfo["path"] . '/' . \OCA\Memories\Util::$ARCHIVE_FOLDER . '/%';
|
||||||
|
if (!$archive) {
|
||||||
|
// Exclude archive folder
|
||||||
|
$pathQuery = $query->expr()->andX(
|
||||||
|
$pathQuery,
|
||||||
|
$query->expr()->notLike('f.path', $query->createNamedParameter($archiveLikePath))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Show only archive folder
|
||||||
|
$pathQuery = $query->expr()->like('f.path', $query->createNamedParameter($archiveLikePath));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// If getting non-recursively folder only check for parent
|
// If getting non-recursively folder only check for parent
|
||||||
$pathQuery = $query->expr()->eq('f.parent', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT));
|
$pathQuery = $query->expr()->eq('f.parent', $query->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT));
|
||||||
|
@ -87,6 +105,7 @@ trait TimelineQueryDays {
|
||||||
Folder &$folder,
|
Folder &$folder,
|
||||||
string $uid,
|
string $uid,
|
||||||
bool $recursive,
|
bool $recursive,
|
||||||
|
bool $archive,
|
||||||
array $queryTransforms = []
|
array $queryTransforms = []
|
||||||
): array {
|
): array {
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
@ -95,7 +114,7 @@ trait TimelineQueryDays {
|
||||||
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
|
$count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count');
|
||||||
$query->select('m.dayid', $count)
|
$query->select('m.dayid', $count)
|
||||||
->from('memories', 'm')
|
->from('memories', 'm')
|
||||||
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive));
|
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive));
|
||||||
|
|
||||||
// Group and sort by dayid
|
// Group and sort by dayid
|
||||||
$query->groupBy('m.dayid')
|
$query->groupBy('m.dayid')
|
||||||
|
@ -121,6 +140,7 @@ trait TimelineQueryDays {
|
||||||
string $uid,
|
string $uid,
|
||||||
int $dayid,
|
int $dayid,
|
||||||
bool $recursive,
|
bool $recursive,
|
||||||
|
bool $archive,
|
||||||
array $queryTransforms = []
|
array $queryTransforms = []
|
||||||
): array {
|
): array {
|
||||||
$query = $this->connection->getQueryBuilder();
|
$query = $this->connection->getQueryBuilder();
|
||||||
|
@ -133,7 +153,7 @@ trait TimelineQueryDays {
|
||||||
// when using DISTINCT on selected fields
|
// when using DISTINCT on selected fields
|
||||||
$query->select($fileid, 'f.etag', 'm.isvideo', 'vco.categoryid', 'm.datetaken')
|
$query->select($fileid, 'f.etag', 'm.isvideo', 'vco.categoryid', 'm.datetaken')
|
||||||
->from('memories', 'm')
|
->from('memories', 'm')
|
||||||
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive))
|
->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, $recursive, $archive))
|
||||||
->andWhere($query->expr()->eq('m.dayid', $query->createNamedParameter($dayid, IQueryBuilder::PARAM_INT)));
|
->andWhere($query->expr()->eq('m.dayid', $query->createNamedParameter($dayid, IQueryBuilder::PARAM_INT)));
|
||||||
|
|
||||||
// Add favorite field
|
// Add favorite field
|
||||||
|
|
|
@ -10,6 +10,8 @@ class Util {
|
||||||
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
public static $TAG_DAYID_START = -(1 << 30); // the world surely didn't exist
|
||||||
public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1;
|
public static $TAG_DAYID_FOLDERS = -(1 << 30) + 1;
|
||||||
|
|
||||||
|
public static $ARCHIVE_FOLDER = '.archive';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the user's configured photos directory.
|
* Get the path to the user's configured photos directory.
|
||||||
* @param IConfig $config
|
* @param IConfig $config
|
||||||
|
|
|
@ -19,6 +19,10 @@
|
||||||
:title="t('memories', 'Videos')">
|
:title="t('memories', 'Videos')">
|
||||||
<Video slot="icon" :size="20" />
|
<Video slot="icon" :size="20" />
|
||||||
</NcAppNavigationItem>
|
</NcAppNavigationItem>
|
||||||
|
<NcAppNavigationItem :to="{name: 'archive'}"
|
||||||
|
:title="t('memories', 'Archive')">
|
||||||
|
<ArchiveIcon slot="icon" :size="20" />
|
||||||
|
</NcAppNavigationItem>
|
||||||
</template>
|
</template>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NcAppNavigationSettings :title="t('memories', 'Settings')">
|
<NcAppNavigationSettings :title="t('memories', 'Settings')">
|
||||||
|
@ -64,6 +68,7 @@ import ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'
|
||||||
import FolderIcon from 'vue-material-design-icons/Folder.vue'
|
import FolderIcon from 'vue-material-design-icons/Folder.vue'
|
||||||
import Star from 'vue-material-design-icons/Star.vue'
|
import Star from 'vue-material-design-icons/Star.vue'
|
||||||
import Video from 'vue-material-design-icons/Video.vue'
|
import Video from 'vue-material-design-icons/Video.vue'
|
||||||
|
import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -80,6 +85,7 @@ import Video from 'vue-material-design-icons/Video.vue'
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
Star,
|
Star,
|
||||||
Video,
|
Video,
|
||||||
|
ArchiveIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class App extends Mixins(GlobalMixin) {
|
export default class App extends Mixins(GlobalMixin) {
|
||||||
|
|
|
@ -111,6 +111,25 @@
|
||||||
{{ t('memories', 'Favorite') }}
|
{{ t('memories', 'Favorite') }}
|
||||||
<template #icon> <Star :size="20" /> </template>
|
<template #icon> <Star :size="20" /> </template>
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NcActionButton
|
||||||
|
v-if="!routeIsArchive()"
|
||||||
|
:aria-label="t('memories', 'Archive')"
|
||||||
|
@click="archiveSelection" close-after-click>
|
||||||
|
{{ t('memories', 'Archive') }}
|
||||||
|
<template #icon> <ArchiveIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
|
<NcActionButton
|
||||||
|
v-else
|
||||||
|
:aria-label="t('memories', 'Unarchive')"
|
||||||
|
@click="archiveSelection" close-after-click>
|
||||||
|
{{ t('memories', 'Unarchive') }}
|
||||||
|
<template #icon> <UnarchiveIcon :size="20" /> </template>
|
||||||
|
</NcActionButton>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<NcActionButton
|
<NcActionButton
|
||||||
:aria-label="t('memories', 'Edit Date/Time')"
|
:aria-label="t('memories', 'Edit Date/Time')"
|
||||||
@click="editDateSelection" close-after-click>
|
@click="editDateSelection" close-after-click>
|
||||||
|
@ -147,6 +166,8 @@ import Delete from 'vue-material-design-icons/Delete.vue';
|
||||||
import Close from 'vue-material-design-icons/Close.vue';
|
import Close from 'vue-material-design-icons/Close.vue';
|
||||||
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
|
||||||
import EditIcon from 'vue-material-design-icons/ClockEdit.vue';
|
import EditIcon from 'vue-material-design-icons/ClockEdit.vue';
|
||||||
|
import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
|
||||||
|
import UnarchiveIcon from 'vue-material-design-icons/PackageUp.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 MAX_PHOTO_WIDTH = 175; // Max width of a photo
|
||||||
|
@ -177,6 +198,8 @@ for (const [key, value] of Object.entries(API_ROUTES)) {
|
||||||
Close,
|
Close,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
EditIcon,
|
EditIcon,
|
||||||
|
ArchiveIcon,
|
||||||
|
UnarchiveIcon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
|
@ -468,6 +491,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
query.set('folder', this.$route.params.path || '/');
|
query.set('folder', this.$route.params.path || '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Archive
|
||||||
|
if (this.routeIsArchive()) {
|
||||||
|
query.set('archive', '1');
|
||||||
|
}
|
||||||
|
|
||||||
// Create query string and append to URL
|
// Create query string and append to URL
|
||||||
const queryStr = query.toString();
|
const queryStr = query.toString();
|
||||||
if (queryStr) {
|
if (queryStr) {
|
||||||
|
@ -476,6 +504,11 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Is archive route */
|
||||||
|
routeIsArchive() {
|
||||||
|
return this.$route.name === 'archive';
|
||||||
|
}
|
||||||
|
|
||||||
/** Get name of header */
|
/** Get name of header */
|
||||||
getHeadName(head: IHeadRow) {
|
getHeadName(head: IHeadRow) {
|
||||||
// Check cache
|
// Check cache
|
||||||
|
@ -1090,6 +1123,29 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
|
||||||
(<any>this.$refs.editDate).open(Array.from(this.selection.values()));
|
(<any>this.$refs.editDate).open(Array.from(this.selection.values()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive the currently selected photos
|
||||||
|
*/
|
||||||
|
async archiveSelection() {
|
||||||
|
if (this.selection.size >= 100) {
|
||||||
|
if (!confirm(this.t("memories", "You are about to touch a large number of files. Are you sure?"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.loading++;
|
||||||
|
for await (const delIds of dav.archiveFilesByIds(Array.from(this.selection.keys()), !this.routeIsArchive())) {
|
||||||
|
const delPhotos = delIds.map(id => this.selection.get(id));
|
||||||
|
await this.deleteFromViewWithAnimation(delPhotos);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete elements from main view with some animation
|
* Delete elements from main view with some animation
|
||||||
* This function looks horribly slow, probably isn't that bad
|
* This function looks horribly slow, probably isn't that bad
|
||||||
|
|
|
@ -81,5 +81,14 @@
|
||||||
rootTitle: t('memories', 'Videos'),
|
rootTitle: t('memories', 'Videos'),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/archive',
|
||||||
|
component: Timeline,
|
||||||
|
name: 'archive',
|
||||||
|
props: route => ({
|
||||||
|
rootTitle: t('memories', 'Archive'),
|
||||||
|
}),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
|
@ -217,6 +217,43 @@ export async function* deleteFilesByIds(fileIds: number[]) {
|
||||||
yield* runInParallel(calls, 10);
|
yield* runInParallel(calls, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive or unarchive a single file
|
||||||
|
*
|
||||||
|
* @param fileid File id
|
||||||
|
* @param archive Archive or unarchive
|
||||||
|
*/
|
||||||
|
export async function archiveFile(fileid: number, archive: boolean) {
|
||||||
|
return await axios.patch(generateUrl('/apps/memories/api/archive/{fileid}', { fileid }), { archive });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Archive all files in a given list of Ids
|
||||||
|
*
|
||||||
|
* @param fileIds list of file ids
|
||||||
|
* @param archive Archive or unarchive
|
||||||
|
* @returns list of file ids that were deleted
|
||||||
|
*/
|
||||||
|
export async function* archiveFilesByIds(fileIds: number[], archive: boolean) {
|
||||||
|
if (fileIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Archive each file
|
||||||
|
const calls = fileIds.map((id) => async () => {
|
||||||
|
try {
|
||||||
|
await archiveFile(id, archive);
|
||||||
|
return id as number;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to archive', id, error);
|
||||||
|
showError(t('memories', 'Failed to archive some files.'));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
yield* runInParallel(calls, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a file
|
* Download a file
|
||||||
|
|
Loading…
Reference in New Issue