nx: implement multi-share (fix #901)
Signed-off-by: Varun Patil <radialapps@gmail.com>pulsejet/aio-hw-docs
parent
fe74b9f089
commit
14a890796e
|
@ -29,7 +29,6 @@ use OCA\Memories\Exif;
|
||||||
use OCA\Memories\Service;
|
use OCA\Memories\Service;
|
||||||
use OCA\Memories\Util;
|
use OCA\Memories\Util;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\FileDisplayResponse;
|
|
||||||
use OCP\AppFramework\Http\JSONResponse;
|
use OCP\AppFramework\Http\JSONResponse;
|
||||||
use OCP\Files\IRootFolder;
|
use OCP\Files\IRootFolder;
|
||||||
|
|
||||||
|
@ -56,11 +55,22 @@ class ImageController extends GenericApiController
|
||||||
throw Exceptions::MissingParameter('id, x, y');
|
throw Exceptions::MissingParameter('id, x, y');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get preview for this file
|
||||||
$file = $this->fs->getUserFile($id);
|
$file = $this->fs->getUserFile($id);
|
||||||
$preview = \OC::$server->get(\OCP\IPreview::class)->getPreview($file, $x, $y, !$a, $mode);
|
$preview = \OC::$server->get(\OCP\IPreview::class)->getPreview($file, $x, $y, !$a, $mode);
|
||||||
$response = new FileDisplayResponse($preview, Http::STATUS_OK, [
|
|
||||||
'Content-Type' => $preview->getMimeType(),
|
// Get the filename. We need to move the extension from
|
||||||
]);
|
// the preview file to the filename's end if it's not there
|
||||||
|
// Do the comparison case-insensitive
|
||||||
|
$filename = $file->getName();
|
||||||
|
if ($ext = pathinfo($preview->getName(), PATHINFO_EXTENSION)) {
|
||||||
|
if (!str_ends_with(strtolower($filename), strtolower('.'.$ext))) {
|
||||||
|
$filename .= '.'.$ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate response with proper content-disposition
|
||||||
|
$response = new Http\DataDownloadResponse($preview->getContent(), $filename, $preview->getMimeType());
|
||||||
$response->cacheFor(3600 * 24, false, true);
|
$response->cacheFor(3600 * 24, false, true);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
@ -293,13 +303,17 @@ class ImageController extends GenericApiController
|
||||||
/** @var string Blob of image */
|
/** @var string Blob of image */
|
||||||
$blob = $file->getContent();
|
$blob = $file->getContent();
|
||||||
|
|
||||||
|
/** @var string Name of file */
|
||||||
|
$name = $file->getName();
|
||||||
|
|
||||||
// Convert image to JPEG if required
|
// Convert image to JPEG if required
|
||||||
if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg', 'image/gif'], true)) {
|
if (!\in_array($mimetype, ['image/png', 'image/webp', 'image/jpeg', 'image/gif'], true)) {
|
||||||
[$blob, $mimetype] = $this->getImageJPEG($blob, $mimetype);
|
[$blob, $mimetype] = $this->getImageJPEG($blob, $mimetype);
|
||||||
|
$name .= '.jpg';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the image
|
// Return the image
|
||||||
$response = new Http\DataDisplayResponse($blob, Http::STATUS_OK, ['Content-Type' => $mimetype]);
|
$response = new Http\DataDownloadResponse($blob, $name, $mimetype);
|
||||||
$response->cacheFor(3600 * 24, false, false);
|
$response->cacheFor(3600 * 24, false, false);
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
|
|
|
@ -50,6 +50,7 @@ import * as dav from '@services/dav';
|
||||||
import * as utils from '@services/utils';
|
import * as utils from '@services/utils';
|
||||||
import * as nativex from '@native';
|
import * as nativex from '@native';
|
||||||
|
|
||||||
|
import ShareIcon from 'vue-material-design-icons/ShareVariant.vue';
|
||||||
import StarIcon from 'vue-material-design-icons/Star.vue';
|
import StarIcon from 'vue-material-design-icons/Star.vue';
|
||||||
import DownloadIcon from 'vue-material-design-icons/Download.vue';
|
import DownloadIcon from 'vue-material-design-icons/Download.vue';
|
||||||
import DeleteIcon from 'vue-material-design-icons/TrashCanOutline.vue';
|
import DeleteIcon from 'vue-material-design-icons/TrashCanOutline.vue';
|
||||||
|
@ -203,6 +204,12 @@ export default defineComponent({
|
||||||
callback: this.deleteSelection.bind(this),
|
callback: this.deleteSelection.bind(this),
|
||||||
if: () => this.routeIsAlbums,
|
if: () => this.routeIsAlbums,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: t('memories', 'Share'),
|
||||||
|
icon: ShareIcon,
|
||||||
|
callback: this.shareSelection.bind(this),
|
||||||
|
if: () => !this.routeIsAlbums,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: t('memories', 'Download'),
|
name: t('memories', 'Download'),
|
||||||
icon: DownloadIcon,
|
icon: DownloadIcon,
|
||||||
|
@ -809,6 +816,13 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share the currently selected photos
|
||||||
|
*/
|
||||||
|
shareSelection(selection: Selection) {
|
||||||
|
_m.modals.sharePhotos(selection.photosNoDupFileId());
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the edit date dialog
|
* Open the edit date dialog
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal ref="modal" @close="cleanup" size="normal" v-if="show">
|
<Modal ref="modal" @close="cleanup" size="normal" v-if="show">
|
||||||
<template #title>
|
<template #title>
|
||||||
{{ t('memories', 'Share File') }}
|
{{ n('memories', 'Share File', 'Share Files', photos?.length ?? 0) }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="loading-icon fill-block" v-if="loading > 0">
|
<div class="loading-icon fill-block" v-if="loading > 0">
|
||||||
|
@ -10,25 +10,21 @@
|
||||||
|
|
||||||
<ul class="options" v-else>
|
<ul class="options" v-else>
|
||||||
<NcListItem
|
<NcListItem
|
||||||
v-if="canShareNative && canShareTranscode"
|
v-if="canShareNative && canShareLowRes"
|
||||||
:title="t('memories', 'Reduced Size')"
|
:title="t('memories', 'Reduced Size')"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
@click.prevent="sharePreview()"
|
@click.prevent="shareLowRes()"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<PhotoIcon class="avatar" :size="24" />
|
<PhotoIcon class="avatar" :size="24" />
|
||||||
</template>
|
</template>
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
{{
|
{{ t('memories', 'Share in lower quality (small file size)') }}
|
||||||
isVideo
|
|
||||||
? t('memories', 'Share the video as a low quality MP4')
|
|
||||||
: t('memories', 'Share a lower resolution image preview')
|
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
</NcListItem>
|
</NcListItem>
|
||||||
|
|
||||||
<NcListItem
|
<NcListItem
|
||||||
v-if="canShareNative && canShareTranscode"
|
v-if="canShareNative && canShareHighRes"
|
||||||
:title="t('memories', 'High Resolution')"
|
:title="t('memories', 'High Resolution')"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
@click.prevent="shareHighRes()"
|
@click.prevent="shareHighRes()"
|
||||||
|
@ -37,11 +33,7 @@
|
||||||
<LargePhotoIcon class="avatar" :size="24" />
|
<LargePhotoIcon class="avatar" :size="24" />
|
||||||
</template>
|
</template>
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
{{
|
{{ t('memories', 'Share in high quality (large file size)') }}
|
||||||
isVideo
|
|
||||||
? t('memories', 'Share the video as a high quality MP4')
|
|
||||||
: t('memories', 'Share the image as a high quality JPEG')
|
|
||||||
}}
|
|
||||||
</template>
|
</template>
|
||||||
</NcListItem>
|
</NcListItem>
|
||||||
|
|
||||||
|
@ -55,7 +47,7 @@
|
||||||
<FileIcon class="avatar" :size="24" />
|
<FileIcon class="avatar" :size="24" />
|
||||||
</template>
|
</template>
|
||||||
<template #subtitle>
|
<template #subtitle>
|
||||||
{{ t('memories', 'Share the original image / video file') }}
|
{{ n('memories', 'Share the original file', 'Share the original files', photos?.length ?? 0) }}
|
||||||
</template>
|
</template>
|
||||||
</NcListItem>
|
</NcListItem>
|
||||||
|
|
||||||
|
@ -112,47 +104,57 @@ export default defineComponent({
|
||||||
mixins: [UserConfig, ModalMixin],
|
mixins: [UserConfig, ModalMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
photo: null as IPhoto | null,
|
photos: null as IPhoto[] | null,
|
||||||
loading: 0,
|
loading: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
console.assert(!_m.modals.sharePhoto, 'ShareModal created twice');
|
console.assert(!_m.modals.sharePhotos, 'ShareModal created twice');
|
||||||
_m.modals.sharePhoto = this.open;
|
_m.modals.sharePhotos = this.open;
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
isVideo(): boolean {
|
isSingle(): boolean {
|
||||||
return !!this.photo && (this.photo.mimetype?.startsWith('video/') || !!(this.photo.flag & this.c.FLAG_IS_VIDEO));
|
return this.photos?.length === 1 ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasVideos(): boolean {
|
||||||
|
return !!this.photos?.some(utils.isVideo);
|
||||||
},
|
},
|
||||||
|
|
||||||
canShareNative(): boolean {
|
canShareNative(): boolean {
|
||||||
return 'share' in navigator || nativex.has();
|
return 'share' in navigator || nativex.has();
|
||||||
},
|
},
|
||||||
|
|
||||||
canShareTranscode(): boolean {
|
canShareLowRes(): boolean {
|
||||||
return !this.isLocal && (!this.isVideo || !this.config.vod_disable);
|
// Only allow transcoding videos if a single video is selected
|
||||||
|
return !this.hasLocal && (!this.hasVideos || (!this.config.vod_disable && this.isSingle));
|
||||||
|
},
|
||||||
|
|
||||||
|
canShareHighRes(): boolean {
|
||||||
|
// High-CPU operations only permitted for single node
|
||||||
|
return this.isSingle && this.canShareLowRes;
|
||||||
},
|
},
|
||||||
|
|
||||||
canShareLink(): boolean {
|
canShareLink(): boolean {
|
||||||
return !!this.photo?.imageInfo?.permissions?.includes('S') && !this.routeIsAlbums;
|
return !this.routeIsAlbums && !!this.photos?.every((p) => p?.imageInfo?.permissions?.includes('S'));
|
||||||
},
|
},
|
||||||
|
|
||||||
isLocal(): boolean {
|
hasLocal(): boolean {
|
||||||
return utils.isLocalPhoto(this.photo!);
|
return !!this.photos?.some(utils.isLocalPhoto);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
open(photo: IPhoto) {
|
open(photos: IPhoto[]) {
|
||||||
this.photo = photo;
|
this.photos = photos;
|
||||||
this.loading = 0;
|
this.loading = 0;
|
||||||
this.show = true;
|
this.show = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
this.photo = null;
|
this.photos = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
async l<T>(cb: () => Promise<T>): Promise<T> {
|
async l<T>(cb: () => Promise<T>): Promise<T> {
|
||||||
|
@ -164,93 +166,89 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async sharePreview() {
|
async shareLowRes() {
|
||||||
const src = this.isVideo
|
await this.shareWithHref(
|
||||||
? API.VIDEO_TRANSCODE(this.photo!.fileid, '480p.mp4')
|
this.photos!.map((photo) => ({
|
||||||
: utils.getPreviewUrl({ photo: this.photo!, size: 2048 });
|
auid: String(), // no local
|
||||||
this.shareWithHref(src, true);
|
href: utils.isVideo(photo)
|
||||||
|
? API.VIDEO_TRANSCODE(photo.fileid, '480p.mp4')
|
||||||
|
: utils.getPreviewUrl({ photo, size: 2048 }),
|
||||||
|
})),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareHighRes() {
|
async shareHighRes() {
|
||||||
const fileid = this.photo!.fileid;
|
await this.shareWithHref(
|
||||||
const src = this.isVideo
|
this.photos!.map((photo) => ({
|
||||||
? API.VIDEO_TRANSCODE(fileid, '1080p.mp4')
|
auid: String(), // no local
|
||||||
: API.IMAGE_DECODABLE(fileid, this.photo!.etag);
|
href: utils.isVideo(photo)
|
||||||
this.shareWithHref(src, !this.isVideo);
|
? API.VIDEO_TRANSCODE(photo.fileid, '1080p.mp4')
|
||||||
|
: API.IMAGE_DECODABLE(photo.fileid, photo.etag),
|
||||||
|
})),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareOriginal() {
|
async shareOriginal() {
|
||||||
if (nativex.has()) {
|
await this.shareWithHref(
|
||||||
try {
|
this.photos!.map((photo) => ({
|
||||||
return await this.l(async () => await nativex.shareLocal(this.photo!));
|
auid: photo.auid ?? String(),
|
||||||
} catch (e) {
|
href: dav.getDownloadLink(photo),
|
||||||
// maybe the file doesn't exist locally
|
})),
|
||||||
}
|
);
|
||||||
|
|
||||||
// if it's purel local, we can't share it
|
|
||||||
if (this.isLocal) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.shareWithHref(dav.getDownloadLink(this.photo!));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareLink() {
|
async shareLink() {
|
||||||
const fileInfo = await this.l(async () => (await dav.getFiles([this.photo!]))[0]);
|
const fileInfo = await this.l(async () => (await dav.getFiles([this.photos![0]]))[0]);
|
||||||
await this.close(); // wait till transition is done
|
await this.close(); // wait till transition is done
|
||||||
_m.modals.shareNodeLink(fileInfo.filename, true);
|
_m.modals.shareNodeLink(fileInfo.filename, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a file and then share the blob.
|
* Download a file and then share the blob.
|
||||||
|
*
|
||||||
|
* If a download object includes AUID then local download
|
||||||
|
* is allowed when on NativeX.
|
||||||
*/
|
*/
|
||||||
async shareWithHref(href: string, replaceExt = false) {
|
async shareWithHref(
|
||||||
|
objects: {
|
||||||
|
auid: string;
|
||||||
|
href: string;
|
||||||
|
}[],
|
||||||
|
) {
|
||||||
if (nativex.has()) {
|
if (nativex.has()) {
|
||||||
return await this.l(async () => nativex.shareBlobFromUrl(href));
|
return await this.l(async () => nativex.shareBlobs(objects));
|
||||||
}
|
}
|
||||||
|
|
||||||
let blob: Blob | undefined;
|
// Pull blobs in parallel
|
||||||
await this.l(async () => {
|
const calls = objects.map((obj) => async () => {
|
||||||
const res = await axios.get(href, { responseType: 'blob' });
|
return await this.l(async () => {
|
||||||
blob = res.data;
|
try {
|
||||||
|
return await axios.get(obj.href, { responseType: 'blob' });
|
||||||
|
} catch (e) {
|
||||||
|
showError(this.t('memories', 'Failed to download file {href}', { href: obj.href }));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!blob) {
|
// Get all blobs from parallel calls
|
||||||
showError(this.t('memories', 'Failed to download file'));
|
const files: File[] = [];
|
||||||
return;
|
for await (const responses of dav.runInParallel(calls, 8)) {
|
||||||
}
|
for (const res of responses.filter(Boolean)) {
|
||||||
|
const filename = res!.headers['content-disposition']?.match(/filename="(.+)"/)?.[1] ?? '';
|
||||||
let basename = this.photo?.basename ?? 'blank';
|
const blob = res!.data;
|
||||||
|
files.push(new File([blob], filename, { type: blob.type }));
|
||||||
if (replaceExt) {
|
|
||||||
// Fix basename extension
|
|
||||||
let targetExts: string[] = [];
|
|
||||||
if (blob.type === 'image/png') {
|
|
||||||
targetExts = ['png'];
|
|
||||||
} else {
|
|
||||||
targetExts = ['jpg', 'jpeg'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append extension if not found
|
|
||||||
const baseExt = basename.split('.').pop()?.toLowerCase() ?? '';
|
|
||||||
if (!targetExts.includes(baseExt)) {
|
|
||||||
basename += '.' + targetExts[0];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
const data = {
|
// Check if we can share this type of data
|
||||||
files: [
|
if (!navigator.canShare({ files })) {
|
||||||
new File([blob], basename, {
|
|
||||||
type: blob.type,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!navigator.canShare(data)) {
|
|
||||||
showError(this.t('memories', 'Cannot share this type of data'));
|
showError(this.t('memories', 'Cannot share this type of data'));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.share(data);
|
await navigator.share({ files });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Don't show this error because it's silly stuff
|
// Don't show this error because it's silly stuff
|
||||||
// like "share canceled"
|
// like "share canceled"
|
||||||
|
|
|
@ -986,8 +986,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Share the current photo externally */
|
/** Share the current photo externally */
|
||||||
async shareCurrent() {
|
shareCurrent() {
|
||||||
_m.modals.sharePhoto(this.currentPhoto!);
|
_m.modals.sharePhotos([this.currentPhoto!]);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Key press events */
|
/** Key press events */
|
||||||
|
|
|
@ -40,7 +40,7 @@ declare global {
|
||||||
modals: {
|
modals: {
|
||||||
editMetadata: (photos: IPhoto[], sections?: number[]) => void;
|
editMetadata: (photos: IPhoto[], sections?: number[]) => void;
|
||||||
updateAlbums: (photos: IPhoto[]) => void;
|
updateAlbums: (photos: IPhoto[]) => void;
|
||||||
sharePhoto: (photo: IPhoto) => void;
|
sharePhotos: (photo: IPhoto[]) => void;
|
||||||
shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
|
shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
|
||||||
moveToFolder: (photos: IPhoto[]) => void;
|
moveToFolder: (photos: IPhoto[]) => void;
|
||||||
moveToFace: (photos: IPhoto[]) => void;
|
moveToFace: (photos: IPhoto[]) => void;
|
||||||
|
|
|
@ -69,22 +69,15 @@ export const NAPI = {
|
||||||
*/
|
*/
|
||||||
SHARE_URL: (url: string) => `${BASE_URL}/api/share/url/${euc(euc(url))}`,
|
SHARE_URL: (url: string) => `${BASE_URL}/api/share/url/${euc(euc(url))}`,
|
||||||
/**
|
/**
|
||||||
* Share an object (as blob) natively using a given URL.
|
* Share an object (as blob) natively.
|
||||||
|
* The list of objects to share is already set using setShareBlobs
|
||||||
* The native client MUST download the object using a download manager
|
* The native client MUST download the object using a download manager
|
||||||
* and immediately prompt the user to download it. The asynchronous call
|
* and immediately prompt the user to download it. The asynchronous call
|
||||||
* must return only after the object has been downloaded.
|
* must return only after the object has been downloaded.
|
||||||
* @regex ^/api/share/blob/.+$
|
* @regex ^/api/share/blobs$
|
||||||
* @param url URL to share (double-encoded)
|
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
SHARE_BLOB: (url: string) => `${BASE_URL}/api/share/blob/${euc(euc(url))}`,
|
SHARE_BLOBS: () => `${BASE_URL}/api/share/blobs`,
|
||||||
/**
|
|
||||||
* Share a local file (as blob) with native page.
|
|
||||||
* @regex ^/api/share/local/\d+$
|
|
||||||
* @param auid AUID of the photo
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
SHARE_LOCAL: (auid: string) => `${BASE_URL}/api/share/local/${auid}`,
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow usage of local media (permissions request)
|
* Allow usage of local media (permissions request)
|
||||||
|
@ -139,6 +132,13 @@ export type NativeX = {
|
||||||
*/
|
*/
|
||||||
downloadFromUrl: (url: string, filename: string) => void;
|
downloadFromUrl: (url: string, filename: string) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the list of objects to share with SHARE_BLOB API.
|
||||||
|
* @param objects List of objects to share (JSON-encoded)
|
||||||
|
* @details The SHARE_BLOB API must be called immediately after this call.
|
||||||
|
*/
|
||||||
|
setShareBlobs: (objects: string) => void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Play a video from the given AUID or URL(s).
|
* Play a video from the given AUID or URL(s).
|
||||||
* @param auid AUID of file (will play local if available)
|
* @param auid AUID of file (will play local if available)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import axios from '@nextcloud/axios';
|
import axios from '@nextcloud/axios';
|
||||||
import { BASE_URL, NAPI, nativex } from './api';
|
import { NAPI, nativex } from './api';
|
||||||
import { addOrigin } from './basic';
|
import { addOrigin } from './basic';
|
||||||
import type { IPhoto } from '@typings';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download a file from the given URL.
|
* Download a file from the given URL.
|
||||||
|
@ -29,17 +28,16 @@ export async function shareUrl(url: string) {
|
||||||
/**
|
/**
|
||||||
* Download a blob from the given URL and share it.
|
* Download a blob from the given URL and share it.
|
||||||
*/
|
*/
|
||||||
export async function shareBlobFromUrl(url: string) {
|
export async function shareBlobs(
|
||||||
if (url.startsWith(BASE_URL)) {
|
objects: {
|
||||||
throw new Error('Cannot share localhost URL');
|
auid: string;
|
||||||
}
|
href: string;
|
||||||
await axios.get(NAPI.SHARE_BLOB(addOrigin(url)));
|
}[],
|
||||||
}
|
) {
|
||||||
|
// Make sure all URLs are absolute
|
||||||
|
objects.forEach((obj) => (obj.href = addOrigin(obj.href)));
|
||||||
|
|
||||||
/**
|
// Hand off to native client
|
||||||
* Share a local file with native page.
|
nativex.setShareBlobs(JSON.stringify(objects));
|
||||||
*/
|
await axios.get(NAPI.SHARE_BLOBS());
|
||||||
export async function shareLocal(photo: IPhoto) {
|
|
||||||
if (!photo.auid) throw new Error('Cannot share local file without AUID');
|
|
||||||
await axios.get(NAPI.SHARE_LOCAL(photo.auid));
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,14 @@ export function isLocalPhoto(photo: IPhoto): boolean {
|
||||||
return Boolean(photo?.fileid) && Boolean((photo?.flag ?? 0) & c.FLAG_IS_LOCAL);
|
return Boolean(photo?.fileid) && Boolean((photo?.flag ?? 0) & c.FLAG_IS_LOCAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an object is a video
|
||||||
|
* @param photo Photo object
|
||||||
|
*/
|
||||||
|
export function isVideo(photo: IPhoto): boolean {
|
||||||
|
return !!photo?.mimetype?.startsWith('video/') || !!(photo.flag & c.FLAG_IS_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URL for the imageInfo of a photo
|
* Get the URL for the imageInfo of a photo
|
||||||
*
|
*
|
||||||
|
|
Loading…
Reference in New Issue