Move exif info into common "details" view
This commit is contained in:
parent
609e4a97c2
commit
0f7b9ac7ab
12 changed files with 308 additions and 230 deletions
|
|
@ -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 }
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue