Browse Source

feat(Playlists): you can now easily re-order playlists, and syncs with backend

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 years ago
parent
commit
7aa68bcf78

+ 54 - 24
backend/logic/actions/playlists.js

@@ -287,18 +287,36 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	indexMyPlaylists: isLoginRequired(async function indexMyPlaylists(session, showNonModifiablePlaylists, cb) {
-		const playlistModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "playlist"
-			},
-			this
-		);
+		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
 		async.waterfall(
 			[
 				next => {
-					if (showNonModifiablePlaylists) playlistModel.find({ createdBy: session.userId }, next);
-					else playlistModel.find({ createdBy: session.userId, isUserModifiable: true }, next);
+					userModel.findById(session.userId).select({ "preferences.orderOfPlaylists": -1 }).exec(next);
+				},
+
+				({ preferences }, next) => {
+					const { orderOfPlaylists } = preferences;
+
+					const match = {
+						createdBy: session.userId
+					};
+
+					// if non modifiable playlists should be shown as well
+					if (!showNonModifiablePlaylists) match.isUserModifiable = true;
+
+					// if a playlist order exists
+					if (orderOfPlaylists > 0) match._id = { $in: orderOfPlaylists };
+
+					playlistModel
+						.aggregate()
+						.match(match)
+						.addFields({
+							weight: { $indexOfArray: [orderOfPlaylists, "$_id"] }
+						})
+						.sort({ weight: 1 })
+						.exec(next);
 				}
 			],
 			async (err, playlists) => {
@@ -332,13 +350,8 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	create: isLoginRequired(async function create(session, data, cb) {
-		const playlistModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "playlist"
-			},
-			this
-		);
+		const playlistModel = await DBModule.runJob("GET_MODEL", { modelName: "playlist" }, this);
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		const blacklist = ["liked songs", "likedsongs", "disliked songs", "dislikedsongs"];
 
@@ -361,6 +374,17 @@ export default {
 						},
 						next
 					);
+				},
+
+				(playlist, next) => {
+					userModel.updateOne(
+						{ _id: session.userId },
+						{ $push: { "preferences.orderOfPlaylists": playlist._id } },
+						err => {
+							if (err) return next(err);
+							return next(null, playlist);
+						}
+					);
 				}
 			],
 			async (err, playlist) => {
@@ -1224,13 +1248,8 @@ export default {
 	 * @param {Function} cb - gets called with the result
 	 */
 	remove: isLoginRequired(async function remove(session, playlistId, cb) {
-		const stationModel = await DBModule.runJob(
-			"GET_MODEL",
-			{
-				modelName: "station"
-			},
-			this
-		);
+		const stationModel = await DBModule.runJob("GET_MODEL", { modelName: "station" }, this);
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
 			[
@@ -1242,7 +1261,18 @@ export default {
 
 				(playlist, next) => {
 					if (!playlist.isUserModifiable) return next("Playlist cannot be removed.");
-					return next(null);
+					return next(null, playlist);
+				},
+
+				(playlist, next) => {
+					userModel.updateOne(
+						{ _id: playlist.createdBy },
+						{ $pull: { "preferences.orderOfPlaylists": playlist._id } },
+						err => {
+							if (err) return next(err);
+							return next(null);
+						}
+					);
 				},
 
 				next => {

+ 42 - 0
backend/logic/actions/users.js

@@ -545,6 +545,7 @@ export default {
 	 */
 	removeSessions: isLoginRequired(async function removeSessions(session, userId, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
 		async.waterfall(
 			[
 				next => {
@@ -618,6 +619,47 @@ export default {
 		);
 	}),
 
+	updateOrderOfPlaylists: isLoginRequired(async function updateOrderOfPlaylists(session, orderOfPlaylists, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+		async.waterfall(
+			[
+				next => {
+					userModel.updateOne(
+						{ _id: session.userId },
+						{ $set: { preferences: { orderOfPlaylists } } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log(
+						"ERROR",
+						"UPDATE_ORDER_OF_USER_PLAYLISTS",
+						`Couldn't update order of playlists for user "${session.userId}" to "${orderOfPlaylists}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"UPDATE_ORDER_OF_USER_PLAYLISTS",
+					`Updated order of playlists for user "${session.userId}" to "${orderOfPlaylists}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Order of playlists successfully updated"
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Gets user object from username (only a few properties)
 	 *

+ 6 - 1
backend/logic/db/schemas/user.js

@@ -38,5 +38,10 @@ export default {
 	name: { type: String, default: "" },
 	location: { type: String, default: "" },
 	bio: { type: String, default: "" },
-	createdAt: { type: Date, default: Date.now }
+	createdAt: { type: Date, default: Date.now },
+	preferences: {
+		orderOfPlaylists: [{ type: mongoose.Schema.Types.ObjectId }],
+		nightMode: { type: Boolean, default: false },
+		autoSkipDisabled: { type: Boolean, default: true }
+	}
 };

+ 13 - 0
frontend/package-lock.json

@@ -8749,6 +8749,11 @@
         }
       }
     },
+    "sortablejs": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
+      "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
+    },
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -9936,6 +9941,14 @@
       "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz",
       "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw=="
     },
+    "vuedraggable": {
+      "version": "2.24.3",
+      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz",
+      "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==",
+      "requires": {
+        "sortablejs": "1.10.2"
+      }
+    },
     "vuex": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.3.0.tgz",

+ 1 - 0
frontend/package.json

@@ -52,6 +52,7 @@
     "vue": "^2.6.10",
     "vue-loader": "^15.7.0",
     "vue-router": "^3.0.7",
+    "vuedraggable": "^2.24.3",
     "vuex": "^3.1.1",
     "webpack-md5-hash": "0.0.6",
     "webpack-merge": "^4.2.1"

+ 93 - 10
frontend/src/pages/Station/components/Sidebar/MyPlaylists.vue

@@ -1,8 +1,22 @@
 <template>
 	<div id="my-playlists">
-		<ul class="menu-list scrollable-list" v-if="playlists.length > 0">
-			<li v-for="(playlist, index) in playlists" :key="index">
-				<playlist-item :playlist="playlist">
+		<draggable
+			class="menu-list scrollable-list"
+			v-if="playlists.length > 0"
+			v-model="playlists"
+			v-bind="dragOptions"
+			@start="drag = true"
+			@end="drag = false"
+		>
+			<transition-group
+				type="transition"
+				:name="!drag ? 'draggable-list-transition' : null"
+			>
+				<playlist-item
+					v-for="(playlist, index) in playlists"
+					:playlist="playlist"
+					:key="index"
+				>
 					<div class="icons-group" slot="actions">
 						<button
 							v-if="
@@ -34,8 +48,8 @@
 						</button>
 					</div>
 				</playlist-item>
-			</li>
-		</ul>
+			</transition-group>
+		</draggable>
 		<p v-else class="nothing-here-text scrollable-list">
 			No Playlists found
 		</p>
@@ -51,14 +65,19 @@
 <script>
 import { mapState, mapActions } from "vuex";
 import Toast from "toasters";
+import draggable from "vuedraggable";
+
 import PlaylistItem from "../../../../components/ui/PlaylistItem.vue";
 import io from "../../../../io";
 
 export default {
-	components: { PlaylistItem },
+	components: { PlaylistItem, draggable },
 	data() {
 		return {
-			playlists: []
+			orderOfPlaylists: [],
+			interval: null,
+			playlists: [],
+			drag: false
 		};
 	},
 	computed: {
@@ -66,8 +85,17 @@ export default {
 			modals: state => state.modals.station
 		}),
 		...mapState({
-			station: state => state.station.station
-		})
+			station: state => state.station.station,
+			userId: state => state.user.auth.userId
+		}),
+		dragOptions() {
+			return {
+				animation: 200,
+				group: "description",
+				disabled: false,
+				ghostClass: "draggable-list-ghost"
+			};
+		}
 	},
 	mounted() {
 		io.getSocket(socket => {
@@ -76,6 +104,7 @@ export default {
 			/** Get playlists for user */
 			this.socket.emit("playlists.indexMyPlaylists", true, res => {
 				if (res.status === "success") this.playlists = res.data;
+				this.orderOfPlaylists = this.calculatePlaylistOrder(); // order in regards to the database
 			});
 
 			this.socket.on("event:playlist.create", playlist => {
@@ -125,8 +154,15 @@ export default {
 					}
 				});
 			});
+
+			// checks if playlist order has changed every 1/2 second
+			this.interval = setInterval(() => this.savePlaylistOrder(), 500);
 		});
 	},
+	beforeDestroy() {
+		clearInterval(this.interval);
+		this.savePlaylistOrder();
+	},
 	methods: {
 		edit(id) {
 			this.editPlaylist(id);
@@ -167,6 +203,37 @@ export default {
 				return false;
 			return true;
 		},
+		calculatePlaylistOrder() {
+			const calculatedOrder = [];
+			this.playlists.forEach(playlist =>
+				calculatedOrder.push(playlist._id)
+			);
+
+			return calculatedOrder;
+		},
+		savePlaylistOrder() {
+			const recalculatedOrder = this.calculatePlaylistOrder();
+			if (
+				JSON.stringify(this.orderOfPlaylists) ===
+				JSON.stringify(recalculatedOrder)
+			)
+				return; // nothing has changed
+
+			this.socket.emit(
+				"users.updateOrderOfPlaylists",
+				recalculatedOrder,
+				res => {
+					if (res.status === "failure")
+						return new Toast({
+							content: res.message,
+							timeout: 8000
+						});
+
+					this.orderOfPlaylists = this.calculatePlaylistOrder(); // new order in regards to the database
+					return new Toast({ content: res.message, timeout: 4000 });
+				}
+			);
+		},
 		...mapActions("modals", ["openModal"]),
 		...mapActions("user/playlists", ["editPlaylist"])
 	}
@@ -210,8 +277,9 @@ export default {
 	}
 }
 
-.menu-list li {
+.menu-list .playlist {
 	align-items: center;
+	cursor: move;
 
 	&:not(:last-of-type) {
 		margin-bottom: 10px;
@@ -236,4 +304,19 @@ export default {
 		filter: brightness(90%);
 	}
 }
+
+.draggable-list-transition-move {
+	transition: transform 0.5s;
+}
+
+.night-mode {
+	.draggable-list-ghost {
+		background-color: darken($night-mode-bg-secondary, 5%);
+	}
+}
+
+.draggable-list-ghost {
+	opacity: 0.5;
+	background-color: darken(#fff, 5%);
+}
 </style>