mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-30 10:13:16 +00:00
668 lines
19 KiB
Vue
668 lines
19 KiB
Vue
<template>
|
|
<div class="w-100 h-100">
|
|
<div v-if="!loaded" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
|
|
<img src="/img/pixelfed-icon-grey.svg" class="">
|
|
</div>
|
|
<div class="row mt-3" v-if="loaded">
|
|
<div class="col-12 p-0 mb-3">
|
|
<div v-if="owner && !collection.published_at">
|
|
<div class="alert alert-danger d-flex justify-content-center">
|
|
<div class="media align-items-center">
|
|
<i class="far fa-exclamation-triangle fa-3x mr-3"></i>
|
|
<div class="media-body">
|
|
<p class="font-weight-bold mb-0">
|
|
This collection is unpublished.
|
|
</p>
|
|
<p class="small mb-0">
|
|
This collection is not visible to anyone else until you publish it. <br />
|
|
To publish, click on the <strong>Edit</strong> button and then click on the <strong>Publish</strong> button.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 p-0 mb-3">
|
|
|
|
<div class="d-flex align-items-center justify-content-center overflow-hidden">
|
|
<div class="dims"></div>
|
|
<div style="z-index:500;position: absolute;" class="text-white mx-5">
|
|
<p class="text-center pt-3 text-break" style="font-size: 3rem;line-height: 3rem;">{{title || 'Untitled Collection'}}</p>
|
|
<div class="text-center mb-3 text-break read-more" style="overflow-y: hidden">{{description}}</div>
|
|
<p class="text-center">
|
|
|
|
<span v-if="owner && collection.visibility != 'public'">
|
|
<span
|
|
v-if="collection.visibility == 'draft'"
|
|
class="btn btn-outline-light btn-sm text-capitalize py-0"
|
|
style="font-size: 10px"
|
|
>
|
|
<i class="far fa-lock"></i> Draft
|
|
</span>
|
|
<span
|
|
v-else-if="collection.visibility == 'private'"
|
|
class="btn btn-outline-light btn-sm text-capitalize py-0"
|
|
style="font-size: 10px"
|
|
>
|
|
Followers Only
|
|
</span>
|
|
<span>·</span>
|
|
</span>
|
|
<span>{{collection.post_count}} photos</span>
|
|
<span>·</span>
|
|
<span>by <a :href="'/' + profileUsername" class="font-weight-bold text-white">{{profileUsername}}</a></span>
|
|
</p>
|
|
<p v-if="owner == true" class="pt-3 text-center">
|
|
<span>
|
|
<button class="btn btn-outline-light btn-sm" @click.prevent="addToCollection" onclick="this.blur();">
|
|
<span v-if="loadingPostList == false">Add Photo</span>
|
|
<span v-else class="px-4">
|
|
<div class="spinner-border spinner-border-sm" role="status">
|
|
<span class="sr-only">Loading...</span>
|
|
</div>
|
|
</span>
|
|
</button>
|
|
|
|
<button class="btn btn-outline-light btn-sm" @click.prevent="editCollection" onclick="this.blur();">Edit</button>
|
|
|
|
<button class="btn btn-outline-light btn-sm" @click.prevent="deleteCollection">Delete</button>
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<img
|
|
v-if="posts && posts.length"
|
|
:src="previewUrl(posts[0])"
|
|
alt=""
|
|
style="width:100%; height: 400px; object-fit: cover;"
|
|
>
|
|
<div v-else class="bg-info" style="width:100%; height: 400px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 p-0">
|
|
<!-- <masonry
|
|
:cols="{default: 2, 700: 2, 400: 1}"
|
|
:gutter="{default: '5px'}"
|
|
> -->
|
|
<div v-if="posts && posts.length > 0" class="row px-3 px-md-0">
|
|
<div v-for="(s, index) in posts" class="col-6 col-md-4 feed">
|
|
<!-- <a class="card info-overlay card-md-border-0 mb-4 square" :href="s.url">
|
|
<img :src="previewUrl(s)" class="square-content w-100" style="object-fit: cover;">
|
|
</a> -->
|
|
|
|
<a v-if="s.hasOwnProperty('pf_type') && s.pf_type == 'video'" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
|
|
<div class="square">
|
|
<div class="square-content">
|
|
<div class="info-overlay-text-label rounded">
|
|
<h5 class="text-white m-auto font-weight-bold">
|
|
<span>
|
|
<span class="far fa-video fa-2x p-2 d-flex-inline"></span>
|
|
</span>
|
|
</h5>
|
|
</div>
|
|
<blur-hash-canvas
|
|
width="32"
|
|
height="32"
|
|
class="rounded"
|
|
:hash="s.media_attachments[0].blurhash">
|
|
</blur-hash-canvas>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a v-else-if="s.sensitive" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
|
|
<div class="square">
|
|
<div class="square-content">
|
|
<div class="info-overlay-text-label rounded">
|
|
<h5 class="text-white m-auto font-weight-bold">
|
|
<span>
|
|
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
|
|
</span>
|
|
</h5>
|
|
</div>
|
|
<blur-hash-canvas
|
|
width="32"
|
|
height="32"
|
|
class="rounded"
|
|
:hash="s.media_attachments[0].blurhash">
|
|
</blur-hash-canvas>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a v-else class="card info-overlay card-md-border-0" :href="statusUrl(s)">
|
|
<div class="square">
|
|
<div class="square-content">
|
|
<!-- <img :src="previewUrl(s)" class="img-fluid w-100 rounded-lg" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0'">
|
|
<span class="badge badge-light" style="position: absolute;bottom:2px;right:2px;opacity: 0.4;">
|
|
{{ timeago(s.created_at) }}
|
|
</span> -->
|
|
<blur-hash-image
|
|
width="32"
|
|
height="32"
|
|
class="rounded"
|
|
:hash="s.media_attachments[0].blurhash"
|
|
:src="previewUrl(s)" />
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div v-if="canLoadMore" class="col-12">
|
|
<intersect @enter="enterIntersect">
|
|
<div class="card card-body shadow-none border">
|
|
<div class="d-flex justify-content-center align-items-center flex-column">
|
|
<b-spinner variant="muted" />
|
|
<p class="text-lighter small mt-2 mb-0">Loading more...</p>
|
|
</div>
|
|
</div>
|
|
</intersect>
|
|
</div>
|
|
</div>
|
|
<!-- </masonry> -->
|
|
</div>
|
|
</div>
|
|
<b-modal ref="editModal" id="edit-modal" hide-footer centered title="Edit Collection" body-class="">
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="title" class="font-weight-bold text-muted">Title</label>
|
|
<input type="text" class="form-control" id="title" placeholder="Untitled Collection" v-model="title" maxlength="50">
|
|
<div class="text-right small text-muted">
|
|
<span>{{title ? title.length : 0}}/50</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="description" class="font-weight-bold text-muted">Description</label>
|
|
<textarea class="form-control" id="description" placeholder="Add a description here ..." v-model="description" rows="3" maxlength="500"></textarea>
|
|
<div class="text-right small text-muted">
|
|
<span>{{description ? description.length : 0}}/500</span>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="visibility" class="font-weight-bold text-muted">Visibility</label>
|
|
<select class="custom-select" v-model="visibility">
|
|
<option value="public">Public</option>
|
|
<option value="private">Followers Only</option>
|
|
<option value="draft">Draft</option>
|
|
</select>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center pt-3">
|
|
<a
|
|
class="text-primary font-weight-bold text-decoration-none"
|
|
href="#"
|
|
@click.prevent="showEditPhotosModal">
|
|
Edit Photos
|
|
</a>
|
|
|
|
<div v-if="collection.published_at">
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm py-1 font-weight-bold px-3 float-right"
|
|
@click.prevent="updateCollection">
|
|
Save
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else class="float-right">
|
|
<button
|
|
type="button"
|
|
class="btn btn-outline-primary btn-sm py-1 font-weight-bold px-3"
|
|
@click.prevent="publishCollection">
|
|
Publish
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
class="btn btn-primary btn-sm py-1 font-weight-bold px-3"
|
|
@click.prevent="updateCollection">
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</b-modal>
|
|
|
|
<b-modal ref="addPhotoModal" id="add-photo-modal" hide-footer centered title="Add Photo" body-class="m-3">
|
|
<div class="form-group">
|
|
<label for="title" class="font-weight-bold text-muted">Add Recent Post</label>
|
|
<div class="row m-1" v-if="postsList.length > 0" style="max-height: 360px; overflow-y: auto;">
|
|
<div v-for="(p, index) in postsList" :key="'postList-'+index" class="col-4 p-1 cursor-pointer" @click="addRecentId(p)">
|
|
<div class="square border">
|
|
<div class="square-content" v-bind:style="'background-image: url(' + getPreviewUrl(p) + ');'"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<hr>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<form>
|
|
<div class="form-group">
|
|
<label for="title" class="font-weight-bold text-muted">Add Post by URL</label>
|
|
<input type="text" class="form-control" placeholder="https://pixelfed.dev/p/admin/1" v-model="photoId">
|
|
<p class="help-text small text-muted">Only local, public posts can be added</p>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm py-1 font-weight-bold px-3 float-right" @click.prevent="pushId">
|
|
<span v-if="addingPostToCollection" class="px-4">
|
|
<div class="spinner-border spinner-border-sm" role="status">
|
|
<span class="sr-only">Loading...</span>
|
|
</div>
|
|
</span>
|
|
<span v-else>
|
|
Add Photo
|
|
</span>
|
|
</button>
|
|
</form>
|
|
</b-modal>
|
|
|
|
<b-modal ref="editPhotosModal" id="edit-photos-modal" hide-footer centered title="Edit Collection Photos" body-class="m-3">
|
|
<div class="form-group">
|
|
<p class="font-weight-bold text-dark text-center">Select a Photo to Delete</p>
|
|
<div class="row m-1 scrollbar-hidden" v-if="posts.length > 0" style="max-height: 350px;overflow-y: auto;">
|
|
<div v-for="(p, index) in posts" :key="'plm-'+index" class="col-4 p-1 cursor-pointer">
|
|
<div :class="[markedForDeletion.indexOf(p.id) == -1 ? 'square' : 'square delete-border']" @click="markPhotoForDeletion(p.id)">
|
|
<div class="square-content border" v-bind:style="'background-image: url(' + p.media_attachments[0].url + ');'"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-show="markedForDeletion.length > 0">
|
|
<button type="button" @click.prevent="confirmDeletion" class="btn btn-primary font-weight-bold py-0 btn-block mb-0 mt-4">Delete {{markedForDeletion.length}} {{markedForDeletion.length == 1 ? 'photo':'photos'}}</button>
|
|
</div>
|
|
</div>
|
|
</b-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.dims {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
background: rgba(0,0,0,.68);
|
|
z-index: 300;
|
|
}
|
|
.scrollbar-hidden::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.delete-border {
|
|
border: 4px solid #ff0000;
|
|
}
|
|
.delete-border .square-content {
|
|
background-color: red;
|
|
background-blend-mode: screen;
|
|
}
|
|
|
|
.info-overlay-text-field {
|
|
font-size: 13.5px;
|
|
margin-bottom: 2px;
|
|
|
|
@media (min-width: 768px) {
|
|
font-size: 20px;
|
|
margin-bottom: 15px;
|
|
}
|
|
}
|
|
|
|
.feed {
|
|
.card.info-overlay {
|
|
margin-bottom: 2rem;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script type="text/javascript">
|
|
import VueMasonry from 'vue-masonry-css';
|
|
import Intersect from 'vue-intersect';
|
|
|
|
export default {
|
|
props: [
|
|
'collection-id',
|
|
'collection-title',
|
|
'collection-description',
|
|
'collection-visibility',
|
|
'profile-id',
|
|
'profile-username'
|
|
],
|
|
|
|
components: {
|
|
"intersect": Intersect,
|
|
},
|
|
|
|
data() {
|
|
return {
|
|
collection: {},
|
|
config: window.App.config,
|
|
loaded: false,
|
|
posts: [],
|
|
ids: [],
|
|
user: false,
|
|
owner: false,
|
|
title: this.collectionTitle,
|
|
description: this.collectionDescription,
|
|
visibility: this.collectionVisibility,
|
|
photoId: '',
|
|
postsList: [],
|
|
loadingPostList: false,
|
|
addingPostToCollection: false,
|
|
markedForDeletion: [],
|
|
canLoadMore: false,
|
|
isIntersecting: false,
|
|
page: 1
|
|
}
|
|
},
|
|
|
|
beforeMount() {
|
|
this.fetchCollection();
|
|
},
|
|
|
|
updated() {
|
|
this.initReadMore();
|
|
},
|
|
|
|
methods: {
|
|
enterIntersect() {
|
|
if(this.isIntersecting) {
|
|
return;
|
|
}
|
|
this.isIntersecting = true;
|
|
this.page++;
|
|
this.fetchItems();
|
|
},
|
|
|
|
statusUrl(s) {
|
|
return '/i/web/post/' + s.id;
|
|
},
|
|
|
|
fetchCollection() {
|
|
axios.get('/api/local/collection/' + this.collectionId)
|
|
.then(res => {
|
|
this.collection = res.data;
|
|
if(this.collection.post_count > 9) {
|
|
this.canLoadMore = true;
|
|
}
|
|
this.fetchCurrentUser();
|
|
})
|
|
},
|
|
|
|
fetchCurrentUser() {
|
|
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == true) {
|
|
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
|
|
this.user = res.data;
|
|
this.owner = this.user.id == this.profileId;
|
|
window._sharedData.curUser = res.data;
|
|
window.App.util.navatar();
|
|
this.fetchItems();
|
|
});
|
|
} else {
|
|
this.fetchItems();
|
|
}
|
|
},
|
|
|
|
fetchItems() {
|
|
axios.get(
|
|
'/api/local/collection/items/' + this.collectionId,
|
|
{
|
|
params: {
|
|
page: this.page
|
|
}
|
|
}
|
|
)
|
|
.then(res => {
|
|
if(res.data.length == 0) {
|
|
console.log('no items found');
|
|
this.loaded = true;
|
|
this.isIntersecting = false;
|
|
this.canLoadMore = false;
|
|
return;
|
|
}
|
|
let data = res.data.filter(p => {
|
|
return this.ids.indexOf(p.id) == -1;
|
|
});
|
|
this.posts.push(...data);
|
|
this.ids = this.posts.map(p => {
|
|
return p.id;
|
|
});
|
|
this.loaded = true;
|
|
this.isIntersecting = false;
|
|
if(data.length == 0) {
|
|
this.canLoadMore = false;
|
|
}
|
|
});
|
|
},
|
|
|
|
previewUrl(status) {
|
|
return status && status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].url;
|
|
},
|
|
|
|
previewBackground(status) {
|
|
let preview = this.previewUrl(status);
|
|
return 'background-image: url(' + preview + ');';
|
|
},
|
|
|
|
addToCollection() {
|
|
let self = this;
|
|
this.loadingPostList = true;
|
|
if(this.postsList.length == 0) {
|
|
axios.get('/api/v1/accounts/'+this.profileId+'/statuses', {
|
|
params: {
|
|
min_id: 1,
|
|
limit: 40
|
|
}
|
|
})
|
|
.then(res => {
|
|
self.postsList = res.data.filter(l => {
|
|
return self.ids.indexOf(l.id) == -1;
|
|
});
|
|
self.loadingPostList = false;
|
|
self.$refs.addPhotoModal.show();
|
|
}).catch(err => {
|
|
self.loadingPostList = false;
|
|
swal('An Error Occured', 'We cannot process your request at this time, please try again later.', 'error');
|
|
})
|
|
} else {
|
|
this.$refs.addPhotoModal.show();
|
|
this.loadingPostList = false;
|
|
}
|
|
},
|
|
|
|
pushId() {
|
|
let max = this.config.uploader.max_collection_length;
|
|
let addingPostToCollection = true;
|
|
let self = this;
|
|
if(this.posts.length >= max) {
|
|
swal('Error', 'You can only add ' + max + ' posts per collection', 'error');
|
|
return;
|
|
}
|
|
let url = this.photoId;
|
|
let origin = window.location.origin;
|
|
let split = url.split('/');
|
|
if(url.slice(0, origin.length) !== origin) {
|
|
swal('Invalid URL', 'You can only add posts from this instance', 'error');
|
|
this.photoId = '';
|
|
}
|
|
|
|
if(!url.includes('/i/web/post/') && !url.includes('/p/')) {
|
|
swal('Invalid URL', 'Invalid URL', 'error');
|
|
this.photoId = '';
|
|
return;
|
|
}
|
|
|
|
let fragment = split[split.length - 1].split('?')[0];
|
|
|
|
axios.post('/api/local/collection/item', {
|
|
collection_id: this.collectionId,
|
|
post_id: fragment
|
|
}).then(res => {
|
|
self.ids.push(...fragment);
|
|
self.posts.push(res.data);
|
|
self.collection.post_count++;
|
|
self.id = '';
|
|
}).catch(err => {
|
|
swal('Invalid URL', 'The post you entered was invalid', 'error');
|
|
this.photoId = '';
|
|
});
|
|
self.$refs.addPhotoModal.hide();
|
|
// window.location.reload();
|
|
},
|
|
|
|
editCollection() {
|
|
this.$refs.editModal.show();
|
|
},
|
|
|
|
deleteCollection() {
|
|
if(this.owner == false) {
|
|
return;
|
|
}
|
|
|
|
let confirmed = window.confirm('Are you sure you want to delete this collection?');
|
|
if(confirmed) {
|
|
axios.delete('/api/local/collection/' + this.collectionId)
|
|
.then(res => {
|
|
window.location.href = '/';
|
|
});
|
|
} else {
|
|
return;
|
|
}
|
|
},
|
|
|
|
publishCollection() {
|
|
if(this.owner == false) {
|
|
return;
|
|
}
|
|
|
|
let confirmed = window.confirm('Are you sure you want to publish this collection?');
|
|
if(confirmed) {
|
|
axios.post('/api/local/collection/' + this.collectionId + '/publish', {
|
|
title: this.title,
|
|
description: this.description,
|
|
visibility: this.visibility
|
|
})
|
|
.then(res => {
|
|
console.log(res.data);
|
|
// window.location.href = res.data.url;
|
|
});
|
|
} else {
|
|
return;
|
|
}
|
|
},
|
|
|
|
updateCollection() {
|
|
this.closeModals();
|
|
axios.post('/api/local/collection/' + this.collectionId, {
|
|
title: this.title,
|
|
description: this.description,
|
|
visibility: this.visibility
|
|
}).then(res => {
|
|
this.collection = res.data;
|
|
});
|
|
},
|
|
|
|
showEditPhotosModal() {
|
|
this.$refs.editModal.hide();
|
|
this.$refs.editPhotosModal.show();
|
|
},
|
|
|
|
markPhotoForDeletion(id) {
|
|
this.markedForDeletion.indexOf(id) == -1 ?
|
|
this.markedForDeletion.push(id) :
|
|
this.markedForDeletion = this.markedForDeletion.filter(d => {
|
|
return d != id;
|
|
});
|
|
},
|
|
|
|
confirmDeletion() {
|
|
let self = this;
|
|
let confirmed = window.confirm('Are you sure you want to delete this?');
|
|
if(confirmed) {
|
|
this.markedForDeletion.forEach(mfd => {
|
|
axios.delete('/api/local/collection/item', {
|
|
params: {
|
|
collection_id: self.collectionId,
|
|
post_id: mfd
|
|
}
|
|
})
|
|
.then(res => {
|
|
self.removeItem(mfd);
|
|
this.collection.post_count = this.collection.post_count - 1;
|
|
this.closeModals();
|
|
|
|
})
|
|
.catch(err => {
|
|
swal(
|
|
'Oops!',
|
|
'An error occured with your request, please try again later.',
|
|
'error'
|
|
);
|
|
})
|
|
});
|
|
this.markedForDeletion = [];
|
|
}
|
|
},
|
|
|
|
removeItem(id) {
|
|
this.posts = this.posts.filter(post => {
|
|
return post.id != id;
|
|
});
|
|
},
|
|
|
|
addRecentId(post) {
|
|
let self = this;
|
|
axios.post('/api/local/collection/item', {
|
|
collection_id: self.collectionId,
|
|
post_id: post.id
|
|
}).then(res => {
|
|
// window.location.reload();
|
|
this.closeModals();
|
|
this.posts.push(res.data);
|
|
this.collection.post_count++;
|
|
}).catch(err => {
|
|
swal('Oops!', 'An error occured, please try selecting another post.', 'error');
|
|
this.photoId = '';
|
|
});
|
|
},
|
|
|
|
timeago(ts) {
|
|
return App.util.format.timeAgo(ts);
|
|
},
|
|
|
|
closeModals() {
|
|
this.$refs.editModal.hide();
|
|
this.$refs.addPhotoModal.hide();
|
|
this.$refs.editPhotosModal.hide();
|
|
},
|
|
|
|
getPreviewUrl(post) {
|
|
if(!post.media_attachments || !post.media_attachments.length) {
|
|
return '/storage/no-preview.png';
|
|
}
|
|
|
|
let media = post.media_attachments[0];
|
|
|
|
if(media.preview_url.endsWith('storage/no-preview.png')) {
|
|
return media.type === 'image' ?
|
|
media.url :
|
|
'/storage/no-preview.png';
|
|
}
|
|
|
|
return media.preview_url;
|
|
},
|
|
|
|
initReadMore() {
|
|
$('.read-more').each(function(k,v) {
|
|
let el = $(this);
|
|
let attr = el.attr('data-readmore');
|
|
if(typeof attr !== typeof undefined && attr !== false) {
|
|
return;
|
|
}
|
|
el.readmore({
|
|
collapsedHeight: 38,
|
|
heightMargin: 38,
|
|
moreLink: '<a href="#" class="d-block text-center small font-weight-bold mt-n3 mb-2" style="color: rgba(255, 255, 255, 0.5)">Show more</a>',
|
|
lessLink: '<a href="#" class="d-block text-center small font-weight-bold mt-n3 mb-2" style="color: rgba(255, 255, 255, 0.5)">Show less</a>',
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|