Update Collection components, fix addId bug #

This commit is contained in:
Daniel Supernault 2022-09-06 18:09:14 -06:00
parent a68e95fe10
commit 62c056651a
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
2 changed files with 276 additions and 59 deletions

View file

@ -5,13 +5,53 @@
</div> </div>
<div class="row mt-3" v-if="loaded"> <div class="row mt-3" v-if="loaded">
<div class="col-12 p-0 mb-3"> <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">
<picture class="d-flex align-items-center justify-content-center"> <picture class="d-flex align-items-center justify-content-center">
<div class="dims"></div> <div class="dims"></div>
<div style="z-index:500;position: absolute;" class="text-white"> <div style="z-index:500;position: absolute;" class="text-white">
<p class="display-4 text-center pt-3">{{title || 'Untitled Collection'}}</p> <p class="display-4 text-center pt-3">{{title || 'Untitled Collection'}}</p>
<p class="lead text-center mb-3">{{description}}</p> <p class="lead text-center mb-3">{{description}}</p>
<p class="text-center"> <p class="text-center">
{{posts.length}} photos · by <a :href="'/' + profileUsername" class="font-weight-bold text-white">{{profileUsername}}</a>
<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>
<p v-if="owner == true" class="pt-3 text-center"> <p v-if="owner == true" class="pt-3 text-center">
<span> <span>
@ -23,30 +63,103 @@
</div> </div>
</span> </span>
</button> </button>
&nbsp; &nbsp; &nbsp; &nbsp;
<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="editCollection" onclick="this.blur();">Edit</button>
&nbsp; &nbsp; &nbsp; &nbsp;
<button class="btn btn-outline-light btn-sm" @click.prevent="deleteCollection">Delete</button> <button class="btn btn-outline-light btn-sm" @click.prevent="deleteCollection">Delete</button>
</span> </span>
</p> </p>
</div> </div>
<img :src="previewUrl(posts[0])" <img
v-if="posts && posts.length"
:src="previewUrl(posts[0])"
alt="" alt=""
style="width:100%; height: 600px; object-fit: cover;" style="width:100%; height: 400px; object-fit: cover;"
> >
<div v-else class="bg-info" style="width:100%; height: 400px;"></div>
</picture> </picture>
</div> </div>
<div class="col-12 p-0"> <div class="col-12 p-0">
<masonry <!-- <masonry
:cols="{default: 2, 700: 2, 400: 1}" :cols="{default: 2, 700: 2, 400: 1}"
:gutter="{default: '5px'}" :gutter="{default: '5px'}"
> > -->
<div v-for="(s, index) in posts"> <div v-if="posts && posts.length > 0" class="row px-3 px-md-0">
<a class="card info-overlay card-md-border-0 mb-1" :href="s.url"> <div v-for="(s, index) in posts" class="col-6 col-md-4 feed">
<img :src="previewUrl(s)" class="img-fluid w-100"> <!-- <a class="card info-overlay card-md-border-0 mb-4 square" :href="s.url">
</a> <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>
</masonry>
<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>
</div> </div>
<b-modal ref="editModal" id="edit-modal" hide-footer centered title="Edit Collection" body-class=""> <b-modal ref="editModal" id="edit-modal" hide-footer centered title="Edit Collection" body-class="">
@ -64,6 +177,7 @@
<select class="custom-select" v-model="visibility"> <select class="custom-select" v-model="visibility">
<option value="public">Public</option> <option value="public">Public</option>
<option value="private">Followers Only</option> <option value="private">Followers Only</option>
<option value="draft">Draft</option>
</select> </select>
</div> </div>
<div class="d-flex justify-content-between align-items-center pt-3"> <div class="d-flex justify-content-between align-items-center pt-3">
@ -101,13 +215,14 @@
</div> </div>
</form> </form>
</b-modal> </b-modal>
<b-modal ref="addPhotoModal" id="add-photo-modal" hide-footer centered title="Add Photo" body-class="m-3"> <b-modal ref="addPhotoModal" id="add-photo-modal" hide-footer centered title="Add Photo" body-class="m-3">
<div class="form-group"> <div class="form-group">
<label for="title" class="font-weight-bold text-muted">Add Recent Post</label> <label for="title" class="font-weight-bold text-muted">Add Recent Post</label>
<div class="row m-1" v-if="postsList.length > 0"> <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 v-for="(p, index) in postsList" :key="'postList-'+index" class="col-4 p-1 cursor-pointer" @click="addRecentId(p)">
<div class="square"> <div class="square border">
<div class="square-content" v-bind:style="'background-image: url(' + p.media_attachments[0].url + ');'"></div> <div class="square-content" v-bind:style="'background-image: url(' + getPreviewUrl(p) + ');'"></div>
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
@ -133,13 +248,14 @@
</button> </button>
</form> </form>
</b-modal> </b-modal>
<b-modal ref="editPhotosModal" id="edit-photos-modal" hide-footer centered title="Edit Collection Photos" body-class="m-3"> <b-modal ref="editPhotosModal" id="edit-photos-modal" hide-footer centered title="Edit Collection Photos" body-class="m-3">
<div class="form-group"> <div class="form-group">
<p class="font-weight-bold text-dark text-center">Select a Photo to Delete</p> <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 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 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="[markedForDeletion.indexOf(p.id) == -1 ? 'square' : 'square delete-border']" @click="markPhotoForDeletion(p.id)">
<div class="square-content" v-bind:style="'background-image: url(' + p.media_attachments[0].url + ');'"></div> <div class="square-content border" v-bind:style="'background-image: url(' + p.media_attachments[0].url + ');'"></div>
</div> </div>
</div> </div>
</div> </div>
@ -147,12 +263,11 @@
<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> <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>
</div> </div>
</b-modal> </b-modal>
</div> </div>
</template> </template>
<style type="text/css" scoped> <style lang="scss" scoped>
.dims { .dims {
position: absolute; position: absolute;
top: 0; top: 0;
@ -172,12 +287,28 @@
background-color: red; background-color: red;
background-blend-mode: screen; 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> </style>
<script type="text/javascript"> <script type="text/javascript">
import VueMasonry from 'vue-masonry-css' import VueMasonry from 'vue-masonry-css';
import Intersect from 'vue-intersect';
Vue.use(VueMasonry);
export default { export default {
props: [ props: [
'collection-id', 'collection-id',
@ -188,6 +319,10 @@ export default {
'profile-username' 'profile-username'
], ],
components: {
"intersect": Intersect,
},
data() { data() {
return { return {
collection: {}, collection: {},
@ -195,7 +330,7 @@ export default {
loaded: false, loaded: false,
posts: [], posts: [],
ids: [], ids: [],
currentUser: false, user: false,
owner: false, owner: false,
title: this.collectionTitle, title: this.collectionTitle,
description: this.collectionDescription, description: this.collectionDescription,
@ -204,7 +339,10 @@ export default {
postsList: [], postsList: [],
loadingPostList: false, loadingPostList: false,
addingPostToCollection: false, addingPostToCollection: false,
markedForDeletion: [] markedForDeletion: [],
canLoadMore: false,
isIntersecting: false,
page: 1
} }
}, },
@ -212,14 +350,27 @@ export default {
this.fetchCollection(); this.fetchCollection();
}, },
mounted() {
},
methods: { methods: {
enterIntersect() {
if(this.isIntersecting) {
return;
}
this.isIntersecting = true;
this.page++;
this.fetchItems();
},
statusUrl(s) {
return '/i/web/post/' + s.id;
},
fetchCollection() { fetchCollection() {
axios.get('/api/local/collection/' + this.collectionId) axios.get('/api/local/collection/' + this.collectionId)
.then(res => { .then(res => {
this.collection = res.data; this.collection = res.data;
if(this.collection.post_count > 9) {
this.canLoadMore = true;
}
this.fetchCurrentUser(); this.fetchCurrentUser();
}) })
}, },
@ -227,8 +378,8 @@ export default {
fetchCurrentUser() { fetchCurrentUser() {
if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == true) { if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == true) {
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => { axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
this.currentUser = res.data; this.user = res.data;
this.owner = this.currentUser.id == this.profileId; this.owner = this.user.id == this.profileId;
window._sharedData.curUser = res.data; window._sharedData.curUser = res.data;
window.App.util.navatar(); window.App.util.navatar();
this.fetchItems(); this.fetchItems();
@ -239,18 +390,39 @@ export default {
}, },
fetchItems() { fetchItems() {
axios.get('/api/local/collection/items/' + this.collectionId) axios.get(
'/api/local/collection/items/' + this.collectionId,
{
params: {
page: this.page
}
}
)
.then(res => { .then(res => {
this.posts = res.data; 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 => { this.ids = this.posts.map(p => {
return p.id; return p.id;
}); });
this.loaded = true; this.loaded = true;
this.isIntersecting = false;
if(data.length == 0) {
this.canLoadMore = false;
}
}); });
}, },
previewUrl(status) { previewUrl(status) {
return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].preview_url; return status && status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].url;
}, },
previewBackground(status) { previewBackground(status) {
@ -262,16 +434,16 @@ export default {
let self = this; let self = this;
this.loadingPostList = true; this.loadingPostList = true;
if(this.postsList.length == 0) { if(this.postsList.length == 0) {
axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/statuses', { axios.get('/api/v1/accounts/'+this.profileId+'/statuses', {
params: { params: {
min_id: 1, min_id: 1,
limit: 13 limit: 40
} }
}) })
.then(res => { .then(res => {
self.postsList = res.data.filter(l => { self.postsList = res.data.filter(l => {
return self.ids.indexOf(l.id) == -1; return self.ids.indexOf(l.id) == -1;
}).splice(0,9); });
self.loadingPostList = false; self.loadingPostList = false;
self.$refs.addPhotoModal.show(); self.$refs.addPhotoModal.show();
}).catch(err => { }).catch(err => {
@ -299,22 +471,29 @@ export default {
swal('Invalid URL', 'You can only add posts from this instance', 'error'); swal('Invalid URL', 'You can only add posts from this instance', 'error');
this.photoId = ''; this.photoId = '';
} }
if(url.slice(0, origin.length + 3) !== origin + '/p/' || split.length !== 6) {
swal('Invalid URL', 'Invalid URL', 'error'); if(!url.includes('/i/web/post/') && !url.includes('/p/')) {
this.photoId = ''; swal('Invalid URL', 'Invalid URL', 'error');
} this.photoId = '';
return;
}
let fragment = split[split.length - 1].split('?')[0];
axios.post('/api/local/collection/item', { axios.post('/api/local/collection/item', {
collection_id: this.collectionId, collection_id: this.collectionId,
post_id: split[5] post_id: fragment
}).then(res => { }).then(res => {
self.ids.push(...split[5]); self.ids.push(...fragment);
self.posts.push(res.data);
self.collection.post_count++;
self.id = '';
}).catch(err => { }).catch(err => {
swal('Invalid URL', 'The post you entered was invalid', 'error'); swal('Invalid URL', 'The post you entered was invalid', 'error');
this.photoId = ''; this.photoId = '';
}); });
self.$refs.addPhotoModal.hide(); self.$refs.addPhotoModal.hide();
window.location.reload(); // window.location.reload();
}, },
editCollection() { editCollection() {
@ -350,7 +529,8 @@ export default {
visibility: this.visibility visibility: this.visibility
}) })
.then(res => { .then(res => {
window.location.href = '/'; console.log(res.data);
// window.location.href = res.data.url;
}); });
} else { } else {
return; return;
@ -358,13 +538,13 @@ export default {
}, },
updateCollection() { updateCollection() {
this.$refs.editModal.hide(); this.closeModals();
axios.post('/api/local/collection/' + this.collectionId, { axios.post('/api/local/collection/' + this.collectionId, {
title: this.title, title: this.title,
description: this.description, description: this.description,
visibility: this.visibility visibility: this.visibility
}).then(res => { }).then(res => {
console.log(res.data); this.collection = res.data;
}); });
}, },
@ -394,7 +574,9 @@ export default {
}) })
.then(res => { .then(res => {
self.removeItem(mfd); self.removeItem(mfd);
this.$refs.editPhotosModal.hide(); this.collection.post_count = this.collection.post_count - 1;
this.closeModals();
}) })
.catch(err => { .catch(err => {
swal( swal(
@ -420,12 +602,41 @@ export default {
collection_id: self.collectionId, collection_id: self.collectionId,
post_id: post.id post_id: post.id
}).then(res => { }).then(res => {
window.location.reload(); // window.location.reload();
this.closeModals();
this.posts.push(res.data);
this.collection.post_count++;
}).catch(err => { }).catch(err => {
swal('Oops!', 'An error occured, please try selecting another post.', 'error'); swal('Oops!', 'An error occured, please try selecting another post.', 'error');
this.photoId = ''; 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;
}
} }
} }
</script> </script>

View file

@ -25,12 +25,13 @@
<select class="custom-select" v-model="collection.visibility"> <select class="custom-select" v-model="collection.visibility">
<option value="public">Public</option> <option value="public">Public</option>
<option value="private">Followers Only</option> <option value="private">Followers Only</option>
<option value="draft">Draft</option>
</select> </select>
</div> </div>
</form> </form>
<hr> <hr>
<p> <p>
<button v-if="posts.length > 0" type="button" class="btn btn-primary font-weight-bold btn-block" @click="publish">Publish</button> <button v-if="posts.length > 0 && collection.visibility != 'draft'" type="button" class="btn btn-primary font-weight-bold btn-block" @click="publish">Publish</button>
<button v-else type="button" class="btn btn-primary font-weight-bold btn-block disabled" disabled>Publish</button> <button v-else type="button" class="btn btn-primary font-weight-bold btn-block disabled" disabled>Publish</button>
</p> </p>
<p> <p>
@ -55,7 +56,7 @@
</ul> </ul>
</div> </div>
<div class="card rounded-0 shadow-none border border-top-0"> <div class="card rounded-0 shadow-none border border-top-0">
<div class="card-body" style="height: 460px; overflow-y: auto"> <div class="card-body" style="min-height: 460px;">
<div v-if="tab == 'all'" class="row"> <div v-if="tab == 'all'" class="row">
<div class="col-4 p-1" v-for="(s, index) in posts"> <div class="col-4 p-1" v-for="(s, index) in posts">
<a class="card info-overlay card-md-border-0" :href="s.url"> <a class="card info-overlay card-md-border-0" :href="s.url">
@ -89,7 +90,7 @@
</div> </div>
<div class="form-group pt-4"> <div class="form-group pt-4">
<label for="title" class="font-weight-bold text-muted">Add Recent Post</label> <label for="title" class="font-weight-bold text-muted">Add Recent Post</label>
<div> <div style="max-height: 360px; overflow-y: auto">
<div v-for="(s, index) in recentPosts" :class="[selectedPost == s.id ? 'box-shadow border border-warning d-inline-block m-1':'d-inline-block m-1']" @click="selectPost(s)"> <div v-for="(s, index) in recentPosts" :class="[selectedPost == s.id ? 'box-shadow border border-warning d-inline-block m-1':'d-inline-block m-1']" @click="selectPost(s)">
<div class="cursor-pointer" :style="'width: 175px; height: 175px; ' + previewBackground(s)"></div> <div class="cursor-pointer" :style="'width: 175px; height: 175px; ' + previewBackground(s)"></div>
</div> </div>
@ -119,11 +120,10 @@ export default {
step: 1, step: 1,
title: '', title: '',
description: '', description: '',
visibility: 'private',
collection: { collection: {
title: '', title: '',
description: '', description: '',
visibility: 'public' visibility: 'draft'
}, },
id: '', id: '',
posts: [], posts: [],
@ -188,11 +188,16 @@ export default {
swal('Invalid URL', 'You can only add posts from this instance', 'error'); swal('Invalid URL', 'You can only add posts from this instance', 'error');
this.id = ''; this.id = '';
} }
if(url.slice(0, origin.length + 3) !== origin + '/p/' || split.length !== 6) {
if(url.includes('/i/web/post/') || url.includes('/p/')) {
let id = split[split.length - 1];
console.log('adding ' + id);
this.addToIds(id);
return;
} else {
swal('Invalid URL', 'Invalid URL', 'error'); swal('Invalid URL', 'Invalid URL', 'error');
this.id = ''; this.id = '';
} }
this.addToIds(split[5]);
return; return;
}, },
@ -206,10 +211,11 @@ export default {
}, },
fetchRecentPosts() { fetchRecentPosts() {
axios.get('/api/pixelfed/v1/accounts/' + this.profileId + '/statuses', { axios.get('/api/v1/accounts/' + this.profileId + '/statuses', {
params: { params: {
only_media: true, only_media: true,
min_id: 1, min_id: 1,
limit: 40
} }
}).then(res => { }).then(res => {
this.recentPosts = res.data.filter(s => { this.recentPosts = res.data.filter(s => {
@ -217,7 +223,7 @@ export default {
return s.id; return s.id;
}); });
return s.visibility == 'public' && s.sensitive == false && ids.indexOf(s.id) == -1; return s.visibility == 'public' && s.sensitive == false && ids.indexOf(s.id) == -1;
}).slice(0,3); });
}); });
}, },
@ -237,7 +243,7 @@ export default {
visibility: this.collection.visibility visibility: this.collection.visibility
}) })
.then(res => { .then(res => {
window.location.href = res.data; window.location.href = res.data.url;
}).catch(err => { }).catch(err => {
swal('Something went wrong', 'There was a problem with your request, please try again later.', 'error'); swal('Something went wrong', 'There was a problem with your request, please try again later.', 'error');
}); });
@ -266,4 +272,4 @@ export default {
} }
} }
} }
</script> </script>