mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Frontend: Replace "number" output filter with $util.formatNs() #3168
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
text = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
// Make URLs clickable.
|
||||
text = text.replace(linkRegex, linkFunc);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user