Move exif info into common "details" view

This commit is contained in:
N-Pex 2025-09-23 12:52:52 +02:00
parent 609e4a97c2
commit 0f7b9ac7ab
12 changed files with 308 additions and 230 deletions

View file

@ -1,29 +1,27 @@
<template>
<div v-if="props.flags">
<div class="cc-detail-info" v-if="infoText !== undefined">
{{ infoText }}
</div>
<div class="attachment-info__detail-box">
<div class="detail-title">
{{ t("cc.content_credentials") }}
<v-icon>$vuetify.icons.ic_cr</v-icon>
{{ t("cc.details") }}
<v-icon v-if="hasC2PA">$vuetify.icons.ic_cr</v-icon>
</div>
<CCProperty v-for="d in details" :icon="d.icon" :title="d.title" :value="d.value">
<template v-if="d.link" v-slot:default>
<a :href="d.link">{{ d.value }}</a>
</template>
</CCProperty>
<div class="detail-subtitle" v-if="hasC2PA">
{{ t("cc.content_credentials_info") }}
<i18n-t keypath="cc.cc_info" tag="span">
<template v-slot:link>
<a href="https://contentcredentials.org/verify" target="_blank">{{ $t("cc.cc_info_link") }}</a>
<v-icon>$vuetify.icons.ic_copy</v-icon>
</template>
</i18n-t>
</div>
<div class="cc-detail-info" v-if="infoText !== undefined">
{{ infoText }}
</div>
<CCProperty
v-if="props.flags?.device"
icon="$vuetify.icons.ic_media_camera"
:title="t('file_mode.cc_source')"
:value="props.flags?.device"
/>
<CCProperty
v-if="creationDate"
icon="$vuetify.icons.ic_exif_time"
:title="t('file_mode.cc_capture_timestamp')"
:value="creationDate"
/>
</div>
</template>
@ -32,7 +30,7 @@ import { useI18n } from "vue-i18n";
import { Proof, ProofHintFlags } from "../../models/proof";
import { computed, ref, Ref, watch } from "vue";
import dayjs from "dayjs";
import CCProperty from "./CCProperty.vue";
import CCProperty, { CCPropertyProps } from "./CCProperty.vue";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime);
@ -42,15 +40,109 @@ const { t } = useI18n();
const props = defineProps<{
proof?: Proof;
flags?: ProofHintFlags;
metaStripped?: boolean;
}>();
const infoText: Ref<string | undefined> = ref(undefined);
const creationDate: Ref<string | undefined> = ref(undefined);
const details: Ref<(CCPropertyProps & { link?: string })[]> = ref([]);
const hasC2PA = computed(() => {
return props.proof?.integrity?.c2pa !== undefined;
});
const updateDetails = () => {
let d: (CCPropertyProps & { link?: string })[] = [];
if (props.flags?.device) {
d.push({
icon: "$vuetify.icons.ic_media_camera",
title: t("file_mode.cc_source"),
value: props.flags?.device,
});
}
if (creationDate.value) {
d.push({
icon: "$vuetify.icons.ic_exif_time",
title: t("file_mode.cc_capture_timestamp"),
value: creationDate.value,
});
}
const exif = props.proof?.integrity?.exif;
if (exif) {
const getSimpleValue = (key: string): string | undefined => {
return exif ? (exif[key] as string)?.replace(/^"(.+(?="$))"$/, "$1") : undefined;
};
const toDegrees = (dms: string | undefined, direction: string) => {
if (!dms || dms.length == 0) return undefined;
var parts = dms.split(/deg|min|sec/);
var d = parts[0];
var m = parts[1];
var s = parts[2];
var deg = (Number(d) + Number(m) / 60 + Number(s) / 3600).toFixed(6);
if (direction == "S" || direction == "W") {
deg = "-" + deg;
}
return deg;
};
// Make and model
let makeAndModel = "";
const make = getSimpleValue("Make");
const model = getSimpleValue("Model");
if (make) {
makeAndModel += make;
}
if (model) {
if (makeAndModel.length > 0) {
makeAndModel += ", ";
}
makeAndModel += model;
}
if (makeAndModel.length > 0) {
d.push({
icon: "$vuetify.icons.ic_exif_device_camera",
title: t("file_mode.cc_source"),
value: makeAndModel,
});
}
// Creation date
const date = getSimpleValue("DateTimeOriginal");
const dateOffset = getSimpleValue("OffsetTimeOriginal");
if (date) {
d.push({
icon: "$vuetify.icons.ic_exif_time",
title: t("file_mode.cc_capture_timestamp"),
value: dayjs(Date.parse(date + (dateOffset ? dateOffset : ""))).format("lll"),
});
}
// Location
try {
const lat = toDegrees(getSimpleValue("GPSLatitude"), getSimpleValue("GPSLatitudeRef") ?? "");
const lon = toDegrees(getSimpleValue("GPSLongitude"), getSimpleValue("GPSLongitudeRef") ?? "");
if (lat && lon && lat.length > 0 && lon.length > 0) {
const location = lat + " " + lon;
const link =
"https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(lat) + "," + encodeURIComponent(lon);
d.push({
icon: "$vuetify.icons.ic_exif_location",
title: t("file_mode.cc_location"),
value: location,
link: link,
});
}
} catch (error) {}
}
details.value = d;
};
watch(
props,
() => {
@ -84,9 +176,15 @@ watch(
result += t("file_mode.old_photo");
}
if (props.metaStripped) {
result += t("cc.metadata-stripped");
}
infoText.value = result === "" ? undefined : result;
}
} catch (error) {}
updateDetails();
},
{ immediate: true }
);

View file

@ -11,11 +11,12 @@
</template>
<script setup lang="ts">
const props = defineProps<{
export type CCPropertyProps = {
icon: string;
title: string;
value?: string;
}>();
}
const props = defineProps<CCPropertyProps>();
</script>
<style lang="scss">

View file

@ -31,11 +31,15 @@ const infoText = computed(() => {
return "<b>This image is generated by AI.</b> Take a closer look at the file details.";
} else if (flags[0].generator === "screenshot") {
return "<b>This is a screenshot.</b> Take a closer look at the file details.";
} else if (flags[0].edits) {
return "<b>This image has been modified.</b> Take a closer look at the file details.";
}
} else if (flags.some((f) => f.generator === "ai")) {
return "<b>Contains AI generated media.</b> Take a closer look at the file details for each.";
} else if (flags.some((f) => f.generator === "screenshot")) {
return "<b>Contains screenshots.</b> Take a closer look at the file details for each.";
} else if (flags.some((f) => f.edits?.length)) {
return "<b>Contains modified media.</b> Take a closer look at the file details for each.";
}
return "TODO - Content Credentials Info";
});

View file

@ -1,112 +0,0 @@
<template>
<div v-if="exif">
<div class="detail-title">
{{ t("file_mode.exif_data") }}
</div>
<CCProperty v-if="makeAndModel" icon="$vuetify.icons.ic_exif_device_camera" :title="t('file_mode.cc_source')" :value="makeAndModel" />
<CCProperty v-if="dateTime" icon="$vuetify.icons.ic_exif_time" :title="t('file_mode.cc_capture_timestamp')" :value="dateTime" />
<CCProperty v-if="location" icon="$vuetify.icons.ic_exif_location" :title="t('file_mode.cc_location')">
<template v-slot:default>
<a :href="locationLink">{{ location }}</a>
</template>
</CCProperty>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
import CCProperty from "./CCProperty.vue";
const { t } = useI18n();
const props = defineProps<{
exif?: { [key: string]: string | Object };
}>();
const { exif } = props;
const getSimpleValue = (key: string): string | undefined => {
return exif ? (exif[key] as string)?.replace(/^"(.+(?="$))"$/, "$1") : undefined;
};
const toDegrees = (dms: string, direction: string) => {
var parts = dms.split(/deg|min|sec/);
var d = parts[0];
var m = parts[1];
var s = parts[2];
var deg = (Number(d) + Number(m)/60 + Number(s)/3600).toFixed(6);
if (direction == "S" || direction == "W") {
deg = "-" + deg;
}
return deg;
}
const getLocation = () => {
try {
if (exif) {
const gpsLat = getSimpleValue("GPSLatitude");
const gpsLon = getSimpleValue("GPSLongitude");
if (gpsLat && gpsLon) {
const lat = toDegrees(gpsLat, getSimpleValue("GPSLatitudeRef") ?? "");
const lon = toDegrees(gpsLon, getSimpleValue("GPSLongitudeRef") ?? "");
return {lat, lon};
}
}
} catch (error) {}
return undefined;
}
const location = computed(() => {
const pos = getLocation();
if (pos) {
return pos.lat + " " + pos.lon;
}
return undefined;
});
const locationLink = computed(() => {
const pos = getLocation();
if (pos) {
return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent(pos.lat) + "," + encodeURIComponent(pos.lon);
}
return undefined;
});
const dateTime = computed(() => {
try {
if (exif) {
const date = getSimpleValue("DateTimeOriginal");
const dateOffset = getSimpleValue("OffsetTimeOriginal");
if (date) {
return dayjs(Date.parse(date + (dateOffset ? dateOffset : ""))).format("lll");
}
}
} catch (error) {}
return undefined;
});
const makeAndModel = computed(() => {
let result = "";
if (exif) {
const make = getSimpleValue("Make");
const model = getSimpleValue("Model");
if (make) {
result += make;
}
if (model) {
if (result.length > 0) {
result += ", ";
}
result += model;
}
}
return result.length > 0 ? result : undefined;
});
</script>
<style lang="scss">
@use "@/assets/css/chat.scss" as *;
</style>