feat: allow multi-share with sel manager (close #472)
Signed-off-by: Varun Patil <radialapps@gmail.com>pulsejet/aio-hw-docs
parent
f0e1b00096
commit
0273ae8537
|
@ -62,6 +62,7 @@
|
||||||
<ShareModal />
|
<ShareModal />
|
||||||
<MoveToFolderModal />
|
<MoveToFolderModal />
|
||||||
<FaceMoveModal />
|
<FaceMoveModal />
|
||||||
|
<AlbumShareModal />
|
||||||
</NcContent>
|
</NcContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -91,6 +92,7 @@ import NodeShareModal from '@components/modal/NodeShareModal.vue';
|
||||||
import ShareModal from '@components/modal/ShareModal.vue';
|
import ShareModal from '@components/modal/ShareModal.vue';
|
||||||
import MoveToFolderModal from '@components/modal/MoveToFolderModal.vue';
|
import MoveToFolderModal from '@components/modal/MoveToFolderModal.vue';
|
||||||
import FaceMoveModal from '@components/modal/FaceMoveModal.vue';
|
import FaceMoveModal from '@components/modal/FaceMoveModal.vue';
|
||||||
|
import AlbumShareModal from '@components/modal/AlbumShareModal.vue';
|
||||||
|
|
||||||
import * as utils from '@services/utils';
|
import * as utils from '@services/utils';
|
||||||
import * as nativex from '@native';
|
import * as nativex from '@native';
|
||||||
|
@ -139,6 +141,7 @@ export default defineComponent({
|
||||||
ShareModal,
|
ShareModal,
|
||||||
MoveToFolderModal,
|
MoveToFolderModal,
|
||||||
FaceMoveModal,
|
FaceMoveModal,
|
||||||
|
AlbumShareModal,
|
||||||
|
|
||||||
ImageMultiple,
|
ImageMultiple,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
|
|
|
@ -334,6 +334,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async createPublicLinkForAlbum() {
|
async createPublicLinkForAlbum() {
|
||||||
|
// Check if link already exists
|
||||||
|
if (this.isPublicLinkSelected) {
|
||||||
|
return await this.copyPublicLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new link
|
||||||
this.selectEntity(`${Type.SHARE_TYPE_LINK}`);
|
this.selectEntity(`${Type.SHARE_TYPE_LINK}`);
|
||||||
await this.updateAlbumCollaborators();
|
await this.updateAlbumCollaborators();
|
||||||
try {
|
try {
|
||||||
|
@ -343,9 +349,7 @@ export default defineComponent({
|
||||||
if (!utils.uid) return;
|
if (!utils.uid) return;
|
||||||
const album = await dav.getAlbum(utils.uid, this.albumName);
|
const album = await dav.getAlbum(utils.uid, this.albumName);
|
||||||
this.populateCollaborators(album.collaborators);
|
this.populateCollaborators(album.collaborators);
|
||||||
|
await this.copyPublicLink();
|
||||||
// Direct share if native share is available
|
|
||||||
if (nativex.has()) this.copyPublicLink();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response?.status === 404) {
|
if (error.response?.status === 404) {
|
||||||
this.errorFetchingAlbum = 404;
|
this.errorFetchingAlbum = 404;
|
||||||
|
@ -396,16 +400,12 @@ export default defineComponent({
|
||||||
|
|
||||||
await navigator.clipboard.writeText(link);
|
await navigator.clipboard.writeText(link);
|
||||||
this.publicLinkCopied = true;
|
this.publicLinkCopied = true;
|
||||||
setTimeout(() => {
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
this.publicLinkCopied = false;
|
this.publicLinkCopied = false;
|
||||||
}, 10000);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
selectEntity(collaboratorKey: string) {
|
selectEntity(collaboratorKey: string) {
|
||||||
if (this.selectedCollaboratorsKeys.includes(collaboratorKey)) {
|
if (this.selectedCollaboratorsKeys.includes(collaboratorKey)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refs.popover?.$refs.popover.hide();
|
this.refs.popover?.$refs.popover.hide();
|
||||||
this.selectedCollaboratorsKeys.push(collaboratorKey);
|
this.selectedCollaboratorsKeys.push(collaboratorKey);
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,24 @@
|
||||||
{{ t('memories', 'Share Album') }}
|
{{ t('memories', 'Share Album') }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="showEditFields">
|
||||||
|
<span class="field-title">
|
||||||
|
{{ t('memories', 'Name of the album') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<NcTextField
|
||||||
|
:value.sync="albumName"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
:required="true"
|
||||||
|
autofocus="true"
|
||||||
|
:placeholder="t('memories', 'Name of the album')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<AlbumCollaborators
|
<AlbumCollaborators
|
||||||
v-if="album"
|
v-if="album"
|
||||||
|
ref="collaborators"
|
||||||
:album-name="album.basename"
|
:album-name="album.basename"
|
||||||
:collaborators="album.collaborators"
|
:collaborators="album.collaborators"
|
||||||
:public-link="album.publicLink"
|
:public-link="album.publicLink"
|
||||||
|
@ -16,7 +32,7 @@
|
||||||
:aria-label="t('memories', 'Save collaborators for this album.')"
|
:aria-label="t('memories', 'Save collaborators for this album.')"
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="loadingAddCollaborators"
|
:disabled="loadingAddCollaborators"
|
||||||
@click="handleSetCollaborators(collaborators)"
|
@click="save(collaborators)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<XLoadingIcon v-if="loadingAddCollaborators" />
|
<XLoadingIcon v-if="loadingAddCollaborators" />
|
||||||
|
@ -32,7 +48,10 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
import { showError } from '@nextcloud/dialogs';
|
||||||
|
|
||||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton';
|
import NcButton from '@nextcloud/vue/dist/Components/NcButton';
|
||||||
|
const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
|
||||||
|
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
import ModalMixin from './ModalMixin';
|
import ModalMixin from './ModalMixin';
|
||||||
|
@ -44,6 +63,7 @@ export default defineComponent({
|
||||||
name: 'AlbumShareModal',
|
name: 'AlbumShareModal',
|
||||||
components: {
|
components: {
|
||||||
NcButton,
|
NcButton,
|
||||||
|
NcTextField,
|
||||||
Modal,
|
Modal,
|
||||||
AlbumCollaborators,
|
AlbumCollaborators,
|
||||||
},
|
},
|
||||||
|
@ -54,32 +74,85 @@ export default defineComponent({
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
album: null as any,
|
album: null as any,
|
||||||
|
albumName: String(),
|
||||||
loadingAddCollaborators: false,
|
loadingAddCollaborators: false,
|
||||||
collaborators: [] as any[],
|
collaborators: [] as any[],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
refs() {
|
||||||
|
return this.$refs as {
|
||||||
|
collaborators?: InstanceType<typeof AlbumCollaborators>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
showEditFields() {
|
||||||
|
return this.album?.basename?.startsWith('.link-');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
console.assert(!_m.modals.albumShare, 'AlbumShareModal created twice');
|
||||||
|
_m.modals.albumShare = this.open;
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async open() {
|
async open(user: string, name: string, link?: boolean) {
|
||||||
this.show = true;
|
this.show = true;
|
||||||
|
|
||||||
|
// Load album info
|
||||||
|
try {
|
||||||
this.loadingAddCollaborators = true;
|
this.loadingAddCollaborators = true;
|
||||||
const { user, name } = this.$route.params;
|
this.albumName = name;
|
||||||
this.album = await dav.getAlbum(user, name);
|
this.album = await dav.getAlbum(user, name);
|
||||||
|
} catch {
|
||||||
|
showError(this.t('memories', 'Failed to load album info: {name}', { name }));
|
||||||
|
} finally {
|
||||||
this.loadingAddCollaborators = false;
|
this.loadingAddCollaborators = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we immediately want to share a link
|
||||||
|
if (link) {
|
||||||
|
await this.$nextTick(); // load collaborators component
|
||||||
|
this.refs.collaborators?.createPublicLinkForAlbum();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
this.show = false;
|
this.show = false;
|
||||||
this.album = null;
|
this.album = null;
|
||||||
|
this.albumName = String();
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleSetCollaborators(collaborators: any[]) {
|
async save(collaborators: any[]) {
|
||||||
try {
|
try {
|
||||||
this.loadingAddCollaborators = true;
|
this.loadingAddCollaborators = true;
|
||||||
|
|
||||||
|
// Update album collaborators
|
||||||
await dav.updateAlbum(this.album, {
|
await dav.updateAlbum(this.album, {
|
||||||
albumName: this.album.basename,
|
albumName: this.album.basename,
|
||||||
properties: { collaborators },
|
properties: { collaborators },
|
||||||
});
|
});
|
||||||
this.close();
|
|
||||||
|
// Update album name if changed
|
||||||
|
if (this.album.basename !== this.albumName) {
|
||||||
|
await dav.renameAlbum(this.album, this.album.basename, this.albumName);
|
||||||
|
|
||||||
|
// Change route to new album name if we're on album page
|
||||||
|
if (this.routeIsAlbums) {
|
||||||
|
// Do not await but proceed to close modal instantly
|
||||||
|
this.$router.replace({
|
||||||
|
name: this.$route.name!,
|
||||||
|
params: {
|
||||||
|
user: this.$route.params.user,
|
||||||
|
name: this.albumName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
await this.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -94,4 +167,8 @@ export default defineComponent({
|
||||||
.album-share.loading-icon {
|
.album-share.loading-icon {
|
||||||
height: 350px;
|
height: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.field-title {
|
||||||
|
color: var(--color-text-lighter);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -119,7 +119,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
hasVideos(): boolean {
|
hasVideos(): boolean {
|
||||||
return !!this.photos?.some(utils.isVideo);
|
return Boolean(this.photos?.some(utils.isVideo));
|
||||||
},
|
},
|
||||||
|
|
||||||
canShareNative(): boolean {
|
canShareNative(): boolean {
|
||||||
|
@ -137,11 +137,20 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
canShareLink(): boolean {
|
canShareLink(): boolean {
|
||||||
return !this.routeIsAlbums && !!this.photos?.every((p) => p?.imageInfo?.permissions?.includes('S'));
|
if (this.routeIsAlbums || !this.photos?.length) return false;
|
||||||
|
|
||||||
|
// Check if all imageInfos are loaded (e.g. on viewer)
|
||||||
|
// Then check if all images can be shared
|
||||||
|
if (this.photos.every((p) => !!p.imageInfo)) {
|
||||||
|
return Boolean(this.photos.every((p) => p.imageInfo?.permissions?.includes('S')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If imageInfos are not loaded, fail later
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
hasLocal(): boolean {
|
hasLocal(): boolean {
|
||||||
return !!this.photos?.some(utils.isLocalPhoto);
|
return Boolean(this.photos?.some(utils.isLocalPhoto));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -198,9 +207,58 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
|
|
||||||
async shareLink() {
|
async shareLink() {
|
||||||
const fileInfo = await this.l(async () => (await dav.getFiles([this.photos![0]]))[0]);
|
// Check if we have photos
|
||||||
|
if (!this.photos) return;
|
||||||
|
|
||||||
|
// Fill in image infos to get permissions and paths
|
||||||
|
await this.l(async () => await dav.fillImageInfo(this.photos!));
|
||||||
|
|
||||||
|
// Check if permissions allow sharing
|
||||||
|
for (const photo of this.photos!) {
|
||||||
|
// Error shown by fillImageInfo
|
||||||
|
if (!photo.imageInfo) return;
|
||||||
|
|
||||||
|
// Check if we can share this file
|
||||||
|
if (!photo.imageInfo.permissions?.includes('S')) {
|
||||||
|
const err = this.t('memories', 'Not allowed to share file: {name}', {
|
||||||
|
name: photo.basename ?? photo.fileid,
|
||||||
|
});
|
||||||
|
showError(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open node share modal if single file
|
||||||
|
if (this.photos.length === 1) {
|
||||||
|
const filename = this.photos[0].imageInfo!.filename;
|
||||||
|
if (!filename) return;
|
||||||
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(filename, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate random alphanumeric string name for album
|
||||||
|
const name = '.link-' + (Math.random() + 1).toString(36).substring(2);
|
||||||
|
|
||||||
|
// Create hidden album if multiple files are selected
|
||||||
|
await this.l(async () => {
|
||||||
|
// Create album using WebDAV
|
||||||
|
try {
|
||||||
|
await dav.createAlbum(name);
|
||||||
|
} catch (e) {
|
||||||
|
showError(this.t('memories', 'Failed to create album for public link'));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Album is created, now add photos to it
|
||||||
|
for await (const _ of dav.addToAlbum(utils.uid!, name, this.photos!)) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open album share modal
|
||||||
|
await this.close(); // wait till transition is done
|
||||||
|
await _m.modals.albumShare(utils.uid!, name, true);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</NcActionButton>
|
</NcActionButton>
|
||||||
<NcActionButton
|
<NcActionButton
|
||||||
:aria-label="t('memories', 'Share album')"
|
:aria-label="t('memories', 'Share album')"
|
||||||
@click="refs.shareModal.open()"
|
@click="openShareModal()"
|
||||||
close-after-click
|
close-after-click
|
||||||
v-if="canEditAlbum"
|
v-if="canEditAlbum"
|
||||||
>
|
>
|
||||||
|
@ -89,7 +89,6 @@
|
||||||
|
|
||||||
<AlbumCreateModal ref="createModal" />
|
<AlbumCreateModal ref="createModal" />
|
||||||
<AlbumDeleteModal ref="deleteModal" />
|
<AlbumDeleteModal ref="deleteModal" />
|
||||||
<AlbumShareModal ref="shareModal" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -106,7 +105,6 @@ import axios from '@nextcloud/axios';
|
||||||
|
|
||||||
import AlbumCreateModal from '@components/modal/AlbumCreateModal.vue';
|
import AlbumCreateModal from '@components/modal/AlbumCreateModal.vue';
|
||||||
import AlbumDeleteModal from '@components/modal/AlbumDeleteModal.vue';
|
import AlbumDeleteModal from '@components/modal/AlbumDeleteModal.vue';
|
||||||
import AlbumShareModal from '@components/modal/AlbumShareModal.vue';
|
|
||||||
|
|
||||||
import { downloadWithHandle } from '@services/dav/download';
|
import { downloadWithHandle } from '@services/dav/download';
|
||||||
import { API } from '@services/API';
|
import { API } from '@services/API';
|
||||||
|
@ -132,7 +130,6 @@ export default defineComponent({
|
||||||
|
|
||||||
AlbumCreateModal,
|
AlbumCreateModal,
|
||||||
AlbumDeleteModal,
|
AlbumDeleteModal,
|
||||||
AlbumShareModal,
|
|
||||||
|
|
||||||
BackIcon,
|
BackIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
|
@ -152,7 +149,6 @@ export default defineComponent({
|
||||||
return this.$refs as {
|
return this.$refs as {
|
||||||
createModal: InstanceType<typeof AlbumCreateModal>;
|
createModal: InstanceType<typeof AlbumCreateModal>;
|
||||||
deleteModal: InstanceType<typeof AlbumDeleteModal>;
|
deleteModal: InstanceType<typeof AlbumDeleteModal>;
|
||||||
shareModal: InstanceType<typeof AlbumShareModal>;
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -179,6 +175,10 @@ export default defineComponent({
|
||||||
this.$router.go(-1);
|
this.$router.go(-1);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openShareModal() {
|
||||||
|
_m.modals.albumShare(this.$route.params.user, this.$route.params.name);
|
||||||
|
},
|
||||||
|
|
||||||
async downloadAlbum() {
|
async downloadAlbum() {
|
||||||
const res = await axios.post(API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name));
|
const res = await axios.post(API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name));
|
||||||
if (res.status === 200 && res.data.handle) {
|
if (res.status === 200 && res.data.handle) {
|
||||||
|
|
|
@ -44,6 +44,7 @@ declare global {
|
||||||
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;
|
||||||
|
albumShare: (user: string, name: string, link?: boolean) => Promise<void>;
|
||||||
showSettings: () => void;
|
showSettings: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -365,7 +365,8 @@ export async function fillImageInfo(photos: IPhoto[], query?: { tags?: number },
|
||||||
p.datetaken = res.data.datetaken;
|
p.datetaken = res.data.datetaken;
|
||||||
p.imageInfo = res.data;
|
p.imageInfo = res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get image info for', p.fileid, error);
|
console.error('Failed to get image info', p, error);
|
||||||
|
showError(t('memories', 'Failed to load image info: {name}', { name: p.basename ?? p.fileid }));
|
||||||
} finally {
|
} finally {
|
||||||
done++;
|
done++;
|
||||||
progress?.(done);
|
progress?.(done);
|
||||||
|
|
Loading…
Reference in New Issue