Merge branch 'master' into stable24

old_stable24
Varun Patil 2022-11-06 00:00:39 -07:00
commit 37501bcbdf
28 changed files with 1368 additions and 237 deletions

View File

@ -2,6 +2,17 @@
This file is manually updated. Please file an issue if something is missing.
## v4.6.0, v3.6.0 (2022-11-06)
- **Brand new photo viewer** with improved touch interface and UX
- Improvements from v4.5.4 below
- Known regressions: Photo Editor and Slideshow are not implemented yet
## v4.5.4, v3.5.4 (skipped)
- New layout for Albums view (date ascending, grouped by month)
- Re-enable viewer editing and deletion
## v4.5.2, v3.5.2 (2022-10-30)
- Improved scroller performance

View File

@ -133,6 +133,26 @@ class ApiBase extends Controller
return $folder;
}
protected function isRecursive()
{
return null === $this->request->getParam('folder');
}
protected function isArchive()
{
return null !== $this->request->getParam('archive');
}
protected function isMonthView()
{
return null !== $this->request->getParam('monthView');
}
protected function isReverse()
{
return null !== $this->request->getParam('reverse');
}
protected function getShareToken()
{
return $this->request->getParam('folder_share');

View File

@ -49,25 +49,31 @@ class DaysController extends ApiBase
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
// Params
$recursive = null === $this->request->getParam('folder');
$archive = null !== $this->request->getParam('archive');
// Run actual query
try {
$list = $this->timelineQuery->getDays(
$folder,
$uid,
$recursive,
$archive,
$this->isRecursive(),
$this->isArchive(),
$this->getTransformations(true),
);
// Preload some day responses
$this->preloadDays($list, $uid, $folder, $recursive, $archive);
if ($this->isMonthView()) {
// Group days together into months
$list = $this->timelineQuery->daysToMonths($list);
} else {
// Preload some day responses
$this->preloadDays($list, $uid, $folder);
}
// Reverse response if requested. Folders still stay at top.
if ($this->isReverse()) {
$list = array_reverse($list);
}
// Add subfolder info if querying non-recursively
if (!$recursive) {
if (!$this->isRecursive()) {
array_unshift($list, $this->getSubfoldersEntry($folder));
}
@ -88,18 +94,18 @@ class DaysController extends ApiBase
$uid = $this->getUid();
// Check for wildcard
$day_ids = [];
$dayIds = [];
if ('*' === $id) {
$day_ids = null;
$dayIds = null;
} else {
// Split at commas and convert all parts to int
$day_ids = array_map(function ($part) {
$dayIds = array_map(function ($part) {
return (int) $part;
}, explode(',', $id));
}
// Check if $day_ids is empty
if (null !== $day_ids && 0 === \count($day_ids)) {
// Check if $dayIds is empty
if (null !== $dayIds && 0 === \count($dayIds)) {
return new JSONResponse([], Http::STATUS_OK);
}
@ -112,21 +118,34 @@ class DaysController extends ApiBase
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
// Params
$recursive = null === $this->request->getParam('folder');
$archive = null !== $this->request->getParam('archive');
// Convert to actual dayIds if month view
if ($this->isMonthView()) {
$dayIds = $this->timelineQuery->monthIdToDayIds($dayIds[0]);
}
// Run actual query
try {
$list = $this->timelineQuery->getDay(
$folder,
$uid,
$day_ids,
$recursive,
$archive,
$dayIds,
$this->isRecursive(),
$this->isArchive(),
$this->getTransformations(false),
);
// Force month id for dayId for month view
if ($this->isMonthView()) {
foreach ($list as &$photo) {
$photo['dayid'] = (int) $dayIds[0];
}
}
// Reverse response if requested.
if ($this->isReverse()) {
$list = array_reverse($list);
}
return new JSONResponse($list, Http::STATUS_OK);
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_INTERNAL_SERVER_ERROR);
@ -252,13 +271,11 @@ class DaysController extends ApiBase
/**
* Preload a few "day" at the start of "days" response.
*
* @param array $days the days array
* @param string $uid User ID or blank for public shares
* @param null|Folder $folder the folder to search in
* @param bool $recursive search in subfolders
* @param bool $archive search in archive folder only
* @param array $days the days array
* @param string $uid User ID or blank for public shares
* @param null|Folder $folder the folder to search in
*/
private function preloadDays(array &$days, string $uid, &$folder, bool $recursive, bool $archive)
private function preloadDays(array &$days, string $uid, &$folder)
{
$transforms = $this->getTransformations(false);
$preloaded = 0;
@ -283,8 +300,8 @@ class DaysController extends ApiBase
$folder,
$uid,
$preloadDayIds,
$recursive,
$archive,
$this->isRecursive(),
$this->isArchive(),
$transforms,
);

View File

@ -4,7 +4,6 @@ namespace OCA\Memories\Controller;
use OCA\Files\Event\LoadSidebar;
use OCA\Memories\AppInfo\Application;
use OCA\Viewer\Event\LoadViewer;
use OCP\App\IAppManager;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\ContentSecurityPolicy;
@ -62,7 +61,6 @@ class PageController extends Controller
// Scripts
Util::addScript($this->appName, 'memories-main');
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadViewer());
// Configuration
$uid = $user->getUid();
@ -114,7 +112,6 @@ class PageController extends Controller
// Scripts
Util::addScript($this->appName, 'memories-main');
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
$this->eventDispatcher->dispatchTyped(new LoadViewer());
// App version
$this->initialState->provideInitialState('version', $this->appManager->getAppInfo('memories')['version']);

View File

@ -78,6 +78,44 @@ trait TimelineQueryAlbums
return $albums;
}
/**
* Convert days response to months response.
* The dayId is used to group the days into months.
*/
public function daysToMonths(array &$days)
{
$months = [];
foreach ($days as &$day) {
$dayId = $day['dayid'];
$time = $dayId * 86400;
$monthid = strtotime(date('Ym', $time).'01') / 86400;
if (empty($months) || $months[\count($months) - 1]['dayid'] !== $monthid) {
$months[] = [
'dayid' => $monthid,
'count' => 0,
];
}
$months[\count($months) - 1]['count'] += $day['count'];
}
return $months;
}
/** Convert list of month IDs to list of dayIds */
public function monthIdToDayIds(int $monthId)
{
$dayIds = [];
$firstDay = (int) $monthId;
$lastDay = strtotime(date('Ymt', $firstDay * 86400)) / 86400;
for ($i = $firstDay; $i <= $lastDay; ++$i) {
$dayIds[] = (string) $i;
}
return $dayIds;
}
/**
* Get album if allowed. Also check if album is shared with user.
*

View File

@ -78,7 +78,7 @@ trait TimelineQueryDays
*
* @param null|Folder $folder The folder to get the day from
* @param string $uid The user id
* @param int[] $dayid The day id
* @param int[] $day_ids The day ids to fetch
* @param bool $recursive If the query should be recursive
* @param bool $archive If the query should include only the archive folder
* @param array $queryTransforms The query transformations to apply

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
@ -32,11 +31,11 @@ use OCP\Migration\SimpleMigrationStep;
class Version000000Date20220812163631 extends SimpleMigrationStep
{
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options)
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options)
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
@ -43,9 +42,9 @@ class Version200000Date20220924015634 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -59,9 +58,9 @@ class Version200000Date20220924015634 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -85,9 +84,9 @@ class Version200000Date20220924015634 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
}

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
@ -35,16 +34,16 @@ use OCP\Migration\SimpleMigrationStep;
class Version400000Date20221015121115 extends SimpleMigrationStep
{
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -68,9 +67,9 @@ class Version400000Date20221015121115 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
}

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@ -34,16 +33,16 @@ use OCP\Migration\SimpleMigrationStep;
class Version400307Date20221025002524 extends SimpleMigrationStep
{
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -59,9 +58,9 @@ class Version400307Date20221025002524 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
}

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
@ -34,16 +33,16 @@ use OCP\Migration\SimpleMigrationStep;
class Version400308Date20221026151748 extends SimpleMigrationStep
{
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -57,9 +56,9 @@ class Version400308Date20221026151748 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
}

View File

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace OCA\Memories\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
@ -43,16 +42,16 @@ class Version400503Date20221101033144 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
{
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
{
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
@ -76,9 +75,9 @@ class Version400503Date20221101033144 extends SimpleMigrationStep
}
/**
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
*/
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
$this->dbc->executeQuery('UPDATE *PREFIX*memories SET objectid = CAST(fileid AS CHAR(64))');

408
package-lock.json generated
View File

@ -17,7 +17,9 @@
"justified-layout": "^4.1.0",
"moment": "^2.29.4",
"path-posix": "^1.0.0",
"photoswipe": "^5.3.3",
"reflect-metadata": "^0.1.13",
"video.js": "^7.20.3",
"vue": "^2.7.10",
"vue-class-component": "^7.2.6",
"vue-material-design-icons": "^5.1.2",
@ -2334,6 +2336,52 @@
"@types/node": "*"
}
},
"node_modules/@videojs/http-streaming": {
"version": "2.14.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.14.3.tgz",
"integrity": "sha512-2tFwxCaNbcEZzQugWf8EERwNMyNtspfHnvxRGRABQs09W/5SqmkWFuGWfUAm4wQKlXGfdPyAJ1338ASl459xAA==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.5",
"aes-decrypter": "3.1.3",
"global": "^4.4.0",
"m3u8-parser": "4.7.1",
"mpd-parser": "0.21.1",
"mux.js": "6.0.1",
"video.js": "^6 || ^7"
},
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"video.js": "^6 || ^7"
}
},
"node_modules/@videojs/vhs-utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/@videojs/xhr": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz",
"integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
@ -2589,6 +2637,14 @@
}
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -2652,6 +2708,17 @@
"acorn": "^8"
}
},
"node_modules/aes-decrypter": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz",
"integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -3990,6 +4057,11 @@
"node": ">=6"
}
},
"node_modules/dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/domain-browser": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
@ -4818,6 +4890,15 @@
"node": "*"
}
},
"node_modules/global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"dependencies": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -5217,6 +5298,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -5395,6 +5481,11 @@
"node": ">=0.10.0"
}
},
"node_modules/is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
},
"node_modules/is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@ -5893,6 +5984,11 @@
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
},
"node_modules/keycode": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
},
"node_modules/kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -6018,6 +6114,16 @@
"yallist": "^2.1.2"
}
},
"node_modules/m3u8-parser": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz",
"integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0"
}
},
"node_modules/magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@ -6202,6 +6308,14 @@
"node": ">=6"
}
},
"node_modules/min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
"dependencies": {
"dom-walk": "^0.1.0"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -6245,6 +6359,20 @@
"node": "*"
}
},
"node_modules/mpd-parser": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
"integrity": "sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0"
},
"bin": {
"mpd-to-m3u8-json": "bin/parse.js"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -6265,6 +6393,22 @@
"multicast-dns": "cli.js"
}
},
"node_modules/mux.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
},
"bin": {
"muxjs-transmux": "bin/transmux.js"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@ -6673,6 +6817,14 @@
"node": ">=0.12"
}
},
"node_modules/photoswipe": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q==",
"engines": {
"node": ">= 0.12.0"
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -6690,6 +6842,17 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"dependencies": {
"@babel/runtime": "^7.5.5"
},
"bin": {
"pkcs7": "bin/cli.js"
}
},
"node_modules/pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@ -6871,8 +7034,6 @@
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 0.6.0"
}
@ -7349,6 +7510,14 @@
"node": ">=8"
}
},
"node_modules/rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==",
"dependencies": {
"individual": "^2.0.0"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -7369,6 +7538,14 @@
}
]
},
"node_modules/safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==",
"dependencies": {
"rust-result": "^1.0.0"
}
},
"node_modules/safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
@ -8615,6 +8792,11 @@
"requires-port": "^1.0.0"
}
},
"node_modules/url-toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
},
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@ -8680,6 +8862,39 @@
"node": ">= 0.8"
}
},
"node_modules/video.js": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.20.3.tgz",
"integrity": "sha512-JMspxaK74LdfWcv69XWhX4rILywz/eInOVPdKefpQiZJSMD5O8xXYueqACP2Q5yqKstycgmmEKlJzZ+kVmDciw==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.14.3",
"@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.3",
"global": "^4.4.0",
"keycode": "^2.2.0",
"m3u8-parser": "4.7.1",
"mpd-parser": "0.21.1",
"mux.js": "6.0.1",
"safe-json-parse": "4.0.0",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.4"
}
},
"node_modules/videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"node_modules/videojs-vtt.js": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz",
"integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==",
"dependencies": {
"global": "^4.3.1"
}
},
"node_modules/vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@ -11414,6 +11629,41 @@
"@types/node": "*"
}
},
"@videojs/http-streaming": {
"version": "2.14.3",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.14.3.tgz",
"integrity": "sha512-2tFwxCaNbcEZzQugWf8EERwNMyNtspfHnvxRGRABQs09W/5SqmkWFuGWfUAm4wQKlXGfdPyAJ1338ASl459xAA==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.5",
"aes-decrypter": "3.1.3",
"global": "^4.4.0",
"m3u8-parser": "4.7.1",
"mpd-parser": "0.21.1",
"mux.js": "6.0.1",
"video.js": "^6 || ^7"
}
},
"@videojs/vhs-utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"requires": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
}
},
"@videojs/xhr": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz",
"integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
"requires": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
}
},
"@vue/compiler-sfc": {
"version": "2.7.13",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.13.tgz",
@ -11649,6 +11899,11 @@
"peer": true,
"requires": {}
},
"@xmldom/xmldom": {
"version": "0.7.9",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.9.tgz",
"integrity": "sha512-yceMpm/xd4W2a85iqZyO09gTnHvXF6pyiWjD2jcOJs7hRoZtNNOO1eJlhHj1ixA+xip2hOyGn+LgcvLCMo5zXA=="
},
"@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@ -11698,6 +11953,17 @@
"peer": true,
"requires": {}
},
"aes-decrypter": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz",
"integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
}
},
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -12765,6 +13031,11 @@
"@leichtgewicht/ip-codec": "^2.0.1"
}
},
"dom-walk": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"domain-browser": {
"version": "4.22.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
@ -13423,6 +13694,15 @@
"dev": true,
"peer": true
},
"global": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
"integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
"requires": {
"min-document": "^2.19.0",
"process": "^0.11.10"
}
},
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@ -13736,6 +14016,11 @@
"resolve-cwd": "^3.0.0"
}
},
"individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha512-pWt8hBCqJsUWI/HtcfWod7+N9SgAqyPEaF7JQjwzjn5vGrpg6aQ5qeAFQ7dx//UH4J1O+7xqew+gCeeFt6xN/g=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -13860,6 +14145,11 @@
"dev": true,
"peer": true
},
"is-function": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
"integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ=="
},
"is-generator-function": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
@ -14216,6 +14506,11 @@
"resolved": "https://registry.npmjs.org/justified-layout/-/justified-layout-4.1.0.tgz",
"integrity": "sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg=="
},
"keycode": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
},
"kind-of": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
@ -14321,6 +14616,16 @@
"yallist": "^2.1.2"
}
},
"m3u8-parser": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.1.tgz",
"integrity": "sha512-pbrQwiMiq+MmI9bl7UjtPT3AK603PV9bogNlr83uC+X9IoxqL5E4k7kU7fMQ0dpRgxgeSMygqUa0IMLQNXLBNA==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"global": "^4.4.0"
}
},
"magic-string": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
@ -14471,6 +14776,14 @@
"dev": true,
"peer": true
},
"min-document": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz",
"integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==",
"requires": {
"dom-walk": "^0.1.0"
}
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -14505,6 +14818,17 @@
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"mpd-parser": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.1.tgz",
"integrity": "sha512-BxlSXWbKE1n7eyEPBnTEkrzhS3PdmkkKdM1pgKbPnPOH0WFZIc0sPOWi7m0Uo3Wd2a4Or8Qf4ZbS7+ASqQ49fw==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.5",
"@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -14522,6 +14846,15 @@
"thunky": "^1.0.2"
}
},
"mux.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"requires": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
}
},
"nanoid": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
@ -14840,6 +15173,11 @@
"sha.js": "^2.4.8"
}
},
"photoswipe": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.3.3.tgz",
"integrity": "sha512-BUuulwZwkYFKADSe5xf0dd+wf6dws34ZvqP8R3oYHepRauOXoQHvw600sw1HlWd8K0S3LRCS4jxyR5fTuI383Q=="
},
"picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -14851,6 +15189,14 @@
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true
},
"pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"pkg-dir": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
@ -14961,9 +15307,7 @@
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"dev": true,
"peer": true
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="
},
"process-nextick-args": {
"version": "2.0.1",
@ -15347,12 +15691,28 @@
}
}
},
"rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha512-6cJzSBU+J/RJCF063onnQf0cDUOHs9uZI1oroSGnHOph+CQTIJ5Pp2hK5kEQq1+7yE/EEWfulSNXAQ2jikPthA==",
"requires": {
"individual": "^2.0.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha512-RjZPPHugjK0TOzFrLZ8inw44s9bKox99/0AZW9o/BEQVrJfhI+fIHMErnPyRa89/yRXUUr93q+tiN6zhoVV4wQ==",
"requires": {
"rust-result": "^1.0.0"
}
},
"safe-regex-test": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz",
@ -16310,6 +16670,11 @@
"requires-port": "^1.0.0"
}
},
"url-toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
},
"util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@ -16356,6 +16721,39 @@
"dev": true,
"peer": true
},
"video.js": {
"version": "7.20.3",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.20.3.tgz",
"integrity": "sha512-JMspxaK74LdfWcv69XWhX4rILywz/eInOVPdKefpQiZJSMD5O8xXYueqACP2Q5yqKstycgmmEKlJzZ+kVmDciw==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.14.3",
"@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.3",
"global": "^4.4.0",
"keycode": "^2.2.0",
"m3u8-parser": "4.7.1",
"mpd-parser": "0.21.1",
"mux.js": "6.0.1",
"safe-json-parse": "4.0.0",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.4"
}
},
"videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"videojs-vtt.js": {
"version": "0.15.4",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.4.tgz",
"integrity": "sha512-r6IhM325fcLb1D6pgsMkTQT1PpFdUdYZa1iqk7wJEu+QlibBwATPfPc9Bg8Jiym0GE5yP1AG2rMLu+QMVWkYtA==",
"requires": {
"global": "^4.3.1"
}
},
"vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",

