EditPlaylist.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945
  1. <template>
  2. <modal
  3. :title="
  4. userId === playlist.createdBy ? 'Edit Playlist' : 'View Playlist'
  5. "
  6. class="edit-playlist-modal"
  7. >
  8. <div
  9. slot="body"
  10. :class="{
  11. 'view-only': !isEditable(),
  12. 'edit-playlist-modal-inner-container': true
  13. }"
  14. >
  15. <div id="first-column">
  16. <div id="playlist-info-section" class="section">
  17. <h3>{{ playlist.displayName }}</h3>
  18. <h5>Song Count: {{ playlist.songs.length }}</h5>
  19. <h5>Duration: {{ totalLength() }}</h5>
  20. </div>
  21. <div
  22. id="playlist-settings-section"
  23. v-if="
  24. userId === playlist.createdBy ||
  25. isEditable() ||
  26. (playlist.type === 'genre' && isAdmin())
  27. "
  28. class="section"
  29. >
  30. <div v-if="isEditable()">
  31. <h4 class="section-title">Edit Details</h4>
  32. <p class="section-description">
  33. Change the display name and privacy of the playlist.
  34. </p>
  35. <hr class="section-horizontal-rule" />
  36. <label class="label"> Change display name </label>
  37. <div class="control is-grouped input-with-button">
  38. <p class="control is-expanded">
  39. <input
  40. v-model="playlist.displayName"
  41. class="input"
  42. type="text"
  43. placeholder="Playlist Display Name"
  44. @keyup.enter="renamePlaylist()"
  45. />
  46. </p>
  47. <p class="control">
  48. <a
  49. class="button is-info"
  50. @click.prevent="renamePlaylist()"
  51. href="#"
  52. >Rename</a
  53. >
  54. </p>
  55. </div>
  56. </div>
  57. <div
  58. v-if="
  59. userId === playlist.createdBy ||
  60. (playlist.type === 'genre' && isAdmin())
  61. "
  62. >
  63. <label class="label"> Change privacy </label>
  64. <div class="control is-grouped input-with-button">
  65. <div class="control is-expanded select">
  66. <select v-model="playlist.privacy">
  67. <option value="private">Private</option>
  68. <option value="public">Public</option>
  69. </select>
  70. </div>
  71. <p class="control">
  72. <a
  73. class="button is-info"
  74. @click.prevent="updatePrivacy()"
  75. href="#"
  76. >Update Privacy</a
  77. >
  78. </p>
  79. </div>
  80. </div>
  81. </div>
  82. <div
  83. id="import-from-youtube-section"
  84. class="section"
  85. v-if="isEditable()"
  86. >
  87. <h4 class="section-title">Import from YouTube</h4>
  88. <p class="section-description">
  89. Import a playlist or song by searching or using a link
  90. from YouTube.
  91. </p>
  92. <hr class="section-horizontal-rule" />
  93. <label class="label">
  94. Search for a playlist from YouTube
  95. </label>
  96. <div class="control is-grouped input-with-button">
  97. <p class="control is-expanded">
  98. <input
  99. class="input"
  100. type="text"
  101. placeholder="Enter YouTube Playlist URL here..."
  102. v-model="search.playlist.query"
  103. @keyup.enter="importPlaylist()"
  104. />
  105. </p>
  106. <p class="control has-addons">
  107. <span class="select" id="playlist-import-type">
  108. <select
  109. v-model="
  110. search.playlist.isImportingOnlyMusic
  111. "
  112. >
  113. <option :value="false">Import all</option>
  114. <option :value="true">
  115. Import only music
  116. </option>
  117. </select>
  118. </span>
  119. <a
  120. class="button is-info"
  121. @click.prevent="importPlaylist()"
  122. href="#"
  123. ><i class="material-icons icon-with-button"
  124. >publish</i
  125. >Import</a
  126. >
  127. </p>
  128. </div>
  129. <label class="label">
  130. Search for a song from YouTube
  131. </label>
  132. <div class="control is-grouped input-with-button">
  133. <p class="control is-expanded">
  134. <input
  135. class="input"
  136. type="text"
  137. placeholder="Enter your YouTube query here..."
  138. v-model="search.songs.query"
  139. autofocus
  140. @keyup.enter="searchForSongs()"
  141. />
  142. </p>
  143. <p class="control">
  144. <a
  145. class="button is-info"
  146. @click.prevent="searchForSongs()"
  147. href="#"
  148. ><i class="material-icons icon-with-button"
  149. >search</i
  150. >Search</a
  151. >
  152. </p>
  153. </div>
  154. <div
  155. v-if="search.songs.results.length > 0"
  156. id="song-query-results"
  157. >
  158. <search-query-item
  159. v-for="(result, index) in search.songs.results"
  160. :key="result.id"
  161. :result="result"
  162. >
  163. <div slot="actions">
  164. <transition
  165. name="search-query-actions"
  166. mode="out-in"
  167. >
  168. <a
  169. class="button is-success"
  170. v-if="result.isAddedToQueue"
  171. href="#"
  172. key="added-to-playlist"
  173. >
  174. <i
  175. class="material-icons icon-with-button"
  176. >done</i
  177. >
  178. Added to playlist
  179. </a>
  180. <a
  181. class="button is-dark"
  182. v-else
  183. @click.prevent="
  184. addSongToPlaylist(result.id, index)
  185. "
  186. href="#"
  187. key="add-to-playlist"
  188. >
  189. <i
  190. class="material-icons icon-with-button"
  191. >add</i
  192. >
  193. Add to playlist
  194. </a>
  195. </transition>
  196. </div>
  197. </search-query-item>
  198. <a
  199. class="button is-primary load-more-button"
  200. @click.prevent="loadMoreSongs()"
  201. href="#"
  202. >
  203. Load more...
  204. </a>
  205. </div>
  206. </div>
  207. </div>
  208. <div id="second-column">
  209. <div id="rearrange-songs-section" class="section">
  210. <div v-if="isEditable()">
  211. <h4 class="section-title">Rearrange Songs</h4>
  212. <p class="section-description">
  213. Drag and drop songs to change their order
  214. </p>
  215. <hr class="section-horizontal-rule" />
  216. </div>
  217. <aside class="menu">
  218. <draggable
  219. class="menu-list scrollable-list"
  220. tag="ul"
  221. v-if="playlist.songs.length > 0"
  222. v-model="playlist.songs"
  223. v-bind="dragOptions"
  224. @start="drag = true"
  225. @end="drag = false"
  226. @change="repositionSong"
  227. >
  228. <transition-group
  229. type="transition"
  230. :name="
  231. !drag ? 'draggable-list-transition' : null
  232. "
  233. >
  234. <li
  235. v-for="(song, index) in playlist.songs"
  236. :key="`key-${song._id}`"
  237. >
  238. <song-item
  239. :song="song"
  240. :class="{
  241. 'item-draggable': isEditable()
  242. }"
  243. >
  244. <div
  245. class="song-actions"
  246. slot="actions"
  247. >
  248. <i
  249. class="material-icons add-to-queue-icon"
  250. v-if="
  251. station.partyMode &&
  252. !station.locked
  253. "
  254. @click="
  255. addSongToQueue(
  256. song.youtubeId
  257. )
  258. "
  259. content="Add Song to Queue"
  260. v-tippy
  261. >queue</i
  262. >
  263. <confirm
  264. v-if="
  265. userId ===
  266. playlist.createdBy ||
  267. isEditable()
  268. "
  269. placement="left"
  270. @confirm="
  271. removeSongFromPlaylist(
  272. song.youtubeId
  273. )
  274. "
  275. >
  276. <i
  277. class="material-icons delete-icon"
  278. content="Remove Song from Playlist"
  279. v-tippy
  280. >delete_forever</i
  281. >
  282. </confirm>
  283. <i
  284. class="material-icons"
  285. v-if="isEditable() && index > 0"
  286. @click="moveSongToTop(index)"
  287. content="Move to top of Playlist"
  288. v-tippy
  289. >vertical_align_top</i
  290. >
  291. <i
  292. v-if="
  293. isEditable() &&
  294. playlist.songs.length -
  295. 1 !==
  296. index
  297. "
  298. @click="moveSongToBottom(index)"
  299. class="material-icons"
  300. content="Move to bottom of Playlist"
  301. v-tippy
  302. >vertical_align_bottom</i
  303. >
  304. </div>
  305. </song-item>
  306. </li>
  307. </transition-group>
  308. </draggable>
  309. <p v-else class="nothing-here-text">
  310. This playlist doesn't have any songs.
  311. </p>
  312. </aside>
  313. </div>
  314. </div>
  315. <!--
  316. <button
  317. class="button is-info"
  318. @click="shuffle()"
  319. v-if="playlist.isUserModifiable"
  320. >
  321. Shuffle
  322. </button>
  323. <h5>Edit playlist details:</h5>
  324. -->
  325. </div>
  326. <div slot="footer">
  327. <a
  328. class="button is-default"
  329. v-if="
  330. this.userId === this.playlist.createdBy ||
  331. isEditable() ||
  332. this.playlist.privacy === 'public'
  333. "
  334. @click="downloadPlaylist()"
  335. href="#"
  336. >
  337. Download Playlist
  338. </a>
  339. <div class="right">
  340. <confirm
  341. v-if="playlist.type === 'station'"
  342. @confirm="clearAndRefillStationPlaylist()"
  343. >
  344. <a class="button is-danger">
  345. Clear and refill station playlist
  346. </a>
  347. </confirm>
  348. <confirm
  349. v-if="playlist.type === 'genre'"
  350. @confirm="clearAndRefillGenrePlaylist()"
  351. >
  352. <a class="button is-danger">
  353. Clear and refill genre playlist
  354. </a>
  355. </confirm>
  356. <confirm v-if="isEditable()" @confirm="removePlaylist()">
  357. <a class="button is-danger"> Remove Playlist </a>
  358. </confirm>
  359. </div>
  360. </div>
  361. </modal>
  362. </template>
  363. <script>
  364. import { mapState, mapGetters, mapActions } from "vuex";
  365. import draggable from "vuedraggable";
  366. import Toast from "toasters";
  367. import SearchYoutube from "@/mixins/SearchYoutube.vue";
  368. import validation from "@/validation";
  369. import Confirm from "@/components/Confirm.vue";
  370. import Modal from "../Modal.vue";
  371. import SearchQueryItem from "../SearchQueryItem.vue";
  372. import SongItem from "../SongItem.vue";
  373. import utils from "../../../js/utils";
  374. export default {
  375. components: { Modal, draggable, Confirm, SearchQueryItem, SongItem },
  376. mixins: [SearchYoutube],
  377. data() {
  378. return {
  379. utils,
  380. drag: false,
  381. apiDomain: "",
  382. playlist: { songs: [] }
  383. };
  384. },
  385. computed: {
  386. ...mapState("station", {
  387. station: state => state.station
  388. }),
  389. ...mapState("user/playlists", {
  390. editing: state => state.editing
  391. }),
  392. ...mapState({
  393. userId: state => state.user.auth.userId,
  394. userRole: state => state.user.auth.role
  395. }),
  396. dragOptions() {
  397. return {
  398. animation: 200,
  399. group: "songs",
  400. disabled: !this.isEditable(),
  401. ghostClass: "draggable-list-ghost"
  402. };
  403. },
  404. ...mapGetters({
  405. socket: "websockets/getSocket"
  406. })
  407. },
  408. watch: {
  409. "search.songs.results": function checkIfSongInPlaylist(songs) {
  410. songs.forEach((searchItem, index) =>
  411. this.playlist.songs.find(song => {
  412. if (song.youtubeId === searchItem.id)
  413. this.search.songs.results[index].isAddedToQueue = true;
  414. return song.youtubeId === searchItem.id;
  415. })
  416. );
  417. }
  418. },
  419. mounted() {
  420. this.socket.dispatch("playlists.getPlaylist", this.editing, res => {
  421. if (res.status === "success") {
  422. this.playlist = res.data.playlist;
  423. this.playlist.songs.sort((a, b) => a.position - b.position);
  424. this.playlist.oldId = res.data.playlist._id;
  425. } else new Toast(res.message);
  426. });
  427. this.socket.on(
  428. "event:playlist.song.added",
  429. res => {
  430. if (this.playlist._id === res.data.playlistId)
  431. this.playlist.songs.push(res.data.song);
  432. },
  433. { modal: "editPlaylist" }
  434. );
  435. this.socket.on(
  436. "event:playlist.song.removed",
  437. res => {
  438. if (this.playlist._id === res.data.playlistId) {
  439. // remove song from array of playlists
  440. this.playlist.songs.forEach((song, index) => {
  441. if (song.youtubeId === res.data.youtubeId)
  442. this.playlist.songs.splice(index, 1);
  443. });
  444. // if this song is in search results, mark it available to add to the playlist again
  445. this.search.songs.results.forEach((searchItem, index) => {
  446. if (res.data.youtubeId === searchItem.id) {
  447. this.search.songs.results[
  448. index
  449. ].isAddedToQueue = false;
  450. }
  451. });
  452. }
  453. },
  454. { modal: "editPlaylist" }
  455. );
  456. this.socket.on(
  457. "event:playlist.displayName.updated",
  458. res => {
  459. if (this.playlist._id === res.data.playlistId)
  460. this.playlist.displayName = res.data.displayName;
  461. },
  462. { modal: "editPlaylist" }
  463. );
  464. this.socket.on(
  465. "event:playlist.song.repositioned",
  466. res => {
  467. if (this.playlist._id === res.data.playlistId) {
  468. const { song, playlistId } = res.data;
  469. if (this.playlist._id === playlistId) {
  470. if (
  471. this.playlist.songs[song.newIndex] &&
  472. this.playlist.songs[song.newIndex].youtubeId ===
  473. song.youtubeId
  474. )
  475. return;
  476. this.playlist.songs.splice(
  477. song.newIndex,
  478. 0,
  479. this.playlist.songs.splice(song.oldIndex, 1)[0]
  480. );
  481. }
  482. }
  483. },
  484. { modal: "editPlaylist" }
  485. );
  486. },
  487. methods: {
  488. importPlaylist() {
  489. let isImportingPlaylist = true;
  490. // import query is blank
  491. if (!this.search.playlist.query)
  492. return new Toast("Please enter a YouTube playlist URL.");
  493. const regex = new RegExp(`[\\?&]list=([^&#]*)`);
  494. const splitQuery = regex.exec(this.search.playlist.query);
  495. if (!splitQuery) {
  496. return new Toast({
  497. content: "Please enter a valid YouTube playlist URL.",
  498. timeout: 4000
  499. });
  500. }
  501. // don't give starting import message instantly in case of instant error
  502. setTimeout(() => {
  503. if (isImportingPlaylist) {
  504. new Toast(
  505. "Starting to import your playlist. This can take some time to do."
  506. );
  507. }
  508. }, 750);
  509. return this.socket.dispatch(
  510. "playlists.addSetToPlaylist",
  511. this.search.playlist.query,
  512. this.playlist._id,
  513. this.search.playlist.isImportingOnlyMusic,
  514. res => {
  515. new Toast({ content: res.message, timeout: 20000 });
  516. if (res.status === "success") {
  517. isImportingPlaylist = false;
  518. if (this.search.playlist.isImportingOnlyMusic) {
  519. new Toast({
  520. content: `${res.data.stats.songsInPlaylistTotal} of the ${res.data.stats.videosInPlaylistTotal} videos in the playlist were songs.`,
  521. timeout: 20000
  522. });
  523. }
  524. }
  525. }
  526. );
  527. },
  528. isEditable() {
  529. return (
  530. this.playlist.isUserModifiable &&
  531. (this.userId === this.playlist.createdBy ||
  532. this.userRole === "admin")
  533. );
  534. },
  535. isAdmin() {
  536. return this.userRole === "admin";
  537. },
  538. repositionSong({ moved }) {
  539. if (!moved) return; // we only need to update when song is moved
  540. this.socket.dispatch(
  541. "playlists.repositionSong",
  542. this.playlist._id,
  543. {
  544. ...moved.element,
  545. oldIndex: moved.oldIndex,
  546. newIndex: moved.newIndex
  547. },
  548. res => {
  549. if (res.status !== "success")
  550. this.repositionSongInList({
  551. ...moved.element,
  552. newIndex: moved.oldIndex,
  553. oldIndex: moved.newIndex
  554. });
  555. }
  556. );
  557. },
  558. moveSongToTop(song, index) {
  559. this.repositionSongInPlaylist({
  560. moved: {
  561. element: song,
  562. oldIndex: index,
  563. newIndex: 0
  564. }
  565. });
  566. },
  567. moveSongToBottom(song, index) {
  568. this.repositionSongInPlaylist({
  569. moved: {
  570. element: song,
  571. oldIndex: index,
  572. newIndex: this.songsList.length
  573. }
  574. });
  575. },
  576. totalLength() {
  577. let length = 0;
  578. this.playlist.songs.forEach(song => {
  579. length += song.duration;
  580. });
  581. return this.utils.formatTimeLong(length);
  582. },
  583. shuffle() {
  584. this.socket.dispatch(
  585. "playlists.shuffle",
  586. this.playlist._id,
  587. res => {
  588. new Toast(res.message);
  589. if (res.status === "success") {
  590. this.playlist.songs = res.data.playlist.songs.sort(
  591. (a, b) => a.position - b.position
  592. );
  593. }
  594. }
  595. );
  596. },
  597. addSongToPlaylist(id, index) {
  598. this.socket.dispatch(
  599. "playlists.addSongToPlaylist",
  600. false,
  601. id,
  602. this.playlist._id,
  603. res => {
  604. new Toast(res.message);
  605. if (res.status === "success")
  606. this.search.songs.results[index].isAddedToQueue = true;
  607. }
  608. );
  609. },
  610. removeSongFromPlaylist(id) {
  611. if (this.playlist.displayName === "Liked Songs")
  612. return this.socket.dispatch("songs.unlike", id, res => {
  613. new Toast(res.message);
  614. });
  615. if (this.playlist.displayName === "Disliked Songs")
  616. return this.socket.dispatch("songs.undislike", id, res => {
  617. new Toast(res.message);
  618. });
  619. return this.socket.dispatch(
  620. "playlists.removeSongFromPlaylist",
  621. id,
  622. this.playlist._id,
  623. res => {
  624. new Toast(res.message);
  625. }
  626. );
  627. },
  628. renamePlaylist() {
  629. const { displayName } = this.playlist;
  630. if (!validation.isLength(displayName, 2, 32))
  631. return new Toast(
  632. "Display name must have between 2 and 32 characters."
  633. );
  634. if (!validation.regex.ascii.test(displayName))
  635. return new Toast(
  636. "Invalid display name format. Only ASCII characters are allowed."
  637. );
  638. return this.socket.dispatch(
  639. "playlists.updateDisplayName",
  640. this.playlist._id,
  641. this.playlist.displayName,
  642. res => {
  643. new Toast(res.message);
  644. }
  645. );
  646. },
  647. removePlaylist() {
  648. this.socket.dispatch("playlists.remove", this.playlist._id, res => {
  649. new Toast(res.message);
  650. if (res.status === "success") this.closeModal("editPlaylist");
  651. });
  652. },
  653. async downloadPlaylist() {
  654. if (this.apiDomain === "")
  655. this.apiDomain = await lofig.get("apiDomain");
  656. fetch(
  657. `${this.apiDomain}/export/privatePlaylist/${this.playlist._id}`,
  658. { credentials: "include" }
  659. )
  660. .then(res => res.blob())
  661. .then(blob => {
  662. const url = window.URL.createObjectURL(blob);
  663. const a = document.createElement("a");
  664. a.style.display = "none";
  665. a.href = url;
  666. a.download = `musare-privateplaylist-${
  667. this.playlist._id
  668. }-${new Date().toISOString()}.json`;
  669. document.body.appendChild(a);
  670. a.click();
  671. window.URL.revokeObjectURL(url);
  672. new Toast("Successfully downloaded playlist.");
  673. })
  674. .catch(
  675. () => new Toast("Failed to export and download playlist.")
  676. );
  677. },
  678. updatePrivacy() {
  679. const { privacy } = this.playlist;
  680. if (privacy === "public" || privacy === "private") {
  681. this.socket.dispatch(
  682. "playlists.updatePrivacy",
  683. this.playlist._id,
  684. privacy,
  685. res => {
  686. new Toast(res.message);
  687. }
  688. );
  689. }
  690. },
  691. addSongToQueue(youtubeId) {
  692. this.socket.dispatch(
  693. "stations.addToQueue",
  694. this.station._id,
  695. youtubeId,
  696. data => {
  697. if (data.status !== "success")
  698. new Toast({
  699. content: `Error: ${data.message}`,
  700. timeout: 8000
  701. });
  702. else new Toast({ content: data.message, timeout: 4000 });
  703. }
  704. );
  705. },
  706. clearAndRefillStationPlaylist() {
  707. this.socket.dispatch(
  708. "playlists.clearAndRefillStationPlaylist",
  709. this.playlist._id,
  710. data => {
  711. console.log(data.message);
  712. if (data.status !== "success")
  713. new Toast({
  714. content: `Error: ${data.message}`,
  715. timeout: 8000
  716. });
  717. else new Toast({ content: data.message, timeout: 4000 });
  718. }
  719. );
  720. },
  721. clearAndRefillGenrePlaylist() {
  722. this.socket.dispatch(
  723. "playlists.clearAndRefillGenrePlaylist",
  724. this.playlist._id,
  725. data => {
  726. if (data.status !== "success")
  727. new Toast({
  728. content: `Error: ${data.message}`,
  729. timeout: 8000
  730. });
  731. else new Toast({ content: data.message, timeout: 4000 });
  732. }
  733. );
  734. },
  735. ...mapActions("modalVisibility", ["openModal", "closeModal"])
  736. }
  737. };
  738. </script>
  739. <style lang="scss">
  740. .edit-playlist-modal {
  741. .modal-card {
  742. width: 1300px;
  743. .modal-card-body {
  744. padding: 16px;
  745. }
  746. }
  747. }
  748. </style>
  749. <style lang="scss" scoped>
  750. .night-mode {
  751. .label,
  752. p,
  753. strong {
  754. color: var(--light-grey-2);
  755. }
  756. }
  757. .menu-list li {
  758. display: flex;
  759. justify-content: space-between;
  760. &:not(:last-of-type) {
  761. margin-bottom: 10px;
  762. }
  763. a {
  764. display: flex;
  765. }
  766. }
  767. .controls {
  768. display: flex;
  769. a {
  770. display: flex;
  771. align-items: center;
  772. }
  773. }
  774. @media screen and (max-width: 1300px) {
  775. .edit-playlist-modal .edit-playlist-modal-inner-container {
  776. height: auto !important;
  777. #import-from-youtube-section #song-query-results,
  778. .section {
  779. max-width: 100% !important;
  780. }
  781. }
  782. }
  783. .edit-playlist-modal {
  784. .edit-playlist-modal-inner-container {
  785. display: flex;
  786. flex-wrap: wrap;
  787. height: 100%;
  788. &.view-only {
  789. height: auto !important;
  790. #first-column {
  791. flex-basis: 100%;
  792. }
  793. .section {
  794. max-width: 100% !important;
  795. }
  796. }
  797. }
  798. .nothing-here-text {
  799. display: flex;
  800. align-items: center;
  801. justify-content: center;
  802. }
  803. .section {
  804. padding: 15px !important;
  805. margin: 0 10px;
  806. max-width: 600px;
  807. display: flex;
  808. flex-direction: column;
  809. flex-grow: 1;
  810. }
  811. .label {
  812. font-size: 1rem;
  813. font-weight: normal;
  814. }
  815. .input-with-button .button {
  816. width: 150px;
  817. }
  818. #first-column {
  819. max-width: 100%;
  820. height: 100%;
  821. overflow-y: auto;
  822. flex-grow: 1;
  823. .section {
  824. width: auto;
  825. }
  826. #playlist-info-section {
  827. border: 1px solid var(--light-grey-3);
  828. border-radius: 3px;
  829. padding: 15px !important;
  830. h3 {
  831. font-weight: 600;
  832. font-size: 30px;
  833. }
  834. h5 {
  835. font-size: 18px;
  836. }
  837. h3,
  838. h5 {
  839. margin: 0;
  840. }
  841. }
  842. #import-from-youtube-section {
  843. #playlist-import-type select {
  844. border-radius: 0;
  845. }
  846. #song-query-results {
  847. padding: 10px;
  848. margin-top: 10px;
  849. border: 1px solid var(--light-grey-3);
  850. border-radius: 3px;
  851. max-width: 565px;
  852. .search-query-item:not(:last-of-type) {
  853. margin-bottom: 10px;
  854. }
  855. }
  856. .load-more-button {
  857. width: 100%;
  858. margin-top: 10px;
  859. }
  860. }
  861. }
  862. #second-column {
  863. max-width: 100%;
  864. height: 100%;
  865. overflow-y: auto;
  866. flex-grow: 1;
  867. }
  868. }
  869. </style>