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 />
|
||||
<MoveToFolderModal />
|
||||
<FaceMoveModal />
|
||||
<AlbumShareModal />
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
|
@ -91,6 +92,7 @@ import NodeShareModal from '@components/modal/NodeShareModal.vue';
|
|||
import ShareModal from '@components/modal/ShareModal.vue';
|
||||
import MoveToFolderModal from '@components/modal/MoveToFolderModal.vue';
|
||||
import FaceMoveModal from '@components/modal/FaceMoveModal.vue';
|
||||
import AlbumShareModal from '@components/modal/AlbumShareModal.vue';
|
||||
|
||||
import * as utils from '@services/utils';
|
||||
import * as nativex from '@native';
|
||||
|
@ -139,6 +141,7 @@ export default defineComponent({
|
|||
ShareModal,
|
||||
MoveToFolderModal,
|
||||
FaceMoveModal,
|
||||
AlbumShareModal,
|
||||
|
||||
ImageMultiple,
|
||||
FolderIcon,
|
||||
|
|
|
@ -334,6 +334,12 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async createPublicLinkForAlbum() {
|
||||
// Check if link already exists
|
||||
if (this.isPublicLinkSelected) {
|
||||
return await this.copyPublicLink();
|
||||
}
|
||||
|
||||
// Create new link
|
||||
this.selectEntity(`${Type.SHARE_TYPE_LINK}`);
|
||||
await this.updateAlbumCollaborators();
|
||||
try {
|
||||
|
@ -343,9 +349,7 @@ export default defineComponent({
|
|||
if (!utils.uid) return;
|
||||
const album = await dav.getAlbum(utils.uid, this.albumName);
|
||||
this.populateCollaborators(album.collaborators);
|
||||
|
||||
// Direct share if native share is available
|
||||
if (nativex.has()) this.copyPublicLink();
|
||||
await this.copyPublicLink();
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
this.errorFetchingAlbum = 404;
|
||||
|
@ -396,16 +400,12 @@ export default defineComponent({
|
|||
|
||||
await navigator.clipboard.writeText(link);
|
||||
this.publicLinkCopied = true;
|
||||
setTimeout(() => {
|
||||
this.publicLinkCopied = false;
|
||||
}, 10000);
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
this.publicLinkCopied = false;
|
||||
},
|
||||
|
||||
selectEntity(collaboratorKey: string) {
|
||||
if (this.selectedCollaboratorsKeys.includes(collaboratorKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.selectedCollaboratorsKeys.includes(collaboratorKey)) return;
|
||||
this.refs.popover?.$refs.popover.hide();
|
||||
this.selectedCollaboratorsKeys.push(collaboratorKey);
|
||||
},
|
||||
|
|
|
@ -4,8 +4,24 @@
|
|||
{{ t('memories', 'Share Album') }}
|
||||
</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
|
||||
v-if="album"
|
||||
ref="collaborators"
|
||||
:album-name="album.basename"
|
||||
:collaborators="album.collaborators"
|
||||
:public-link="album.publicLink"
|
||||
|
@ -16,7 +32,7 @@
|
|||
:aria-label="t('memories', 'Save collaborators for this album.')"
|
||||
type="primary"
|
||||
:disabled="loadingAddCollaborators"
|
||||
@click="handleSetCollaborators(collaborators)"
|
||||
@click="save(collaborators)"
|
||||
>
|
||||
<template #icon>
|
||||
<XLoadingIcon v-if="loadingAddCollaborators" />
|
||||
|
@ -32,7 +48,10 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { showError } from '@nextcloud/dialogs';
|
||||
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton';
|
||||
const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
|
||||
|
||||
import Modal from './Modal.vue';
|
||||
import ModalMixin from './ModalMixin';
|
||||
|
@ -44,6 +63,7 @@ export default defineComponent({
|
|||
name: 'AlbumShareModal',
|
||||
components: {
|
||||
NcButton,
|
||||
NcTextField,
|
||||
Modal,
|
||||
AlbumCollaborators,
|
||||
},
|
||||
|
@ -54,32 +74,85 @@ export default defineComponent({
|
|||
|
||||
data: () => ({
|
||||
album: null as any,
|
||||
albumName: String(),
|
||||
loadingAddCollaborators: false,
|
||||
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: {
|
||||
async open() {
|
||||
async open(user: string, name: string, link?: boolean) {
|
||||
this.show = true;
|
||||
this.loadingAddCollaborators = true;
|
||||
const { user, name } = this.$route.params;
|
||||
this.album = await dav.getAlbum(user, name);
|
||||
this.loadingAddCollaborators = false;
|
||||
|
||||
// Load album info
|
||||
try {
|
||||
this.loadingAddCollaborators = true;
|
||||
this.albumName = name;
|
||||
this.album = await dav.getAlbum(user, name);
|
||||
} catch {
|
||||
showError(this.t('memories', 'Failed to load album info: {name}', { name }));
|
||||
} finally {
|
||||
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() {
|
||||
this.show = false;
|
||||
this.album = null;
|
||||
this.albumName = String();
|
||||
},
|
||||
|
||||
async handleSetCollaborators(collaborators: any[]) {
|
||||
async save(collaborators: any[]) {
|
||||
try {
|
||||
this.loadingAddCollaborators = true;
|
||||
|
||||
// Update album collaborators
|
||||
await dav.updateAlbum(this.album, {
|
||||
albumName: this.album.basename,
|
||||
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) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
|
@ -94,4 +167,8 @@ export default defineComponent({
|
|||
.album-share.loading-icon {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
span.field-title {
|
||||
color: var(--color-text-lighter);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -119,7 +119,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
hasVideos(): boolean {
|
||||
return !!this.photos?.some(utils.isVideo);
|
||||
return Boolean(this.photos?.some(utils.isVideo));
|
||||
},
|
||||
|
||||
canShareNative(): boolean {
|
||||
|
@ -137,11 +137,20 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
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 {
|
||||
return !!this.photos?.some(utils.isLocalPhoto);
|
||||
return Boolean(this.photos?.some(utils.isLocalPhoto));
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -198,9 +207,58 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
async shareLink() {
|
||||
const fileInfo = await this.l(async () => (await dav.getFiles([this.photos![0]]))[0]);
|
||||
await this.close(); // wait till transition is done
|
||||
_m.modals.shareNodeLink(fileInfo.filename, true);
|
||||
// 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
|
||||
_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
|
||||
:aria-label="t('memories', 'Share album')"
|
||||
@click="refs.shareModal.open()"
|
||||
@click="openShareModal()"
|
||||
close-after-click
|
||||
v-if="canEditAlbum"
|
||||
>
|
||||
|
@ -89,7 +89,6 @@
|
|||
|
||||
<AlbumCreateModal ref="createModal" />
|
||||
<AlbumDeleteModal ref="deleteModal" />
|
||||
<AlbumShareModal ref="shareModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -106,7 +105,6 @@ import axios from '@nextcloud/axios';
|
|||
|
||||
import AlbumCreateModal from '@components/modal/AlbumCreateModal.vue';
|
||||
import AlbumDeleteModal from '@components/modal/AlbumDeleteModal.vue';
|
||||
import AlbumShareModal from '@components/modal/AlbumShareModal.vue';
|
||||
|
||||
import { downloadWithHandle } from '@services/dav/download';
|
||||
import { API } from '@services/API';
|
||||
|
@ -132,7 +130,6 @@ export default defineComponent({
|
|||
|
||||
AlbumCreateModal,
|
||||
AlbumDeleteModal,
|
||||
AlbumShareModal,
|
||||
|
||||
BackIcon,
|
||||
DownloadIcon,
|
||||
|
@ -152,7 +149,6 @@ export default defineComponent({
|
|||
return this.$refs as {
|
||||
createModal: InstanceType<typeof AlbumCreateModal>;
|
||||
deleteModal: InstanceType<typeof AlbumDeleteModal>;
|
||||
shareModal: InstanceType<typeof AlbumShareModal>;
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -179,6 +175,10 @@ export default defineComponent({
|
|||
this.$router.go(-1);
|
||||
},
|
||||
|
||||
openShareModal() {
|
||||
_m.modals.albumShare(this.$route.params.user, this.$route.params.name);
|
||||
},
|
||||
|
||||
async downloadAlbum() {
|
||||
const res = await axios.post(API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name));
|
||||
if (res.status === 200 && res.data.handle) {
|
||||
|
|
|
@ -44,6 +44,7 @@ declare global {
|
|||
shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
|
||||
moveToFolder: (photos: IPhoto[]) => void;
|
||||
moveToFace: (photos: IPhoto[]) => void;
|
||||
albumShare: (user: string, name: string, link?: boolean) => Promise<void>;
|
||||
showSettings: () => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -365,7 +365,8 @@ export async function fillImageInfo(photos: IPhoto[], query?: { tags?: number },
|
|||
p.datetaken = res.data.datetaken;
|
||||
p.imageInfo = res.data;
|
||||
} 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 {
|
||||
done++;
|
||||
progress?.(done);
|
||||
|
|
Loading…
Reference in New Issue