View File

@ -37,7 +37,9 @@
"justified-layout": "^4.1.0",
"moment": "^2.29.4",
"path-posix": "^1.0.0",
"photoswipe": "^5.3.3",
"reflect-metadata": "^0.1.13",
"video.js": "^7.20.3",
"vue": "^2.7.10",
"vue-class-component": "^7.2.6",
"vue-material-design-icons": "^5.1.2",

View File

@ -271,11 +271,14 @@ body {
}
// Top bar is above everything else on mobile
#content-vue.has-top-bar {
body.has-top-bar header {
@media (max-width: 1024px) {
z-index: 3000;
z-index: 0 !important;
}
}
body.has-viewer header {
z-index: 0 !important;
}
// Patch viewer to remove the title and
// make the image fill the entire screen
@ -286,6 +289,11 @@ body {
.modal-wrapper .modal-container {
top: 0 !important;
bottom: 0 !important;
.viewer__image-editor {
top: 0 !important;
bottom: 0 !important;
}
}
}

View File

@ -154,7 +154,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Get cursor px position
const { top1, top2, y1, y2 } = this.getCoords(scroll, "y");
const topfrac = (scroll - y1) / (y2 - y1);
const rtop = top1 + (top2 - top1) * topfrac;
const rtop = top1 + (top2 - top1) * (topfrac || 0);
// Always move static cursor to right position
this.cursorY = rtop;
@ -200,7 +200,6 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
// Ticks
let prevYear = 9999;
let prevMonth = 0;
const thisYear = new Date().getFullYear();
// Get a new tick
const getTick = (
@ -235,8 +234,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
const dtYear = dateTaken.getUTCFullYear();
const dtMonth = dateTaken.getUTCMonth();
const isMonth = dtMonth !== prevMonth || dtYear !== prevYear;
const text =
dtYear === prevYear || dtYear === thisYear ? undefined : dtYear;
const text = dtYear === prevYear ? undefined : dtYear;
this.ticks.push(getTick(row.dayId, isMonth, text));
prevMonth = dtMonth;
@ -459,7 +457,7 @@ export default class ScrollerManager extends Mixins(GlobalMixin) {
const { top1, top2, y1, y2 } = this.getCoords(y, "topF");
const yfrac = (y - top1) / (top2 - top1);
const ry = y1 + (y2 - y1) * yfrac;
const ry = y1 + (y2 - y1) * (yfrac || 0);
this.recycler.scrollToPosition(ry);
this.handleScroll();

View File

@ -13,8 +13,8 @@
<div class="text">
{{
n("memories", "{n} selected", "{n} selected", selection.size, {
n: selection.size,
n("memories", "{n} selected", "{n} selected", size, {
n: size,
})
}}
</div>
@ -55,7 +55,7 @@ import { showError } from "@nextcloud/dialogs";
import { generateUrl } from "@nextcloud/router";
import { NcActions, NcActionButton } from "@nextcloud/vue";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import { IHeadRow, IPhoto, ISelectionAction } from "../types";
import { IDay, IHeadRow, IPhoto, ISelectionAction } from "../types";
import { getCurrentUser } from "@nextcloud/auth";
import * as dav from "../services/DavRequests";
@ -92,6 +92,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
@Prop() public heads: { [dayid: number]: IHeadRow };
private show = false;
private size = 0;
private readonly selection!: Selection;
private readonly defaultActions: ISelectionAction[];
@ -182,17 +183,17 @@ export default class SelectionManager 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);
document.body.classList.add(klass);
} else {
elem.classList.remove(klass);
document.body.classList.remove(klass);
}
}
private selectionChanged() {
this.show = this.selection.size > 0;
this.size = this.selection.size;
}
/** Is this fileid (or anything if not specified) selected */
@ -203,6 +204,39 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
return this.selection.has(fileid);
}
/** Restore selections from new day object */
public restoreDay(day: IDay) {
if (!this.has()) {
return;
}
// FileID => Photo for new day
const dayMap = new Map<number, IPhoto>();
day.detail.forEach((photo) => {
dayMap.set(photo.fileid, photo);
});
this.selection.forEach((photo, fileid) => {
// Process this day only
if (photo.dayid !== day.dayid) {
return;
}
// Remove all selections that are not in the new day
if (!dayMap.has(fileid)) {
this.selection.delete(fileid);
return;
}
// Update the photo object
const newPhoto = dayMap.get(fileid);
this.selection.set(fileid, newPhoto);
newPhoto.flag |= this.c.FLAG_SELECTED;
});
this.selectionChanged();
}
/** Click on an action */
private async click(action: ISelectionAction) {
try {
@ -335,18 +369,6 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
Array.from(selection.values()),
val
)) {
favIds.forEach((id) => {
const photo = selection.get(id);
if (!photo) {
return;
}
if (val) {
photo.flag |= this.c.FLAG_IS_FAVORITE;
} else {
photo.flag &= ~this.c.FLAG_IS_FAVORITE;
}
});
}
this.clearSelection();
}
@ -391,17 +413,7 @@ export default class SelectionManager extends Mixins(GlobalMixin, UserConfig) {
*/
private async viewInFolder(selection: Selection) {
if (selection.size !== 1) return;
const photo: IPhoto = selection.values().next().value;
const f = await dav.getFiles([photo]);
if (f.length === 0) return;
const file = f[0];
const dirPath = file.filename.split("/").slice(0, -1).join("/");
const url = generateUrl(
`/apps/files/?dir=${dirPath}&scrollto=${file.fileid}&openfile=${file.fileid}`
);
window.open(url, "_blank");
dav.viewInFolder(selection.values().next().value);
}
/**

View File

@ -44,8 +44,8 @@
<OnThisDay
v-if="$route.name === 'timeline'"
:viewerManager="viewerManager"
:key="config_timelinePath"
:viewer="$refs.viewer"
@load="scrollerManager.adjust()"
>
</OnThisDay>
@ -121,35 +121,46 @@
@delete="deleteFromViewWithAnimation"
@updateLoading="updateLoading"
/>
<Viewer
ref="viewer"
@deleted="deleteFromViewWithAnimation"
@fetchDay="fetchDay"
@updateLoading="updateLoading"
/>
</div>
</template>
<script lang="ts">
import { Component, Mixins, Watch } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import axios from "@nextcloud/axios";
import { showError } from "@nextcloud/dialogs";
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import { generateUrl } from "@nextcloud/router";
import { NcEmptyContent } from "@nextcloud/vue";
import PeopleIcon from "vue-material-design-icons/AccountMultiple.vue";
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
import ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
import { Component, Mixins, Watch } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import UserConfig from "../mixins/UserConfig";
import * as dav from "../services/DavRequests";
import { getLayout } from "../services/Layout";
import * as utils from "../services/Utils";
import { ViewerManager } from "../services/Viewer";
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types";
import Folder from "./frame/Folder.vue";
import Photo from "./frame/Photo.vue";
import Tag from "./frame/Tag.vue";
import ScrollerManager from "./ScrollerManager.vue";
import SelectionManager from "./SelectionManager.vue";
import Viewer from "./Viewer.vue";
import OnThisDay from "./top-matter/OnThisDay.vue";
import TopMatter from "./top-matter/TopMatter.vue";
import * as dav from "../services/DavRequests";
import * as utils from "../services/Utils";
import PeopleIcon from "vue-material-design-icons/AccountMultiple.vue";
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
import ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
const SCROLL_LOAD_DELAY = 100; // Delay in loading data when scrolling
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
@ -163,6 +174,7 @@ const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
OnThisDay,
SelectionManager,
ScrollerManager,
Viewer,
NcEmptyContent,
CheckCircle,
@ -212,13 +224,6 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
/** Scroller manager component */
private scrollerManager!: ScrollerManager & any;
/** Nextcloud viewer proxy */
private viewerManager = new ViewerManager(
this.deleteFromViewWithAnimation.bind(this),
this.updateLoading.bind(this),
this.$route
);
mounted() {
this.selectionManager = this.$refs.selectionManager;
this.scrollerManager = this.$refs.scrollerManager;
@ -256,6 +261,10 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
return window.innerWidth <= 600;
}
isMonthView() {
return this.$route.name === "albums";
}
allowBreakout() {
return this.isMobileLayout() && !this.config_squareThumbs;
}
@ -534,6 +543,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
query.set("folder_share", this.$route.params.token);
}
// Month view
if (this.isMonthView()) {
query.set("monthView", "1");
query.set("reverse", "1");
}
// Create query string and append to URL
const queryStr = query.toString();
if (queryStr) {
@ -582,7 +597,12 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// The reason this function is separate from processDays is
// because this call is terribly slow even on desktop
const dateTaken = utils.dayIdToDate(head.dayId);
const name = utils.getLongDateStr(dateTaken, true);
let name: string;
if (this.isMonthView()) {
name = utils.getMonthDateStr(dateTaken);
} else {
name = utils.getLongDateStr(dateTaken, true);
}
// Cache and return
head.name = name;
@ -782,6 +802,13 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// Aggregate fetch requests
this.fetchDayQueue.push(dayId);
// Only single queries allowed for month vie
if (this.isMonthView()) {
return this.fetchDayExpire();
}
// Defer for aggregation
if (!this.fetchDayTimer) {
this.fetchDayTimer = window.setTimeout(() => {
this.fetchDayTimer = null;
@ -1029,6 +1056,9 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
row.photos.push(photo);
}
// Restore selection day
this.selectionManager.restoreDay(day);
// Rows that were removed
const removedRows: IRow[] = [];
let headRemoved = false;
@ -1116,7 +1146,7 @@ export default class Timeline extends Mixins(GlobalMixin, UserConfig) {
// selection mode
this.selectionManager.selectPhoto(photo);
} else {
this.viewerManager.open(photo);
(<any>this.$refs.viewer).open(photo, this.list);
}
}

View File

@ -0,0 +1,657 @@
<template>
<div
class="memories_viewer outer"
v-if="show"
:class="{ fullyOpened }"
:style="{ width: outerWidth }"
>
<div class="inner" ref="inner">
<div class="top-bar" v-if="photoswipe" :class="{ opened }">
<NcActions :inline="3" container=".memories_viewer .pswp">
<NcActionButton
:aria-label="t('memories', 'Delete')"
@click="deleteCurrent"
:close-after-click="true"
>
{{ t("memories", "Delete") }}
<template #icon> <DeleteIcon :size="24" /> </template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Favorite')"
@click="favoriteCurrent"
:close-after-click="true"
>
{{ t("memories", "Favorite") }}
<template #icon>
<StarIcon v-if="isFavorite()" :size="24" />
<StarOutlineIcon v-else :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Sidebar')"
@click="toggleSidebar"
:close-after-click="true"
>
{{ t("memories", "Sidebar") }}
<template #icon>
<InfoIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Download')"
@click="downloadCurrent"
:close-after-click="true"
>
{{ t("memories", "Download") }}
<template #icon>
<DownloadIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'View in folder')"
@click="viewInFolder"
:close-after-click="true"
>
{{ t("memories", "View in folder") }}
<template #icon>
<OpenInNewIcon :size="24" />
</template>
</NcActionButton>
</NcActions>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Emit, Mixins } from "vue-property-decorator";
import GlobalMixin from "../mixins/GlobalMixin";
import { IDay, IPhoto, IRow, IRowType } from "../types";
import { NcActions, NcActionButton } from "@nextcloud/vue";
import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import { generateUrl } from "@nextcloud/router";
import * as dav from "../services/DavRequests";
import * as utils from "../services/Utils";
import { getPreviewUrl } from "../services/FileUtils";
import { getAlbumFileInfos } from "../services/DavRequests";
import PhotoSwipe, { PhotoSwipeOptions } from "photoswipe";
import "photoswipe/style.css";
import videojs from "video.js";
import "video.js/dist/video-js.css";
import DeleteIcon from "vue-material-design-icons/Delete.vue";
import StarIcon from "vue-material-design-icons/Star.vue";
import StarOutlineIcon from "vue-material-design-icons/StarOutline.vue";
import DownloadIcon from "vue-material-design-icons/Download.vue";
import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
import OpenInNewIcon from "vue-material-design-icons/OpenInNew.vue";
@Component({
components: {
NcActions,
NcActionButton,
DeleteIcon,
StarIcon,
StarOutlineIcon,
DownloadIcon,
InfoIcon,
OpenInNewIcon,
},
})
export default class Viewer extends Mixins(GlobalMixin) {
@Emit("deleted") deleted(photos: IPhoto[]) {}
@Emit("fetchDay") fetchDay(dayId: number) {}
@Emit("updateLoading") updateLoading(delta: number) {}
private show = false;
private opened = false;
private fullyOpened = false;
private sidebarOpen = false;
private sidebarWidth = 400;
private outerWidth = "100vw";
/** Base dialog */
private photoswipe: PhotoSwipe | null = null;
private list: IPhoto[] = [];
private days = new Map<number, IDay>();
private dayIds: number[] = [];
private globalCount = 0;
private globalAnchor = -1;
mounted() {
subscribe("files:sidebar:opened", this.handleAppSidebarOpen);
subscribe("files:sidebar:closed", this.handleAppSidebarClose);
}
beforeDestroy() {
unsubscribe("files:sidebar:opened", this.handleAppSidebarOpen);
unsubscribe("files:sidebar:closed", this.handleAppSidebarClose);
}
/** Get the currently open photo */
private getCurrentPhoto() {
if (!this.list.length || !this.photoswipe) {
return null;
}
const idx = this.photoswipe.currIndex - this.globalAnchor;
if (idx < 0 || idx >= this.list.length) {
return null;
}
return this.list[idx];
}
/** Create the base photoswipe object */
private async createBase(args: PhotoSwipeOptions) {
this.show = true;
await this.$nextTick();
this.photoswipe = new PhotoSwipe({
counter: true,
zoom: false,
loop: false,
bgOpacity: 1,
appendToEl: this.$refs.inner as HTMLElement,
preload: [2, 2],
getViewportSizeFn: () => {
const sidebarWidth = this.sidebarOpen ? this.sidebarWidth : 0;
this.outerWidth = `calc(100vw - ${sidebarWidth}px)`;
return {
x: window.innerWidth - sidebarWidth,
y: window.innerHeight,
};
},
...args,
});
// Debugging only
globalThis.photoswipe = this.photoswipe;
// Monkey patch for focus trapping in sidebar
const _onFocusIn = this.photoswipe.keyboard._onFocusIn;
this.photoswipe.keyboard._onFocusIn = (e: FocusEvent) => {
if (e.target instanceof HTMLElement) {
if (
e.target.closest("aside.app-sidebar") ||
e.target.closest(".v-popper__popper")
) {
return;
}
}
_onFocusIn.call(this.photoswipe.keyboard, e);
};
// Refresh sidebar on change
this.photoswipe.on("change", () => {
if (this.sidebarOpen) {
this.openSidebar();
}
});
// Make sure buttons are styled properly
this.photoswipe.addFilter("uiElement", (element, data) => {
// add button-vue class if button
if (element.classList.contains("pswp__button")) {
element.classList.add("button-vue");
}
return element;
});
// Total number of photos in this view
this.photoswipe.addFilter("numItems", (numItems) => {
return this.globalCount;
});
// Put viewer over everything else
const navElem = document.getElementById("app-navigation-vue");
const klass = "has-viewer";
this.photoswipe.on("beforeOpen", () => {
document.body.classList.add(klass);
navElem.style.zIndex = "0";
});
this.photoswipe.on("openingAnimationStart", () => {
this.fullyOpened = false;
this.opened = true;
if (this.sidebarOpen) {
this.openSidebar();
}
});
this.photoswipe.on("openingAnimationEnd", () => {
this.fullyOpened = true;
});
this.photoswipe.on("close", () => {
this.fullyOpened = false;
this.opened = false;
this.hideSidebar();
});
this.photoswipe.on("tapAction", () => {
this.opened = !this.opened; // toggle-controls
});
this.photoswipe.on("destroy", () => {
document.body.classList.remove(klass);
navElem.style.zIndex = "";
// reset everything
this.show = false;
this.opened = false;
this.fullyOpened = false;
this.photoswipe = null;
this.list = [];
this.days.clear();
this.dayIds = [];
this.globalCount = 0;
this.globalAnchor = -1;
});
// Video support
this.photoswipe.on("contentLoad", (e) => {
const { content, isLazy } = e;
if (content.data.photo.flag & this.c.FLAG_IS_VIDEO) {
e.preventDefault();
content.type = "video";
// Create video element
content.videoElement = document.createElement("video") as any;
content.videoElement.classList.add("video-js");
// Get DAV URL for video
let url = `remote.php/dav/${content.data.photo.filename}`; // normal route
// Check if albums
const route = vuerouter.currentRoute;
if (route.name === "albums") {
const fInfos = getAlbumFileInfos(
[content.data.photo],
route.params.user,
route.params.name
);
if (fInfos.length) {
url = `remote.php/dav/${fInfos[0].originalFilename}`;
}
}
// Add child with source element
const source = document.createElement("source");
source.src = generateUrl(url);
source.type = content.data.photo.mimetype;
content.videoElement.appendChild(source);
// Create container div
content.element = document.createElement("div");
content.element.appendChild(content.videoElement);
// Init videojs
videojs(content.videoElement, {
fluid: true,
autoplay: content.data.playvideo,
controls: true,
preload: "metadata",
muted: true,
html5: {
vhs: {
withCredentials: true,
},
},
});
}
});
// Play video on open slide
this.photoswipe.on("slideActivate", (e) => {
const { slide } = e;
if (slide.data.photo.flag & this.c.FLAG_IS_VIDEO) {
setTimeout(() => {
slide.content.element.querySelector("video")?.play();
}, 500);
}
});
// Pause video on close slide
this.photoswipe.on("slideDeactivate", (e) => {
const { slide } = e;
if (slide.data.photo.flag & this.c.FLAG_IS_VIDEO) {
slide.content.element.querySelector("video")?.pause();
}
});
return this.photoswipe;
}
/** Open using start photo and rows list */
public async open(anchorPhoto: IPhoto, rows?: IRow[]) {
this.list = [...anchorPhoto.d.detail];
let startIndex = -1;
for (const r of rows) {
if (r.type === IRowType.HEAD) {
if (r.day.dayid == anchorPhoto.d.dayid) {
startIndex = r.day.detail.findIndex(
(p) => p.fileid === anchorPhoto.fileid
);
this.globalAnchor = this.globalCount;
}
this.globalCount += r.day.count;
this.days.set(r.day.dayid, r.day);
this.dayIds.push(r.day.dayid);
}
}
await this.createBase({
index: this.globalAnchor + startIndex,
});
this.photoswipe.addFilter("itemData", (itemData, index) => {
// Get photo object from list
let idx = index - this.globalAnchor;
if (idx < 0) {
// Load previous day
const firstDayId = this.list[0].d.dayid;
const firstDayIdx = utils.binarySearch(this.dayIds, firstDayId);
if (firstDayIdx === 0) {
// No previous day
return {};
}
const prevDayId = this.dayIds[firstDayIdx - 1];
const prevDay = this.days.get(prevDayId);
if (!prevDay.detail) {
console.error("[BUG] No detail for previous day");
return {};
}
this.list.unshift(...prevDay.detail);
this.globalAnchor -= prevDay.count;
} else if (idx >= this.list.length) {
// Load next day
const lastDayId = this.list[this.list.length - 1].d.dayid;
const lastDayIdx = utils.binarySearch(this.dayIds, lastDayId);
if (lastDayIdx === this.dayIds.length - 1) {
// No next day
return {};
}
const nextDayId = this.dayIds[lastDayIdx + 1];
const nextDay = this.days.get(nextDayId);
if (!nextDay.detail) {
console.error("[BUG] No detail for next day");
return {};
}
this.list.push(...nextDay.detail);
}
idx = index - this.globalAnchor;
const photo = this.list[idx];
// Something went really wrong
if (!photo) {
return {};
}
// Preload next and previous 3 days
const dayIdx = utils.binarySearch(this.dayIds, photo.d.dayid);
const preload = (idx: number) => {
if (
idx > 0 &&
idx < this.dayIds.length &&
!this.days.get(this.dayIds[idx]).detail
) {
this.fetchDay(this.dayIds[idx]);
}
};
preload(dayIdx - 1);
preload(dayIdx - 2);
preload(dayIdx - 3);
preload(dayIdx + 1);
preload(dayIdx + 2);
preload(dayIdx + 3);
// Get thumb image
const thumbSrc: string =
photo.flag & this.c.FLAG_IS_VIDEO
? undefined
: this.thumbElem(photo)?.querySelector("img")?.getAttribute("src") ||
getPreviewUrl(photo, false, 256);
// Get full image
return {
...this.getItemData(photo),
msrc: thumbSrc,
};
});
this.photoswipe.addFilter("thumbEl", (thumbEl, data, index) => {
const photo = this.list[index - this.globalAnchor];
if (photo.flag & this.c.FLAG_IS_VIDEO) return thumbEl;
return this.thumbElem(photo) || thumbEl;
});
this.photoswipe.init();
}
/** Open with a static list of photos */
public async openStatic(photo: IPhoto, list: IPhoto[]) {
this.list = list;
await this.createBase({
index: list.findIndex((p) => p.fileid === photo.fileid),
});
this.globalCount = list.length;
this.globalAnchor = 0;
this.photoswipe.addFilter("itemData", (itemData, index) => {
return this.getItemData(this.list[index]);
});
this.photoswipe.init();
}
/** Get base data object */
private getItemData(photo: IPhoto) {
return {
src: getPreviewUrl(photo, false, 2048),
width: photo.w || undefined,
height: photo.h || undefined,
thumbCropped: true,
photo: photo,
};
}
/** Get element for thumbnail if it exists */
private thumbElem(photo: IPhoto) {
if (!photo) return;
return document.getElementById(
`memories-photo-${photo.key || photo.fileid}`
);
}
/** Delete this photo and refresh */
private async deleteCurrent() {
const idx = this.photoswipe.currIndex - this.globalAnchor;
// Delete with WebDAV
try {
this.updateLoading(1);
for await (const p of dav.deletePhotos([this.list[idx]])) {
if (!p[0]) return;
}
} finally {
this.updateLoading(-1);
}
const spliced = this.list.splice(idx, 1);
this.globalCount--;
for (let i = idx - 3; i <= idx + 3; i++) {
this.photoswipe.refreshSlideContent(i + this.globalAnchor);
}
this.deleted(spliced);
}
/** Is the current photo a favorite */
private isFavorite() {
const p = this.getCurrentPhoto();
if (!p) return false;
return Boolean(p.flag & this.c.FLAG_IS_FAVORITE);
}
/** Favorite the current photo */
private async favoriteCurrent() {
const photo = this.getCurrentPhoto();
const val = !this.isFavorite();
try {
this.updateLoading(1);
for await (const p of dav.favoritePhotos([photo], val)) {
if (!p[0]) return;
this.$forceUpdate();
}
} finally {
this.updateLoading(-1);
}
// Set flag on success
if (val) {
photo.flag |= this.c.FLAG_IS_FAVORITE;
} else {
photo.flag &= ~this.c.FLAG_IS_FAVORITE;
}
}
/** Download the current photo */
private async downloadCurrent() {
const photo = this.getCurrentPhoto();
if (!photo) return;
dav.downloadFilesByIds([photo]);
}
/** Open the sidebar */
private async openSidebar(photo?: IPhoto) {
const fInfo = await dav.getFiles([photo || this.getCurrentPhoto()]);
globalThis.OCA?.Files?.Sidebar?.setFullScreenMode?.(true);
globalThis.OCA.Files.Sidebar.open(fInfo[0].filename);
}
private async updateSizeWithoutAnim() {
const wasFullyOpened = this.fullyOpened;
this.fullyOpened = false;
this.photoswipe.updateSize();
await new Promise((resolve) => setTimeout(resolve, 200));
this.fullyOpened = wasFullyOpened;
}
private handleAppSidebarOpen() {
if (this.show && this.photoswipe) {
const sidebar: HTMLElement = document.querySelector("aside.app-sidebar");
if (sidebar) {
this.sidebarWidth = sidebar.offsetWidth - 2;
}
this.sidebarOpen = true;
this.updateSizeWithoutAnim();
}
}
private handleAppSidebarClose() {
if (this.show && this.photoswipe && this.fullyOpened) {
this.sidebarOpen = false;
this.updateSizeWithoutAnim();
}
}
/** Hide the sidebar, without marking it as closed */
private hideSidebar() {
globalThis.OCA?.Files?.Sidebar?.close();
globalThis.OCA?.Files?.Sidebar?.setFullScreenMode?.(false);
}
/** Close the sidebar */
private closeSidebar() {
this.hideSidebar();
this.sidebarOpen = false;
this.photoswipe.updateSize();
}
/** Toggle the sidebar visibility */
private toggleSidebar() {
if (this.sidebarOpen) {
this.closeSidebar();
} else {
this.openSidebar();
}
}
/**
* Open the files app with the current file.
*/
private async viewInFolder() {
const photo = this.getCurrentPhoto();
if (photo) dav.viewInFolder(photo);
}
}
</script>
<style lang="scss" scoped>
.outer {
z-index: 3000;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
overflow: hidden;
color: white;
}
.top-bar {
z-index: 100001;
position: absolute;
top: 8px;
right: 50px;
:deep .button-vue--icon-only {
color: white;
background-color: transparent !important;
}
transition: opacity 0.2s ease-in-out;
opacity: 0;
&.opened {
opacity: 1;
}
}
.fullyOpened :deep .pswp__container {
@media (min-width: 1024px) {
transition: transform var(--pswp-transition-duration) ease !important;
}
}
.inner,
.inner :deep .pswp {
width: inherit;
}
:deep .video-js .vjs-big-play-button {
display: none;
}
:deep .pswp {
.pswp__zoom-wrap {
width: 100%;
}
.pswp__button {
color: white;
&,
* {
cursor: pointer;
}
}
.pswp__icn-shadow {
display: none;
}
}
</style>

