parent
862d742efc
commit
847451d8be
|
@ -12,6 +12,8 @@ You may need to clear browser cache to use location search.
|
||||||
- **Feature**: Allow editing of GPS coordinates ([#418](https://github.com/pulsejet/memories/issues/418))
|
- **Feature**: Allow editing of GPS coordinates ([#418](https://github.com/pulsejet/memories/issues/418))
|
||||||
- **Feature**: Allow bulk editing of EXIF attributes other than date/time
|
- **Feature**: Allow bulk editing of EXIF attributes other than date/time
|
||||||
- **Feature**: Allow (optionally bulk) editing of collaborative tags
|
- **Feature**: Allow (optionally bulk) editing of collaborative tags
|
||||||
|
- **Feature**: Allow sharing single photo / video ([#307](https://github.com/pulsejet/memories/issues/307))
|
||||||
|
- **Feature**: Allow sharing videos ([#261](https://github.com/pulsejet/memories/issues/261))
|
||||||
- **Feature**: Show list of tags in sidebar
|
- **Feature**: Show list of tags in sidebar
|
||||||
- **Feature**: Better configurability and feature detection for go-vod ([#450](https://github.com/pulsejet/memories/issues/450))
|
- **Feature**: Better configurability and feature detection for go-vod ([#450](https://github.com/pulsejet/memories/issues/450))
|
||||||
- **Feature**: Configurable folder/album sorting order ([#371](https://github.com/pulsejet/memories/issues/371))
|
- **Feature**: Configurable folder/album sorting order ([#371](https://github.com/pulsejet/memories/issues/371))
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<EditMetadataModal />
|
<EditMetadataModal />
|
||||||
<NodeShareModal />
|
<NodeShareModal />
|
||||||
|
<ShareModal />
|
||||||
</NcContent>
|
</NcContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ import Metadata from "./components/Metadata.vue";
|
||||||
import Sidebar from "./components/Sidebar.vue";
|
import Sidebar from "./components/Sidebar.vue";
|
||||||
import EditMetadataModal from "./components/modal/EditMetadataModal.vue";
|
import EditMetadataModal from "./components/modal/EditMetadataModal.vue";
|
||||||
import NodeShareModal from "./components/modal/NodeShareModal.vue";
|
import NodeShareModal from "./components/modal/NodeShareModal.vue";
|
||||||
|
import ShareModal from "./components/modal/ShareModal.vue";
|
||||||
|
|
||||||
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
|
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue";
|
||||||
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
import FolderIcon from "vue-material-design-icons/Folder.vue";
|
||||||
|
@ -99,6 +101,7 @@ export default defineComponent({
|
||||||
Sidebar,
|
Sidebar,
|
||||||
EditMetadataModal,
|
EditMetadataModal,
|
||||||
NodeShareModal,
|
NodeShareModal,
|
||||||
|
ShareModal,
|
||||||
|
|
||||||
ImageMultiple,
|
ImageMultiple,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
|
|
|
@ -0,0 +1,247 @@
|
||||||
|
<template>
|
||||||
|
<Modal @close="close" size="normal" v-if="photo">
|
||||||
|
<template #title>
|
||||||
|
{{ t("memories", "Share File") }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="loading-icon fill-block" v-if="loading > 0">
|
||||||
|
<NcLoadingIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="options" v-else>
|
||||||
|
<NcListItem
|
||||||
|
v-if="canShareNative && !isVideo"
|
||||||
|
:title="t('memories', 'Reduced Size')"
|
||||||
|
:bold="false"
|
||||||
|
@click.prevent="sharePreview()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PhotoIcon class="avatar" :size="24" />
|
||||||
|
</template>
|
||||||
|
<template #subtitle>
|
||||||
|
{{ t("memories", "Share a lower resolution image preview") }}
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
|
||||||
|
<NcListItem
|
||||||
|
v-if="canShareNative && !isVideo"
|
||||||
|
:title="t('memories', 'High Resolution')"
|
||||||
|
:bold="false"
|
||||||
|
@click.prevent="shareHighRes()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<LargePhotoIcon class="avatar" :size="24" />
|
||||||
|
</template>
|
||||||
|
<template #subtitle>
|
||||||
|
{{ t("memories", "Share the image as a high-quality JPEG") }}
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
|
||||||
|
<NcListItem
|
||||||
|
v-if="canShareNative"
|
||||||
|
:title="t('memories', 'Original File')"
|
||||||
|
:bold="false"
|
||||||
|
@click.prevent="shareOriginal()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<LargePhotoIcon class="avatar" :size="24" />
|
||||||
|
</template>
|
||||||
|
<template #subtitle>
|
||||||
|
{{ t("memories", "Share the original image / video file") }}
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
|
||||||
|
<NcListItem
|
||||||
|
v-if="canShareLink"
|
||||||
|
:title="t('memories', 'Public Link')"
|
||||||
|
:bold="false"
|
||||||
|
@click.prevent="shareLink()"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<LinkIcon class="avatar" :size="24" />
|
||||||
|
</template>
|
||||||
|
<template #subtitle>
|
||||||
|
{{ t("memories", "Share an external Nextcloud link") }}
|
||||||
|
</template>
|
||||||
|
</NcListItem>
|
||||||
|
</ul>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import { showError } from "@nextcloud/dialogs";
|
||||||
|
|
||||||
|
import NcListItem from "@nextcloud/vue/dist/Components/NcListItem";
|
||||||
|
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
|
import { IFileInfo, IPhoto } from "../../types";
|
||||||
|
import * as dav from "../../services/DavRequests";
|
||||||
|
|
||||||
|
import PhotoIcon from "vue-material-design-icons/Image.vue";
|
||||||
|
import LargePhotoIcon from "vue-material-design-icons/ImageArea.vue";
|
||||||
|
import LinkIcon from "vue-material-design-icons/LinkVariant.vue";
|
||||||
|
import { fetchImage } from "../frame/XImgCache";
|
||||||
|
import { getPreviewUrl } from "../../services/FileUtils";
|
||||||
|
import { API } from "../../services/API";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "ShareModal",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
NcListItem,
|
||||||
|
NcLoadingIcon,
|
||||||
|
Modal,
|
||||||
|
|
||||||
|
PhotoIcon,
|
||||||
|
LargePhotoIcon,
|
||||||
|
LinkIcon,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
photo: null as IPhoto,
|
||||||
|
loading: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
globalThis.sharePhoto = (photo: IPhoto) => {
|
||||||
|
this.photo = photo;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
isVideo() {
|
||||||
|
return (
|
||||||
|
this.photo &&
|
||||||
|
(this.photo.mimetype.startsWith("video/") ||
|
||||||
|
this.photo.flag & this.c.FLAG_IS_VIDEO)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
canShareNative() {
|
||||||
|
return "share" in navigator;
|
||||||
|
},
|
||||||
|
|
||||||
|
canShareLink() {
|
||||||
|
return this.photo?.imageInfo?.permissions?.includes("S");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.photo = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async l(cb: Function) {
|
||||||
|
try {
|
||||||
|
this.loading++;
|
||||||
|
await cb();
|
||||||
|
} finally {
|
||||||
|
this.loading--;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getFileInfo() {
|
||||||
|
if (this.$route.name.endsWith("-share")) {
|
||||||
|
return this.photo as IFileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await dav.getFiles([this.photo]))[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
async sharePreview() {
|
||||||
|
await this.l(async () => {
|
||||||
|
const src = getPreviewUrl(this.photo, false, 2048);
|
||||||
|
const blob = await fetchImage(src);
|
||||||
|
await this.shareBlob(blob, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async shareHighRes() {
|
||||||
|
await this.l(async () => {
|
||||||
|
const src = API.IMAGE_JPEG(this.photo.fileid);
|
||||||
|
const blob = await fetchImage(src);
|
||||||
|
await this.shareBlob(blob, true);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async shareOriginal() {
|
||||||
|
await this.l(async () => {
|
||||||
|
const src = dav.getDownloadLink(this.photo);
|
||||||
|
const blob = await fetch(src).then((r) => r.blob());
|
||||||
|
await this.shareBlob(blob);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async shareLink() {
|
||||||
|
this.l(async () =>
|
||||||
|
globalThis.shareNodeLink((await this.getFileInfo()).filename)
|
||||||
|
);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
|
||||||
|
async shareBlob(blob: Blob, replaceExt = false) {
|
||||||
|
const fileInfo = await this.getFileInfo();
|
||||||
|
let basename = fileInfo.originalBasename || fileInfo.basename;
|
||||||
|
|
||||||
|
if (replaceExt) {
|
||||||
|
// Fix basename extension
|
||||||
|
let targetExts = [];
|
||||||
|
if (blob.type === "image/png") {
|
||||||
|
targetExts = ["png"];
|
||||||
|
} else {
|
||||||
|
targetExts = ["jpg", "jpeg"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append extension if not found
|
||||||
|
if (!targetExts.includes(basename.split(".").pop().toLowerCase())) {
|
||||||
|
basename += "." + targetExts[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
files: [
|
||||||
|
new File([blob], basename, {
|
||||||
|
type: blob.type,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!(<any>navigator).canShare(data)) {
|
||||||
|
showError(this.t("memories", "Cannot share this type of data"));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.share(data);
|
||||||
|
} catch (e) {
|
||||||
|
// Don't show this error because it's silly stuff
|
||||||
|
// like "share canceled"
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading-icon {
|
||||||
|
min-height: 240px;
|
||||||
|
:deep svg {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.options {
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
:deep .avatar {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -351,7 +351,7 @@ export default defineComponent({
|
||||||
|
|
||||||
/** Show share button */
|
/** Show share button */
|
||||||
canShare(): boolean {
|
canShare(): boolean {
|
||||||
return "share" in navigator && this.currentPhoto && !this.isVideo;
|
return Boolean(this.currentPhoto);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -879,63 +879,7 @@ export default defineComponent({
|
||||||
|
|
||||||
/** Share the current photo externally */
|
/** Share the current photo externally */
|
||||||
async shareCurrent() {
|
async shareCurrent() {
|
||||||
try {
|
globalThis.sharePhoto(this.currentPhoto);
|
||||||
// Check navigator support
|
|
||||||
if (!this.canShare) throw new Error("Share not supported");
|
|
||||||
|
|
||||||
// Shre image data using navigator api
|
|
||||||
const photo = this.currentPhoto;
|
|
||||||
if (!photo) return;
|
|
||||||
|
|
||||||
// No videos yet
|
|
||||||
if (this.isVideo)
|
|
||||||
throw new Error(
|
|
||||||
this.t("memories", "Video sharing not supported yet")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get image blob
|
|
||||||
const imgSrc = this.photoswipe.currSlide.data.src;
|
|
||||||
const blob = await fetchImage(imgSrc);
|
|
||||||
|
|
||||||
// Fix basename extension
|
|
||||||
let basename = photo.basename;
|
|
||||||
let targetExts = [];
|
|
||||||
if (photo.mimetype === "image/png") {
|
|
||||||
targetExts = ["png"];
|
|
||||||
} else {
|
|
||||||
targetExts = ["jpg", "jpeg"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append extension if not found
|
|
||||||
if (!targetExts.includes(basename.split(".").pop().toLowerCase())) {
|
|
||||||
basename += "." + targetExts[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
files: [
|
|
||||||
new File([blob], basename, {
|
|
||||||
type: blob.type,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
title: photo.basename,
|
|
||||||
text: photo.basename,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(<any>navigator).canShare(data)) {
|
|
||||||
throw new Error(this.t("memories", "Cannot share this type of data"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await navigator.share(data);
|
|
||||||
} catch (e) {
|
|
||||||
// Don't show this error because it's silly stuff
|
|
||||||
// like "share canceled"
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err.name, err.message);
|
|
||||||
showError(err.message);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Key press events */
|
/** Key press events */
|
||||||
|
|
|
@ -23,6 +23,7 @@ declare global {
|
||||||
var OCP: Nextcloud.v24.OCP;
|
var OCP: Nextcloud.v24.OCP;
|
||||||
|
|
||||||
var editMetadata: (photos: IPhoto[], sections?: number[]) => void;
|
var editMetadata: (photos: IPhoto[], sections?: number[]) => void;
|
||||||
|
var sharePhoto: (photo: IPhoto) => void;
|
||||||
var shareNodeLink: (path: string) => void;
|
var shareNodeLink: (path: string) => void;
|
||||||
|
|
||||||
var mSidebar: {
|
var mSidebar: {
|
||||||
|
|
Loading…
Reference in New Issue