timeline: show video duration
parent
c6766833b5
commit
06d226432f
|
@ -102,7 +102,7 @@ trait TimelineQueryDays
|
|||
// We don't actually use m.datetaken here, but postgres
|
||||
// needs that all fields in ORDER BY are also in SELECT
|
||||
// when using DISTINCT on selected fields
|
||||
$query->select($fileid, 'm.isvideo', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
|
||||
$query->select($fileid, 'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h')
|
||||
->from('memories', 'm')
|
||||
;
|
||||
|
||||
|
@ -198,11 +198,13 @@ trait TimelineQueryDays
|
|||
// Convert field types
|
||||
$row['fileid'] = (int) $row['fileid'];
|
||||
$row['isvideo'] = (int) $row['isvideo'];
|
||||
$row['video_duration'] = (int) $row['video_duration'];
|
||||
$row['dayid'] = (int) $row['dayid'];
|
||||
$row['w'] = (int) $row['w'];
|
||||
$row['h'] = (int) $row['h'];
|
||||
if (!$row['isvideo']) {
|
||||
unset($row['isvideo']);
|
||||
unset($row['video_duration']);
|
||||
}
|
||||
if ($row['categoryid']) {
|
||||
$row['isfavorite'] = 1;
|
||||
|
|
|
@ -97,6 +97,12 @@ class TimelineWrite
|
|||
$dateTaken = gmdate('Y-m-d H:i:s', $dateTaken);
|
||||
[$w, $h] = Exif::getDimensions($exif);
|
||||
|
||||
// Video parameters
|
||||
$videoDuration = 0;
|
||||
if ($isvideo) {
|
||||
$videoDuration = round($exif['Duration'] ?? $exif['TrackDuration'] ?? 0);
|
||||
}
|
||||
|
||||
// Store raw metadata in the database
|
||||
// We need to remove blacklisted fields to prevent leaking info
|
||||
unset($exif['SourceFile'], $exif['FileName'], $exif['ExifToolVersion'], $exif['Directory'], $exif['FileSize'], $exif['FileModifyDate'], $exif['FileAccessDate'], $exif['FileInodeChangeDate'], $exif['FilePermissions']);
|
||||
|
@ -124,6 +130,7 @@ class TimelineWrite
|
|||
->set('datetaken', $query->createNamedParameter($dateTaken, IQueryBuilder::PARAM_STR))
|
||||
->set('mtime', $query->createNamedParameter($mtime, IQueryBuilder::PARAM_INT))
|
||||
->set('isvideo', $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT))
|
||||
->set('video_duration', $query->createNamedParameter($videoDuration, IQueryBuilder::PARAM_INT))
|
||||
->set('w', $query->createNamedParameter($w, IQueryBuilder::PARAM_INT))
|
||||
->set('h', $query->createNamedParameter($h, IQueryBuilder::PARAM_INT))
|
||||
->set('exif', $query->createNamedParameter($exifJson, IQueryBuilder::PARAM_STR))
|
||||
|
@ -141,6 +148,7 @@ class TimelineWrite
|
|||
'datetaken' => $query->createNamedParameter($dateTaken, IQueryBuilder::PARAM_STR),
|
||||
'mtime' => $query->createNamedParameter($mtime, IQueryBuilder::PARAM_INT),
|
||||
'isvideo' => $query->createNamedParameter($isvideo, IQueryBuilder::PARAM_INT),
|
||||
'video_duration' => $query->createNamedParameter($videoDuration, IQueryBuilder::PARAM_INT),
|
||||
'w' => $query->createNamedParameter($w, IQueryBuilder::PARAM_INT),
|
||||
'h' => $query->createNamedParameter($h, IQueryBuilder::PARAM_INT),
|
||||
'exif' => $query->createNamedParameter($exifJson, IQueryBuilder::PARAM_STR),
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Your name <your@email.com>
|
||||
* @author Your name <your@email.com>
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Memories\Migration;
|
||||
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\DB\Types;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Auto-generated migration step: Please modify to your needs!
|
||||
*/
|
||||
class Version400700Date20221110030909 extends SimpleMigrationStep
|
||||
{
|
||||
/**
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
*/
|
||||
public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
*/
|
||||
public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper
|
||||
{
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
$table = $schema->getTable('memories');
|
||||
|
||||
$table->addColumn('video_duration', Types::INTEGER, [
|
||||
'notnull' => true,
|
||||
'default' => 0,
|
||||
]);
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
*/
|
||||
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void
|
||||
{
|
||||
}
|
||||
}
|
|
@ -184,7 +184,6 @@ class VideoContentSetup {
|
|||
updateRotation(content, val?: number) {
|
||||
const rotation = val ?? Number(content.data.exif?.Rotation);
|
||||
const shouldRotate = content.videojs?.src().includes("m3u8");
|
||||
console.log("Video.js: Rotation", rotation, shouldRotate);
|
||||
if (rotation && shouldRotate) {
|
||||
let transform = `rotate(${rotation}deg)`;
|
||||
|
||||
|
|
|
@ -16,7 +16,13 @@
|
|||
@click="toggleSelect"
|
||||
/>
|
||||
|
||||
<Video :size="22" v-if="data.flag & c.FLAG_IS_VIDEO" />
|
||||
<div class="video" v-if="data.flag & c.FLAG_IS_VIDEO">
|
||||
<span v-if="data.video_duration" class="time">
|
||||
{{ videoDuration }}
|
||||
</span>
|
||||
<Video :size="22" />
|
||||
</div>
|
||||
|
||||
<Star :size="22" v-if="data.flag & c.FLAG_IS_FAVORITE" />
|
||||
|
||||
<div
|
||||
|
@ -43,14 +49,17 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
|
||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
||||
|
||||
import { getPreviewUrl } from "../../services/FileUtils";
|
||||
import { IDay, IPhoto } from "../../types";
|
||||
import * as utils from "../../services/Utils";
|
||||
|
||||
import errorsvg from "../../assets/error.svg";
|
||||
import CheckCircle from "vue-material-design-icons/CheckCircle.vue";
|
||||
import Star from "vue-material-design-icons/Star.vue";
|
||||
import Video from "vue-material-design-icons/PlayCircleOutline.vue";
|
||||
import { Component, Emit, Mixins, Prop, Watch } from "vue-property-decorator";
|
||||
import errorsvg from "../../assets/error.svg";
|
||||
import GlobalMixin from "../../mixins/GlobalMixin";
|
||||
import { getPreviewUrl } from "../../services/FileUtils";
|
||||
import { IDay, IPhoto } from "../../types";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
|
@ -91,6 +100,13 @@ export default class Photo extends Mixins(GlobalMixin) {
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
get videoDuration() {
|
||||
if (this.data.video_duration) {
|
||||
return utils.getDurationStr(this.data.video_duration);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
this.src = await this.getSrc();
|
||||
}
|
||||
|
@ -274,7 +290,7 @@ $icon-size: $icon-half-size * 2;
|
|||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
.play-circle-outline-icon,
|
||||
.video,
|
||||
.star-icon {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
|
@ -282,12 +298,23 @@ $icon-size: $icon-half-size * 2;
|
|||
transition: transform 0.15s ease;
|
||||
filter: invert(1) brightness(100);
|
||||
}
|
||||
.play-circle-outline-icon {
|
||||
.video {
|
||||
position: absolute;
|
||||
top: var(--icon-dist);
|
||||
right: var(--icon-dist);
|
||||
.p-outer.selected > & {
|
||||
transform: translate(-$icon-size, $icon-size);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.time {
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
.star-icon {
|
||||
bottom: var(--icon-dist);
|
||||
|
|
|
@ -69,6 +69,26 @@ export function getFromNowStr(date: Date) {
|
|||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
}
|
||||
|
||||
/** Convert number of seconds to time string */
|
||||
export function getDurationStr(sec: number) {
|
||||
let hours = Math.floor(sec / 3600);
|
||||
let minutes: number | string = Math.floor((sec - hours * 3600) / 60);
|
||||
let seconds: number | string = sec - hours * 3600 - minutes * 60;
|
||||
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
return `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a hash code from a string
|
||||
* @param {String} str The string to hash.
|
||||
|
|
|
@ -77,6 +77,8 @@ export type IPhoto = {
|
|||
|
||||
/** Video flag from server */
|
||||
isvideo?: boolean;
|
||||
/** Video duration from server */
|
||||
video_duration?: number;
|
||||
/** Favorite flag from server */
|
||||
isfavorite?: boolean;
|
||||
/** Is this a folder */
|
||||
|
|
Loading…
Reference in New Issue