feat: allow multi-share with sel manager (close #472)

Signed-off-by: Varun Patil <radialapps@gmail.com>
pulsejet/aio-hw-docs
Varun Patil 2023-11-14 00:40:04 -08:00
parent f0e1b00096
commit 0273ae8537
7 changed files with 170 additions and 30 deletions

View File

@ -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,

View File

@ -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(() => {
await new Promise((resolve) => setTimeout(resolve, 2000));
this.publicLinkCopied = false;
}, 10000);
},
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);
},

View File

@ -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;
// Load album info
try {
this.loadingAddCollaborators = true;
const { user, name } = this.$route.params;
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>

View File

@ -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]);
// 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(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);
});
},
/**

View File

@ -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) {

1
src/globals.d.ts vendored
View File

@ -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;
};

View File

@ -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);