View File

@ -1,5 +1,6 @@
<template>
<div
:id="`memories-photo-${data.key || data.fileid}`"
class="p-outer fill-block"
:class="{
selected: data.flag & c.FLAG_SELECTED,

View File

@ -47,7 +47,6 @@ import { NcActions, NcActionButton } from "@nextcloud/vue";
import * as utils from "../../services/Utils";
import * as dav from "../../services/DavRequests";
import { ViewerManager } from "../../services/Viewer";
import { IPhoto } from "../../types";
import { getPreviewUrl } from "../../services/FileUtils";
@ -74,6 +73,9 @@ interface IYear {
export default class OnThisDay extends Mixins(GlobalMixin) {
private getPreviewUrl = getPreviewUrl;
@Prop()
private viewer: any;
@Emit("load")
onload() {}
@ -83,14 +85,6 @@ export default class OnThisDay extends Mixins(GlobalMixin) {
private hasLeft = false;
private scrollStack: number[] = [];
/**
* Nextcloud viewer proxy
* Can't use the timeline instance because these photos
* might not be in view, so can't delete them
*/
@Prop()
private viewerManager!: ViewerManager;
mounted() {
const inner = this.$refs.inner as HTMLElement;
inner.addEventListener("scroll", this.onScroll.bind(this), {
@ -200,7 +194,7 @@ export default class OnThisDay extends Mixins(GlobalMixin) {
click(year: IYear) {
const allPhotos = this.years.flatMap((y) => y.photos);
this.viewerManager.open(year.preview, allPhotos);
this.viewer.openStatic(year.preview, allPhotos);
}
}
</script>

View File

@ -7,3 +7,4 @@ export * from "./dav/favorites";
export * from "./dav/folders";
export * from "./dav/onthisday";
export * from "./dav/tags";
export * from "./dav/other";

View File

@ -51,6 +51,15 @@ export function getLongDateStr(date: Date, skipYear = false, time = false) {
});
}
/** Get month and year string */
export function getMonthDateStr(date: Date) {
return date.toLocaleDateString(getCanonicalLocale(), {
month: "long",
year: "numeric",
timeZone: "UTC",
});
}
/** Get text like "5 years ago" from a date */
export function getFromNowStr(date: Date) {
// Get fromNow in correct locale
@ -85,6 +94,12 @@ export function hashCode(str: string): number {
* @param key Key to use for comparison
*/
export function binarySearch(arr: any, elem: any, key?: string) {
if (arr.length === 0) return 0;
const desc = key
? arr[0][key] > arr[arr.length - 1][key]
: arr[0] > arr[arr.length - 1];
let minIndex = 0;
let maxIndex = arr.length - 1;
let currentIndex: number;
@ -94,9 +109,12 @@ export function binarySearch(arr: any, elem: any, key?: string) {
currentIndex = ((minIndex + maxIndex) / 2) | 0;
currentElement = key ? arr[currentIndex][key] : arr[currentIndex];
if (currentElement < elem) {
const e1 = desc ? elem : currentElement;
const e2 = desc ? currentElement : elem;
if (e1 < e2) {
minIndex = currentIndex + 1;
} else if (currentElement > elem) {
} else if (e1 > e2) {
maxIndex = currentIndex - 1;
} else {
return currentIndex;

View File

@ -1,94 +0,0 @@
import { IFileInfo, IPhoto } from "../types";
import { showError } from "@nextcloud/dialogs";
import { subscribe } from "@nextcloud/event-bus";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import { Route } from "vue-router";
import * as dav from "./DavRequests";
// Key to store sidebar state
const SIDEBAR_KEY = "memories:sidebar-open";
export class ViewerManager {
/** Map from fileid to Photo */
private photoMap = new Map<number, IPhoto>();
constructor(
ondelete: (photos: IPhoto[]) => void,
private updateLoading: (delta: number) => void,
private $route: Route
) {
subscribe("files:file:deleted", ({ fileid }: { fileid: number }) => {
const photo = this.photoMap.get(fileid);
ondelete([photo]);
});
}
public async open(photo: IPhoto, list?: IPhoto[]) {
list = list || photo.d?.detail;
if (!list?.length) return;
// Repopulate map
this.photoMap.clear();
for (const p of list) {
this.photoMap.set(p.fileid, p);
}
// Get file infos
let fileInfos: IFileInfo[];
try {
this.updateLoading(1);
fileInfos = await dav.getFiles(list);
} catch (e) {
console.error("Failed to load fileInfos", e);
showError("Failed to load fileInfos");
return;
} finally {
this.updateLoading(-1);
}
if (fileInfos.length === 0) {
return;
}
// Fix sorting of the fileInfos
const itemPositions = {};
for (const [index, p] of list.entries()) {
itemPositions[p.fileid] = index;
}
fileInfos.sort(function (a, b) {
return itemPositions[a.fileid] - itemPositions[b.fileid];
});
// Get this photo in the fileInfos
const fInfo = fileInfos.find((d) => Number(d.fileid) === photo.fileid);
if (!fInfo) {
showError(t("memories", "Cannot find this photo anymore!"));
return;
}
// Check viewer > 2.0.0
const viewerVersion: string = globalThis.OCA.Viewer.version;
const viewerMajor = Number(viewerVersion.split(".")[0]);
// Open Nextcloud viewer
globalThis.OCA.Viewer.open({
fileInfo: fInfo,
path: viewerMajor < 2 ? fInfo.filename : undefined, // Only specify path upto Nextcloud 24
list: fileInfos, // file list
canLoop: false, // don't loop
onClose: () => {
// on viewer close
if (globalThis.OCA.Files.Sidebar.file) {
localStorage.setItem(SIDEBAR_KEY, "1");
} else {
localStorage.removeItem(SIDEBAR_KEY);
}
globalThis.OCA.Files.Sidebar.close();
},
});
// Restore sidebar state
if (localStorage.getItem(SIDEBAR_KEY) === "1") {
globalThis.OCA.Files.Sidebar.open(fInfo.filename);
}
}
}

View File

@ -62,6 +62,7 @@ export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
mime: photo.mimetype,
hasPreview: true,
etag: photo.etag,
permissions: "RWD",
} as IFileInfo;
})
);

View File

@ -3,6 +3,7 @@ import { translate as t } from "@nextcloud/l10n";
import { IPhoto } from "../../types";
import client from "../DavClient";
import * as base from "./base";
import * as utils from "../Utils";
/**
* Favorite a file
@ -61,6 +62,12 @@ export async function* favoritePhotos(
const calls = fileInfos.map((fileInfo) => async () => {
try {
await favoriteFile(fileInfo.originalFilename, favoriteState);
const photo = photos.find((p) => p.fileid === fileInfo.fileid);
if (favoriteState) {
photo.flag |= utils.constants.c.FLAG_IS_FAVORITE;
} else {
photo.flag &= ~utils.constants.c.FLAG_IS_FAVORITE;
}
return fileInfo.fileid as number;
} catch (error) {
console.error("Failed to favorite", fileInfo, error);

View File

@ -0,0 +1,19 @@
import { getFiles } from "./base";
import { generateUrl } from "@nextcloud/router";
import { IPhoto } from "../../types";
/**
* Open the files app with the given photo
* Opens a new window.
*/
export async function viewInFolder(photo: IPhoto) {
const f = await getFiles([photo]);
if (f.length === 0) return;
const file = f[0];
const dirPath = file.filename.split("/").slice(0, -1).join("/");
const url = generateUrl(
`/apps/files/?dir=${dirPath}&scrollto=${file.fileid}&openfile=${file.fileid}`
);
window.open(url, "_blank");
}

View File

@ -19,6 +19,8 @@ export type IFileInfo = {
flag?: number;
/** MIME type of file */
mime?: string;
/** WebDAV permissions string */
permissions?: string;
};
export type IDay = {