|
@@ -0,0 +1,797 @@
|
|
|
+<template>
|
|
|
+ <modal title="Edit Playlist" class="edit-playlist-modal">
|
|
|
+ <div slot="body">
|
|
|
+ <div
|
|
|
+ :class="{
|
|
|
+ 'view-only': !playlist.isUserModifiable,
|
|
|
+ 'edit-playlist-modal-inner-container': true
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div id="first-column">
|
|
|
+ <div id="playlist-info-section" class="section">
|
|
|
+ <h3>{{ playlist.displayName }}</h3>
|
|
|
+ <h5>Duration: {{ totalLength() }}</h5>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section-margin-bottom" />
|
|
|
+
|
|
|
+ <div id="playlist-settings-section" class="section">
|
|
|
+ <div v-if="playlist.isUserModifiable">
|
|
|
+ <h4 class="section-title">Edit Details</h4>
|
|
|
+
|
|
|
+ <p class="section-description">
|
|
|
+ Change the display name and privacy of the
|
|
|
+ playlist.
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <hr class="section-horizontal-rule" />
|
|
|
+
|
|
|
+ <label class="label">
|
|
|
+ Change display name
|
|
|
+ </label>
|
|
|
+
|
|
|
+ <div class="control is-grouped input-with-button">
|
|
|
+ <p class="control is-expanded">
|
|
|
+ <input
|
|
|
+ v-model="playlist.displayName"
|
|
|
+ class="input"
|
|
|
+ type="text"
|
|
|
+ placeholder="Playlist Display Name"
|
|
|
+ @keyup.enter="renamePlaylist()"
|
|
|
+ />
|
|
|
+ </p>
|
|
|
+ <p class="control">
|
|
|
+ <a
|
|
|
+ class="button is-info"
|
|
|
+ @click="renamePlaylist()"
|
|
|
+ href="#"
|
|
|
+ >Rename</a
|
|
|
+ >
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <label class="label">
|
|
|
+ Change privacy
|
|
|
+ </label>
|
|
|
+ <div class="control is-grouped input-with-button">
|
|
|
+ <div class="control is-expanded select">
|
|
|
+ <select v-model="playlist.privacy">
|
|
|
+ <option value="private">Private</option>
|
|
|
+ <option value="public">Public</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <p class="control">
|
|
|
+ <a
|
|
|
+ class="button is-info"
|
|
|
+ @click="updatePrivacy()"
|
|
|
+ href="#"
|
|
|
+ >Update Privacy</a
|
|
|
+ >
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section-margin-bottom" />
|
|
|
+
|
|
|
+ <div
|
|
|
+ id="import-from-youtube-section"
|
|
|
+ class="section"
|
|
|
+ v-if="playlist.isUserModifiable"
|
|
|
+ >
|
|
|
+ <h4 class="section-title">Import from YouTube</h4>
|
|
|
+
|
|
|
+ <p class="section-description">
|
|
|
+ Import a playlist or song by searching or using a
|
|
|
+ link from YouTube.
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <hr class="section-horizontal-rule" />
|
|
|
+
|
|
|
+ <label class="label">
|
|
|
+ Search for a playlist from YouTube
|
|
|
+ </label>
|
|
|
+ <div class="control is-grouped input-with-button">
|
|
|
+ <p class="control is-expanded">
|
|
|
+ <input
|
|
|
+ class="input"
|
|
|
+ type="text"
|
|
|
+ placeholder="Enter YouTube Playlist URL here..."
|
|
|
+ v-model="importQuery"
|
|
|
+ @keyup.enter="importPlaylist()"
|
|
|
+ />
|
|
|
+ </p>
|
|
|
+ <p class="control has-addons">
|
|
|
+ <span class="select" id="playlist-import-type">
|
|
|
+ <select
|
|
|
+ v-model="isImportingOnlyMusicOfPlaylist"
|
|
|
+ >
|
|
|
+ <option :value="false"
|
|
|
+ >Import all</option
|
|
|
+ >
|
|
|
+ <option :value="true"
|
|
|
+ >Import only music</option
|
|
|
+ >
|
|
|
+ </select>
|
|
|
+ </span>
|
|
|
+ <a
|
|
|
+ class="button is-info"
|
|
|
+ @click="importPlaylist()"
|
|
|
+ href="#"
|
|
|
+ ><i class="material-icons icon-with-button"
|
|
|
+ >publish</i
|
|
|
+ >Import</a
|
|
|
+ >
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <label class="label">
|
|
|
+ Search for a song from YouTube
|
|
|
+ </label>
|
|
|
+ <div class="control is-grouped input-with-button">
|
|
|
+ <p class="control is-expanded">
|
|
|
+ <input
|
|
|
+ class="input"
|
|
|
+ type="text"
|
|
|
+ placeholder="Enter your YouTube query here..."
|
|
|
+ v-model="searchSongQuery"
|
|
|
+ autofocus
|
|
|
+ @keyup.enter="searchForSongs()"
|
|
|
+ />
|
|
|
+ </p>
|
|
|
+ <p class="control">
|
|
|
+ <a
|
|
|
+ class="button is-info"
|
|
|
+ @click="searchForSongs()"
|
|
|
+ href="#"
|
|
|
+ ><i class="material-icons icon-with-button"
|
|
|
+ >search</i
|
|
|
+ >Search</a
|
|
|
+ >
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="song-query-results">
|
|
|
+ <search-query-item
|
|
|
+ v-for="(result, index) in queryResults"
|
|
|
+ :key="index"
|
|
|
+ :result="result"
|
|
|
+ >
|
|
|
+ <div slot="actions">
|
|
|
+ <transition
|
|
|
+ name="search-query-actions"
|
|
|
+ mode="out-in"
|
|
|
+ >
|
|
|
+ <a
|
|
|
+ class="button is-success"
|
|
|
+ v-if="result.isAddedToQueue"
|
|
|
+ href="#"
|
|
|
+ key="added-to-playlist"
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ class="material-icons icon-with-button"
|
|
|
+ >done</i
|
|
|
+ >
|
|
|
+ Added to playlist
|
|
|
+ </a>
|
|
|
+ <a
|
|
|
+ class="button is-dark"
|
|
|
+ v-else
|
|
|
+ @click="
|
|
|
+ addSongToPlaylist(
|
|
|
+ result.id,
|
|
|
+ index
|
|
|
+ )
|
|
|
+ "
|
|
|
+ href="#"
|
|
|
+ key="add-to-playlist"
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ class="material-icons icon-with-button"
|
|
|
+ >add</i
|
|
|
+ >
|
|
|
+ Add to playlist
|
|
|
+ </a>
|
|
|
+ </transition>
|
|
|
+ </div>
|
|
|
+ </search-query-item>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="section-margin-bottom" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="second-column">
|
|
|
+ <div id="rearrange-songs-section" class="section">
|
|
|
+ <div v-if="playlist.isUserModifiable">
|
|
|
+ <h4 class="section-title">Rearrange Songs</h4>
|
|
|
+
|
|
|
+ <p class="section-description">
|
|
|
+ Drag and drop songs to change their order
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <hr class="section-horizontal-rule" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <aside class="menu">
|
|
|
+ <draggable
|
|
|
+ class="menu-list scrollable-list"
|
|
|
+ tag="ul"
|
|
|
+ v-if="playlist.songs.length > 0"
|
|
|
+ v-model="playlist.songs"
|
|
|
+ v-bind="dragOptions"
|
|
|
+ @start="drag = true"
|
|
|
+ @end="drag = false"
|
|
|
+ @change="updateSongPositioning"
|
|
|
+ >
|
|
|
+ <transition-group
|
|
|
+ type="transition"
|
|
|
+ :name="
|
|
|
+ !drag
|
|
|
+ ? 'draggable-list-transition'
|
|
|
+ : null
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <li
|
|
|
+ v-for="(song, index) in playlist.songs"
|
|
|
+ :key="'key-' + index"
|
|
|
+ >
|
|
|
+ <playlist-song-item
|
|
|
+ :song="song"
|
|
|
+ :class="{
|
|
|
+ 'item-draggable':
|
|
|
+ playlist.isUserModifiable
|
|
|
+ }"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ slot="actions"
|
|
|
+ v-if="playlist.isUserModifiable"
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ class="material-icons"
|
|
|
+ v-if="index > 0"
|
|
|
+ @click="
|
|
|
+ moveSongToTop(index)
|
|
|
+ "
|
|
|
+ >vertical_align_top</i
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ v-else
|
|
|
+ class="material-icons"
|
|
|
+ style="opacity: 0"
|
|
|
+ >error</i
|
|
|
+ >
|
|
|
+
|
|
|
+ <i
|
|
|
+ v-if="
|
|
|
+ playlist.songs.length -
|
|
|
+ 1 !==
|
|
|
+ index
|
|
|
+ "
|
|
|
+ @click="
|
|
|
+ moveSongToBottom(index)
|
|
|
+ "
|
|
|
+ class="material-icons"
|
|
|
+ >vertical_align_bottom</i
|
|
|
+ >
|
|
|
+ <i
|
|
|
+ v-else
|
|
|
+ class="material-icons"
|
|
|
+ style="opacity: 0"
|
|
|
+ >error</i
|
|
|
+ >
|
|
|
+
|
|
|
+ <i
|
|
|
+ @click="
|
|
|
+ removeSongFromPlaylist(
|
|
|
+ song.songId
|
|
|
+ )
|
|
|
+ "
|
|
|
+ class="material-icons delete-icon"
|
|
|
+ >delete</i
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </playlist-song-item>
|
|
|
+ </li>
|
|
|
+ </transition-group>
|
|
|
+ </draggable>
|
|
|
+ </aside>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!--
|
|
|
+
|
|
|
+
|
|
|
+ <button
|
|
|
+ class="button is-info"
|
|
|
+ @click="shuffle()"
|
|
|
+ v-if="playlist.isUserModifiable"
|
|
|
+ >
|
|
|
+ Shuffle
|
|
|
+ </button>
|
|
|
+ <h5>Edit playlist details:</h5>
|
|
|
+ -->
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div slot="footer">
|
|
|
+ <a
|
|
|
+ class="button is-danger"
|
|
|
+ @click="removePlaylist()"
|
|
|
+ href="#"
|
|
|
+ v-if="playlist.isUserModifiable"
|
|
|
+ >
|
|
|
+ Remove Playlist
|
|
|
+ </a>
|
|
|
+ <a
|
|
|
+ class="button is-default"
|
|
|
+ @click="
|
|
|
+ closeModal({
|
|
|
+ sector: 'station',
|
|
|
+ modal: 'editPlaylist'
|
|
|
+ })
|
|
|
+ "
|
|
|
+ href="#"
|
|
|
+ >Close Modal</a
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+ </modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { mapState, mapActions } from "vuex";
|
|
|
+import draggable from "vuedraggable";
|
|
|
+import Toast from "toasters";
|
|
|
+
|
|
|
+import Modal from "../../Modal.vue";
|
|
|
+import SearchQueryItem from "../../ui/SearchQueryItem.vue";
|
|
|
+import PlaylistSongItem from "./components/PlaylistSongItem.vue";
|
|
|
+
|
|
|
+import io from "../../../io";
|
|
|
+import validation from "../../../validation";
|
|
|
+import utils from "../../../../js/utils";
|
|
|
+
|
|
|
+export default {
|
|
|
+ components: { Modal, draggable, SearchQueryItem, PlaylistSongItem },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ utils,
|
|
|
+ drag: false,
|
|
|
+ playlist: { songs: [] },
|
|
|
+ queryResults: [],
|
|
|
+ searchSongQuery: "",
|
|
|
+ directSongQuery: "",
|
|
|
+ importQuery: "",
|
|
|
+ isImportingOnlyMusicOfPlaylist: true
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ ...mapState("user/playlists", {
|
|
|
+ editing: state => state.editing
|
|
|
+ }),
|
|
|
+ dragOptions() {
|
|
|
+ return {
|
|
|
+ animation: 200,
|
|
|
+ group: "description",
|
|
|
+ disabled: !this.playlist.isUserModifiable,
|
|
|
+ ghostClass: "draggable-list-ghost"
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ io.getSocket(socket => {
|
|
|
+ this.socket = socket;
|
|
|
+
|
|
|
+ this.socket.emit("playlists.getPlaylist", this.editing, res => {
|
|
|
+ if (res.status === "success") {
|
|
|
+ this.playlist = res.data;
|
|
|
+ this.playlist.songs.sort((a, b) => a.position - b.position);
|
|
|
+ }
|
|
|
+ this.playlist.oldId = res.data._id;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socket.on("event:playlist.addSong", data => {
|
|
|
+ if (this.playlist._id === data.playlistId)
|
|
|
+ this.playlist.songs.push(data.song);
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socket.on("event:playlist.removeSong", data => {
|
|
|
+ if (this.playlist._id === data.playlistId) {
|
|
|
+ this.playlist.songs.forEach((song, index) => {
|
|
|
+ if (song.songId === data.songId)
|
|
|
+ this.playlist.songs.splice(index, 1);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socket.on("event:playlist.updateDisplayName", data => {
|
|
|
+ if (this.playlist._id === data.playlistId)
|
|
|
+ this.playlist.displayName = data.displayName;
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socket.on("event:playlist.repositionSongs", data => {
|
|
|
+ if (this.playlist._id === data.playlistId) {
|
|
|
+ // for each song that has a new position
|
|
|
+ data.songsBeingChanged.forEach(changedSong => {
|
|
|
+ this.playlist.songs.forEach((song, index) => {
|
|
|
+ // find song locally
|
|
|
+ if (song.songId === changedSong.songId) {
|
|
|
+ // change song position attribute
|
|
|
+ this.playlist.songs[index].position =
|
|
|
+ changedSong.position;
|
|
|
+
|
|
|
+ // reposition in array if needed
|
|
|
+ if (index !== changedSong.position - 1)
|
|
|
+ this.playlist.songs.splice(
|
|
|
+ changedSong.position - 1,
|
|
|
+ 0,
|
|
|
+ this.playlist.songs.splice(index, 1)[0]
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ updateSongPositioning({ moved }) {
|
|
|
+ if (!moved) return; // we only need to update when song is moved
|
|
|
+
|
|
|
+ const songsBeingChanged = [];
|
|
|
+
|
|
|
+ this.playlist.songs.forEach((song, index) => {
|
|
|
+ if (song.position !== index + 1)
|
|
|
+ songsBeingChanged.push({
|
|
|
+ songId: song.songId,
|
|
|
+ position: index + 1
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ this.socket.emit(
|
|
|
+ "playlists.repositionSongs",
|
|
|
+ this.playlist._id,
|
|
|
+ songsBeingChanged,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+ totalLength() {
|
|
|
+ let length = 0;
|
|
|
+ this.playlist.songs.forEach(song => {
|
|
|
+ length += song.duration;
|
|
|
+ });
|
|
|
+ return this.utils.formatTimeLong(length);
|
|
|
+ },
|
|
|
+ searchForSongs() {
|
|
|
+ let query = this.searchSongQuery;
|
|
|
+ if (query.indexOf("&index=") !== -1) {
|
|
|
+ query = query.split("&index=");
|
|
|
+ query.pop();
|
|
|
+ query = query.join("");
|
|
|
+ }
|
|
|
+ if (query.indexOf("&list=") !== -1) {
|
|
|
+ query = query.split("&list=");
|
|
|
+ query.pop();
|
|
|
+ query = query.join("");
|
|
|
+ }
|
|
|
+ this.socket.emit("apis.searchYoutube", query, res => {
|
|
|
+ if (res.status === "success") {
|
|
|
+ this.queryResults = [];
|
|
|
+ for (let i = 0; i < res.data.items.length; i += 1) {
|
|
|
+ this.queryResults.push({
|
|
|
+ id: res.data.items[i].id.videoId,
|
|
|
+ url: `https://www.youtube.com/watch?v=${this.id}`,
|
|
|
+ title: res.data.items[i].snippet.title,
|
|
|
+ thumbnail:
|
|
|
+ res.data.items[i].snippet.thumbnails.default
|
|
|
+ .url,
|
|
|
+ isAddedToQueue: false
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else if (res.status === "error")
|
|
|
+ new Toast({ content: res.message, timeout: 3000 });
|
|
|
+ });
|
|
|
+ },
|
|
|
+ shuffle() {
|
|
|
+ this.socket.emit("playlists.shuffle", this.playlist._id, res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ if (res.status === "success") {
|
|
|
+ this.playlist.songs = res.data.songs.sort(
|
|
|
+ (a, b) => a.position - b.position
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ importPlaylist() {
|
|
|
+ let isImportingPlaylist = true;
|
|
|
+
|
|
|
+ // don't give starting import message instantly in case of instant error
|
|
|
+ setTimeout(() => {
|
|
|
+ if (isImportingPlaylist) {
|
|
|
+ new Toast({
|
|
|
+ content:
|
|
|
+ "Starting to import your playlist. This can take some time to do.",
|
|
|
+ timeout: 4000
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, 750);
|
|
|
+
|
|
|
+ this.socket.emit(
|
|
|
+ "playlists.addSetToPlaylist",
|
|
|
+ this.importQuery,
|
|
|
+ this.playlist._id,
|
|
|
+ this.isImportingOnlyMusicOfPlaylist,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 20000 });
|
|
|
+ if (res.status === "success") {
|
|
|
+ isImportingPlaylist = false;
|
|
|
+ if (this.isImportingOnlyMusicOfPlaylist) {
|
|
|
+ new Toast({
|
|
|
+ content: `${res.stats.songsInPlaylistTotal} of the ${res.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
|
|
|
+ timeout: 20000
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+ addSongToPlaylist(id, index) {
|
|
|
+ this.socket.emit(
|
|
|
+ "playlists.addSongToPlaylist",
|
|
|
+ false,
|
|
|
+ id,
|
|
|
+ this.playlist._id,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ if (res.status === "success")
|
|
|
+ this.queryResults[index].isAddedToQueue = true;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+ removeSongFromPlaylist(id) {
|
|
|
+ this.socket.emit(
|
|
|
+ "playlists.removeSongFromPlaylist",
|
|
|
+ id,
|
|
|
+ this.playlist._id,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+ renamePlaylist() {
|
|
|
+ const { displayName } = this.playlist;
|
|
|
+ if (!validation.isLength(displayName, 2, 32))
|
|
|
+ return new Toast({
|
|
|
+ content:
|
|
|
+ "Display name must have between 2 and 32 characters.",
|
|
|
+ timeout: 8000
|
|
|
+ });
|
|
|
+ if (!validation.regex.ascii.test(displayName))
|
|
|
+ return new Toast({
|
|
|
+ content:
|
|
|
+ "Invalid display name format. Only ASCII characters are allowed.",
|
|
|
+ timeout: 8000
|
|
|
+ });
|
|
|
+
|
|
|
+ return this.socket.emit(
|
|
|
+ "playlists.updateDisplayName",
|
|
|
+ this.playlist._id,
|
|
|
+ this.playlist.displayName,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ }
|
|
|
+ );
|
|
|
+ },
|
|
|
+ removePlaylist() {
|
|
|
+ this.socket.emit("playlists.remove", this.playlist._id, res => {
|
|
|
+ new Toast({ content: res.message, timeout: 3000 });
|
|
|
+ if (res.status === "success") {
|
|
|
+ this.closeModal({
|
|
|
+ sector: "station",
|
|
|
+ modal: "editPlaylist"
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ moveSongToTop(index) {
|
|
|
+ this.playlist.songs.splice(
|
|
|
+ 0,
|
|
|
+ 0,
|
|
|
+ this.playlist.songs.splice(index, 1)[0]
|
|
|
+ );
|
|
|
+
|
|
|
+ this.updateSongPositioning({ moved: {} });
|
|
|
+ },
|
|
|
+ moveSongToBottom(index) {
|
|
|
+ this.playlist.songs.splice(
|
|
|
+ this.playlist.songs.length,
|
|
|
+ 0,
|
|
|
+ this.playlist.songs.splice(index, 1)[0]
|
|
|
+ );
|
|
|
+
|
|
|
+ this.updateSongPositioning({ moved: {} });
|
|
|
+ },
|
|
|
+ updatePrivacy() {
|
|
|
+ const { privacy } = this.playlist;
|
|
|
+ if (privacy === "public" || privacy === "private") {
|
|
|
+ this.socket.emit(
|
|
|
+ "playlists.updatePrivacy",
|
|
|
+ this.playlist._id,
|
|
|
+ privacy,
|
|
|
+ res => {
|
|
|
+ new Toast({ content: res.message, timeout: 4000 });
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+ },
|
|
|
+ ...mapActions("modalVisibility", ["closeModal"])
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss">
|
|
|
+.edit-playlist-modal {
|
|
|
+ .modal-card {
|
|
|
+ width: 1300px;
|
|
|
+
|
|
|
+ .modal-card-body {
|
|
|
+ padding: 16px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .modal-card-foot {
|
|
|
+ justify-content: flex-end;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+@import "../../../styles/global.scss";
|
|
|
+
|
|
|
+.night-mode {
|
|
|
+ .section {
|
|
|
+ background-color: $night-mode-bg-secondary !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ .label,
|
|
|
+ p,
|
|
|
+ strong {
|
|
|
+ color: $night-mode-text;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.menu {
|
|
|
+ max-height: 800px;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.menu-list li {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ &:not(:last-of-type) {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+
|
|
|
+ a {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.controls {
|
|
|
+ display: flex;
|
|
|
+
|
|
|
+ a {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media screen and (max-width: 1300px) {
|
|
|
+ #import-from-youtube-section #song-query-results,
|
|
|
+ .section {
|
|
|
+ max-width: 100% !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ #second-column {
|
|
|
+ width: 100%;
|
|
|
+
|
|
|
+ .section-margin-bottom {
|
|
|
+ display: block !important;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.edit-playlist-modal {
|
|
|
+ .edit-playlist-modal-inner-container {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ max-height: 950px;
|
|
|
+
|
|
|
+ /** playlist isn't able to modified */
|
|
|
+ &.view-only {
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .section {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .section {
|
|
|
+ padding: 5px !important;
|
|
|
+ margin: 0 20px;
|
|
|
+ max-width: 600px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ flex-grow: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ .label {
|
|
|
+ font-size: 1rem;
|
|
|
+ font-weight: normal;
|
|
|
+ }
|
|
|
+
|
|
|
+ .input-with-button .button {
|
|
|
+ width: 150px;
|
|
|
+ }
|
|
|
+
|
|
|
+ #first-column {
|
|
|
+ flex-grow: 1;
|
|
|
+ max-width: 100%;
|
|
|
+
|
|
|
+ .section {
|
|
|
+ width: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ #playlist-info-section {
|
|
|
+ border: 1px solid $light-grey-2;
|
|
|
+ border-radius: 3px;
|
|
|
+ padding: 15px !important;
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 30px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h5 {
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+
|
|
|
+ h3,
|
|
|
+ h5 {
|
|
|
+ margin: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #import-from-youtube-section {
|
|
|
+ #playlist-import-type select {
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ #song-query-results {
|
|
|
+ padding: 10px;
|
|
|
+ margin-top: 10px;
|
|
|
+ height: 230px;
|
|
|
+ overflow: auto;
|
|
|
+ border: 1px solid $light-grey-2;
|
|
|
+ border-radius: 3px;
|
|
|
+ max-width: 565px;
|
|
|
+
|
|
|
+ .search-query-item:not(:last-of-type) {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #second-column {
|
|
|
+ max-width: 100%;
|
|
|
+
|
|
|
+ .section-margin-bottom {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|