Frontend: Replace "number" output filter with $util.formatNs() #3168

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-01-17 01:09:30 +01:00
parent 54e6283e30
commit d786a8225d
5 changed files with 161 additions and 32 deletions

View File

@@ -32,6 +32,7 @@ import Scrollbar from "common/scrollbar";
import { PhotoClipboard } from "common/clipboard";
import Event from "pubsub-js";
import Log from "common/log";
import Util from "common/util";
import * as components from "component/components";
import icons from "component/icons";
import defaults from "component/defaults";
@@ -67,7 +68,9 @@ config.update().finally(() => {
// Initialize libs and framework.
config.progress(66);
const isPublic = config.isPublic();
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
const isMobile =
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 2);
let app = createApp(PhotoPrism);
// Initialize language and detect alignment.
@@ -91,6 +94,7 @@ config.update().finally(() => {
app.config.globalProperties.$clipboard = PhotoClipboard;
app.config.globalProperties.$isMobile = isMobile;
app.config.globalProperties.$rtl = rtl;
app.config.globalProperties.$util = Util;
app.config.globalProperties.$sponsorFeatures = () => {
return config.load().finally(() => {
if (config.values.sponsor) {
@@ -130,7 +134,10 @@ config.update().finally(() => {
app.use(gettext);
// Use HTML sanitizer with v-sanitize directive.
app.use(Vue3Sanitize, { allowedTags: ["b", "strong", "span"], allowedAttributes: { b: ["dir"], strong: ["dir"], span: ["dir"] } });
app.use(Vue3Sanitize, {
allowedTags: ["b", "strong", "span"],
allowedAttributes: { b: ["dir"], strong: ["dir"], span: ["dir"] },
});
app.use(VueSanitize);
// TODO: check it

View File

@@ -25,7 +25,20 @@ Additional information can be found in our Developer Guide:
import { canUseAv1, canUseHevc, canUseOGV, canUseVP8, canUseVP9, canUseWebM } from "./caniuse";
import { config } from "app/session";
import { DATE_FULL, CodecAv01, CodecAv1C, CodecHev1, CodecHvc1, CodecOGV, CodecVP8, CodecVP9, FormatAv1, FormatAvc, FormatHevc, FormatWebM } from "model/photo";
import {
DATE_FULL,
CodecAv01,
CodecAv1C,
CodecHev1,
CodecHvc1,
CodecOGV,
CodecVP8,
CodecVP9,
FormatAv1,
FormatAvc,
FormatHevc,
FormatWebM,
} from "model/photo";
import sanitizeHtml from "sanitize-html";
import { DateTime } from "luxon";
@@ -99,6 +112,16 @@ export default class Util {
return result.join(":");
}
static formatNs(d) {
if (!d || typeof d !== "number") {
return "";
}
const ms = Math.round(d / 1000000).toLocaleString();
return `${ms} ms`;
}
static formatFPS(fps) {
return `${fps.toFixed(1)} FPS`;
}
@@ -183,7 +206,12 @@ export default class Util {
}
// Escape HTML control characters.
text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
text = text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
// Make URLs clickable.
text = text.replace(linkRegex, linkFunc);

View File

@@ -40,8 +40,6 @@
<template #chip="data">
<v-chip :model-value="data.selected" :disabled="data.disabled" class="bg-highlight rounded-xl text-truncate d-block" @click:close="removeSelection(data.index)">
<v-icon class="pr-1">mdi-bookmark</v-icon>
<!-- TODO: change this filter -->
<!-- {{ data.item.Title ? data.item.Title : data.item | truncate(40) }} -->
{{ data.item.title ? data.item.title : data.item }}
</v-chip>
</template>

View File

@@ -18,14 +18,27 @@
<v-row class="d-flex align-stretch" align="center" justify="center">
<v-col cols="12" class="pa-0 flex-grow-1">
<div class="v-table__overflow">
<v-table tile hover :density="$vuetify.display.smAndDown ? 'compact' : 'default'" class="photo-files d-flex bg-transparent">
<v-table
tile
hover
:density="$vuetify.display.smAndDown ? 'compact' : 'default'"
class="photo-files d-flex bg-transparent"
>
<tbody>
<tr v-if="file.FileType === 'jpg' || file.FileType === 'png'">
<td>
{{ $gettext(`Preview`) }}
</td>
<td>
<v-img :src="file.thumbnailUrl('tile_224')" aspect-ratio="1" max-width="112" max-height="112" rounded="4" class="card elevation-0 clickable my-1" @click.exact="openFile(file)"></v-img>
<v-img
:src="file.thumbnailUrl('tile_224')"
aspect-ratio="1"
max-width="112"
max-height="112"
rounded="4"
class="card elevation-0 clickable my-1"
@click.exact="openFile(file)"
></v-img>
</td>
</tr>
<tr>
@@ -34,19 +47,70 @@
</td>
<td>
<div class="action-buttons justify-start">
<v-btn v-if="features.download" density="comfortable" variant="flat" color="highlight" class="btn-action action-download" :disabled="busy" @click.stop.prevent="downloadFile(file)">
<v-btn
v-if="features.download"
density="comfortable"
variant="flat"
color="highlight"
class="btn-action action-download"
:disabled="busy"
@click.stop.prevent="downloadFile(file)"
>
{{ $gettext(`Download`) }}
</v-btn>
<v-btn v-if="features.edit && (file.FileType === 'jpg' || file.FileType === 'png') && !file.Error && !file.Primary" density="comfortable" variant="flat" color="highlight" class="btn-action action-primary" :disabled="busy" @click.stop.prevent="primaryFile(file)">
<v-btn
v-if="
features.edit &&
(file.FileType === 'jpg' || file.FileType === 'png') &&
!file.Error &&
!file.Primary
"
density="comfortable"
variant="flat"
color="highlight"
class="btn-action action-primary"
:disabled="busy"
@click.stop.prevent="primaryFile(file)"
>
{{ $gettext(`Primary`) }}
</v-btn>
<v-btn v-if="features.edit && !file.Sidecar && !file.Error && !file.Primary && file.Root === '/'" density="comfortable" variant="flat" color="highlight" class="btn-action action-unstack" :disabled="busy" @click.stop.prevent="unstackFile(file)">
<v-btn
v-if="
features.edit &&
!file.Sidecar &&
!file.Error &&
!file.Primary &&
file.Root === '/'
"
density="comfortable"
variant="flat"
color="highlight"
class="btn-action action-unstack"
:disabled="busy"
@click.stop.prevent="unstackFile(file)"
>
{{ $gettext(`Unstack`) }}
</v-btn>
<v-btn v-if="features.delete && !file.Primary" density="comfortable" variant="flat" color="highlight" class="btn-action action-delete" :disabled="busy" @click.stop.prevent="showDeleteDialog(file)">
<v-btn
v-if="features.delete && !file.Primary"
density="comfortable"
variant="flat"
color="highlight"
class="btn-action action-delete"
:disabled="busy"
@click.stop.prevent="showDeleteDialog(file)"
>
{{ $gettext(`Delete`) }}
</v-btn>
<v-btn v-if="experimental && canAccessPrivate && file.Primary" density="comfortable" variant="flat" color="highlight" class="btn-action action-open-folder" :href="folderUrl(file)" target="_blank">
<v-btn
v-if="experimental && canAccessPrivate && file.Primary"
density="comfortable"
variant="flat"
color="highlight"
class="btn-action action-open-folder"
:href="folderUrl(file)"
target="_blank"
>
{{ $gettext(`File Browser`) }}
</v-btn>
</div>
@@ -55,7 +119,9 @@
<tr>
<td title="Unique ID">UID</td>
<td class="text-break">
<span class="clickable text-uppercase" @click.stop.prevent="copyText(file.UID)">{{ file.UID }}</span>
<span class="clickable text-uppercase" @click.stop.prevent="copyText(file.UID)">{{
file.UID
}}</span>
</td>
</tr>
<tr v-if="file.InstanceID" title="XMP">
@@ -63,7 +129,11 @@
{{ $gettext(`Instance ID`) }}
</td>
<td class="text-break">
<span class="clickable text-uppercase" @click.stop.prevent="copyText(file.InstanceID)">{{ file.InstanceID }}</span>
<span
class="clickable text-uppercase"
@click.stop.prevent="copyText(file.InstanceID)"
>{{ file.InstanceID }}</span
>
</td>
</tr>
<tr>
@@ -71,7 +141,9 @@
{{ $gettext(`Hash`) }}
</td>
<td class="text-break">
<span class="clickable text-break" @click.stop.prevent="copyText(file.Hash)">{{ file.Hash }}</span>
<span class="clickable text-break" @click.stop.prevent="copyText(file.Hash)">{{
file.Hash
}}</span>
</td>
</tr>
<tr v-if="file.Name">
@@ -92,7 +164,11 @@
<td>
{{ $gettext(`Original Name`) }}
</td>
<td class="text-break"><span class="clickable" @click.stop.prevent="copyText(file.OriginalName)">{{ file.OriginalName }}</span></td>
<td class="text-break"
><span class="clickable" @click.stop.prevent="copyText(file.OriginalName)">{{
file.OriginalName
}}</span></td
>
</tr>
<tr>
<td>
@@ -197,7 +273,14 @@
item-title="text"
item-value="value"
:list-props="{ density: 'compact' }"
:readonly="readonly || !features.edit || file.Error || (file.Frames && file.Frames > 1) || (file.Duration && file.Duration > 1) || (file.FileType !== 'jpg' && file.FileType !== 'png')"
:readonly="
readonly ||
!features.edit ||
file.Error ||
(file.Frames && file.Frames > 1) ||
(file.Duration && file.Duration > 1) ||
(file.FileType !== 'jpg' && file.FileType !== 'png')
"
:disabled="busy"
class="input-orientation"
@update:model-value="changeOrientation(file)"
@@ -226,15 +309,19 @@
<td>
{{ $gettext(`Main Color`) }}
</td>
<!-- TODO: change filter-->
<!-- <td>{{ file.MainColor | capitalize }}</td>-->
<td>{{ file.MainColor }}</td>
<td class="text-capitalize">{{ file.MainColor }}</td>
</tr>
<tr v-if="file.Chroma">
<td>
{{ $gettext(`Chroma`) }}
</td>
<td><v-progress-linear :model-value="file.Chroma" style="max-width: 300px" :title="`${file.Chroma}%`"></v-progress-linear></td>
<td
><v-progress-linear
:model-value="file.Chroma"
style="max-width: 300px"
:title="`${file.Chroma}%`"
></v-progress-linear
></td>
</tr>
<tr v-if="file.Missing">
<td>
@@ -248,22 +335,20 @@
<td>
{{ $gettext(`Added`) }}
</td>
<td class="text-break">{{ formatTime(file.CreatedAt) }}
<td class="text-break"
>{{ formatTime(file.CreatedAt) }}
{{ $gettext(`in`) }}
<!-- TODO: change filter-->
<!-- {{ Math.round(file.CreatedIn / 1000000) | number("0,0") }} ms-->
{{ Math.round(file.CreatedIn / 1000000) }} ms
{{ $util.formatNs(file.CreatedIn) }}
</td>
</tr>
<tr v-if="file.UpdatedIn">
<td>
{{ $gettext(`Updated`) }}
</td>
<td class="text-break">{{ formatTime(file.UpdatedAt) }}
<td class="text-break"
>{{ formatTime(file.UpdatedAt) }}
{{ $gettext(`in`) }}
<!-- TODO: change filter-->
<!-- {{ Math.round(file.UpdatedIn / 1000000) | number("0,0") }} ms-->
{{ Math.round(file.UpdatedIn / 1000000) }} ms
{{ $util.formatNs(file.UpdatedIn) }}
</td>
</tr>
</tbody>
@@ -278,7 +363,11 @@
</v-expansion-panel>
</template>
</v-expansion-panels>
<p-file-delete-dialog :show="deleteFile.dialog" @cancel="closeDeleteDialog" @confirm="confirmDeleteFile"></p-file-delete-dialog>
<p-file-delete-dialog
:show="deleteFile.dialog"
@cancel="closeDeleteDialog"
@confirm="confirmDeleteFile"
></p-file-delete-dialog>
</div>
</template>
@@ -312,7 +401,8 @@ export default {
config: this.$config.values,
readonly: this.$config.get("readonly"),
experimental: this.$config.get("experimental"),
canAccessPrivate: this.$config.allow("photos", "access_library") && this.$config.allow("photos", "access_private"),
canAccessPrivate:
this.$config.allow("photos", "access_library") && this.$config.allow("photos", "access_private"),
options: options,
busy: false,
rtl: this.$rtl,

View File

@@ -29,6 +29,12 @@ describe("common/util", () => {
const duration = Util.formatDuration(600000000000);
assert.equal(duration, "10:00");
});
it("should return formatted milliseconds", () => {
const short = Util.formatNs(45065875);
assert.equal(short, "45 ms");
const long = Util.formatNs(45065875453454);
assert.equal(long, "45,065,875 ms");
});
it("should return formatted camera name", () => {
const iPhone15Pro = Util.formatCamera({ Make: "Apple", Model: "iPhone 15 Pro" }, 23, "Apple", "iPhone 15 Pro");
assert.equal(iPhone15Pro, "iPhone 15 Pro");