mirror of
				https://github.com/TeamPiped/Piped.git
				synced 2025-11-03 22:21:55 +00:00 
			
		
		
		
	Merge pull request #955 from TeamPiped/playlists
Add support for custom playlists
This commit is contained in:
		@@ -39,6 +39,9 @@
 | 
			
		||||
                <li v-if="shouldShowHistory">
 | 
			
		||||
                    <router-link v-t="'titles.history'" to="/history" />
 | 
			
		||||
                </li>
 | 
			
		||||
                <li v-if="authenticated">
 | 
			
		||||
                    <router-link v-t="'titles.playlists'" to="/playlists" />
 | 
			
		||||
                </li>
 | 
			
		||||
                <li v-if="authenticated">
 | 
			
		||||
                    <router-link v-t="'titles.feed'" to="/feed" />
 | 
			
		||||
                </li>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										94
									
								
								src/components/PlaylistAddModal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/components/PlaylistAddModal.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="modal">
 | 
			
		||||
        <div>
 | 
			
		||||
            <div class="modal-container">
 | 
			
		||||
                <div class="flex">
 | 
			
		||||
                    <span class="text-2xl w-max inline-block" v-t="'actions.select_playlist'" />
 | 
			
		||||
                    <button class="ml-3" @click="$emit('close')"><font-awesome-icon icon="xmark" /></button>
 | 
			
		||||
                </div>
 | 
			
		||||
                <select class="select w-full" v-model="selectedPlaylist">
 | 
			
		||||
                    <option
 | 
			
		||||
                        v-for="playlist in playlists"
 | 
			
		||||
                        :value="playlist.id"
 | 
			
		||||
                        :key="playlist.id"
 | 
			
		||||
                        v-text="playlist.name"
 | 
			
		||||
                    />
 | 
			
		||||
                </select>
 | 
			
		||||
                <button
 | 
			
		||||
                    class="btn mt-2"
 | 
			
		||||
                    @click="handleClick(selectedPlaylist)"
 | 
			
		||||
                    ref="addButton"
 | 
			
		||||
                    v-t="'actions.add_to_playlist'"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.modal {
 | 
			
		||||
    @apply fixed z-50 top-0 left-0 w-full h-full bg-dark-900 bg-opacity-80 transition-opacity table;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal > div {
 | 
			
		||||
    @apply table-cell align-middle;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-container {
 | 
			
		||||
    @apply w-min m-auto px-8 bg-dark-700 p-6;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        videoId: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            playlists: [],
 | 
			
		||||
            selectedPlaylist: null,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.fetchPlaylists();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        handleClick(playlistId) {
 | 
			
		||||
            if (!playlistId) {
 | 
			
		||||
                alert(this.$t("actions.please_select_playlist"));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.$refs.addButton.disabled = true;
 | 
			
		||||
 | 
			
		||||
            this.fetchJson(this.apiUrl() + "/user/playlists/add", null, {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    playlistId: playlistId,
 | 
			
		||||
                    videoId: this.videoId,
 | 
			
		||||
                }),
 | 
			
		||||
                headers: {
 | 
			
		||||
                    Authorization: this.getAuthToken(),
 | 
			
		||||
                    "Content-Type": "application/json",
 | 
			
		||||
                },
 | 
			
		||||
            }).then(json => {
 | 
			
		||||
                this.$emit("close");
 | 
			
		||||
                if (json.error) alert(json.error);
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        async fetchPlaylists() {
 | 
			
		||||
            this.fetchJson(this.apiUrl() + "/user/playlists", null, {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    Authorization: this.getAuthToken(),
 | 
			
		||||
                },
 | 
			
		||||
            }).then(json => {
 | 
			
		||||
                this.playlists = json;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -26,9 +26,13 @@
 | 
			
		||||
 | 
			
		||||
        <div class="video-grid">
 | 
			
		||||
            <VideoItem
 | 
			
		||||
                v-for="video in playlist.relatedStreams"
 | 
			
		||||
                v-for="(video, index) in playlist.relatedStreams"
 | 
			
		||||
                :key="video.url"
 | 
			
		||||
                :video="video"
 | 
			
		||||
                :index="index"
 | 
			
		||||
                :playlist-id="$route.query.list"
 | 
			
		||||
                :admin="admin"
 | 
			
		||||
                @remove="removeVideo(index)"
 | 
			
		||||
                height="94"
 | 
			
		||||
                width="168"
 | 
			
		||||
            />
 | 
			
		||||
@@ -48,6 +52,7 @@ export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            playlist: null,
 | 
			
		||||
            admin: false,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
@@ -57,6 +62,16 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.getPlaylistData();
 | 
			
		||||
        const playlistId = this.$route.query.list;
 | 
			
		||||
        if (this.authenticated && playlistId?.length == 36)
 | 
			
		||||
            this.fetchJson(this.apiUrl() + "/user/playlists", null, {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    Authorization: this.getAuthToken(),
 | 
			
		||||
                },
 | 
			
		||||
            }).then(json => {
 | 
			
		||||
                if (json.error) alert(json.error);
 | 
			
		||||
                else if (json.filter(playlist => playlist.id === playlistId).length > 0) this.admin = true;
 | 
			
		||||
            });
 | 
			
		||||
    },
 | 
			
		||||
    activated() {
 | 
			
		||||
        window.addEventListener("scroll", this.handleScroll);
 | 
			
		||||
@@ -87,6 +102,9 @@ export default {
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        removeVideo(index) {
 | 
			
		||||
            this.playlist.relatedStreams.splice(index, 1);
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										83
									
								
								src/components/PlaylistsPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/components/PlaylistsPage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <h1 class="font-bold text-center" v-t="'titles.playlists'" />
 | 
			
		||||
 | 
			
		||||
    <hr />
 | 
			
		||||
 | 
			
		||||
    <button v-t="'actions.create_playlist'" class="btn" @click="createPlaylist" />
 | 
			
		||||
 | 
			
		||||
    <div class="video-grid">
 | 
			
		||||
        <div v-for="playlist in playlists" :key="playlist.id">
 | 
			
		||||
            <router-link :to="`/playlist?list=${playlist.id}`">
 | 
			
		||||
                <img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
 | 
			
		||||
                <p
 | 
			
		||||
                    style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
 | 
			
		||||
                    class="my-2 overflow-hidden flex link"
 | 
			
		||||
                    :title="playlist.name"
 | 
			
		||||
                    v-text="playlist.name"
 | 
			
		||||
                />
 | 
			
		||||
            </router-link>
 | 
			
		||||
            <button class="btn h-auto" @click="deletePlaylist(playlist.id)" v-t="'actions.delete_playlist'" />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <br />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            playlists: [],
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        if (this.authenticated) this.fetchPlaylists();
 | 
			
		||||
        else this.$router.push("/login");
 | 
			
		||||
    },
 | 
			
		||||
    activated() {
 | 
			
		||||
        document.title = this.$t("titles.playlists") + " - Piped";
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        fetchPlaylists() {
 | 
			
		||||
            this.fetchJson(this.apiUrl() + "/user/playlists", null, {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    Authorization: this.getAuthToken(),
 | 
			
		||||
                },
 | 
			
		||||
            }).then(json => {
 | 
			
		||||
                this.playlists = json;
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        deletePlaylist(id) {
 | 
			
		||||
            if (confirm(this.$t("actions.delete_playlist_confirm")))
 | 
			
		||||
                this.fetchJson(this.apiUrl() + "/user/playlists/delete", null, {
 | 
			
		||||
                    method: "POST",
 | 
			
		||||
                    body: JSON.stringify({
 | 
			
		||||
                        playlistId: id,
 | 
			
		||||
                    }),
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        Authorization: this.getAuthToken(),
 | 
			
		||||
                        "Content-Type": "application/json",
 | 
			
		||||
                    },
 | 
			
		||||
                }).then(json => {
 | 
			
		||||
                    if (json.error) alert(json.error);
 | 
			
		||||
                    else this.playlists = this.playlists.filter(playlist => playlist.id !== id);
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        createPlaylist() {
 | 
			
		||||
            const name = prompt(this.$t("actions.create_playlist"));
 | 
			
		||||
            this.fetchJson(this.apiUrl() + "/user/playlists/create", null, {
 | 
			
		||||
                method: "POST",
 | 
			
		||||
                body: JSON.stringify({
 | 
			
		||||
                    name: name,
 | 
			
		||||
                }),
 | 
			
		||||
                headers: {
 | 
			
		||||
                    Authorization: this.getAuthToken(),
 | 
			
		||||
                    "Content-Type": "application/json",
 | 
			
		||||
                },
 | 
			
		||||
            }).then(json => {
 | 
			
		||||
                if (json.error) alert(json.error);
 | 
			
		||||
                else this.fetchPlaylists();
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </router-link>
 | 
			
		||||
 | 
			
		||||
        <div class="float-right m-0 inline-block">
 | 
			
		||||
        <div class="float-right m-0 inline-block children:px-1">
 | 
			
		||||
            <router-link
 | 
			
		||||
                :to="video.url + '&listen=1'"
 | 
			
		||||
                :aria-label="'Listen to ' + video.title"
 | 
			
		||||
@@ -32,6 +32,18 @@
 | 
			
		||||
            >
 | 
			
		||||
                <font-awesome-icon icon="headphones" />
 | 
			
		||||
            </router-link>
 | 
			
		||||
            <button v-if="authenticated" :title="$t('actions.add_to_playlist')" @click="showModal = !showModal">
 | 
			
		||||
                <font-awesome-icon icon="circle-plus" />
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
                v-if="admin"
 | 
			
		||||
                :title="$t('actions.remove_from_playlist')"
 | 
			
		||||
                ref="removeButton"
 | 
			
		||||
                @click="removeVideo(video.url.substr(-11))"
 | 
			
		||||
            >
 | 
			
		||||
                <font-awesome-icon icon="circle-minus" />
 | 
			
		||||
            </button>
 | 
			
		||||
            <PlaylistAddModal v-if="showModal" :video-id="video.url.substr(-11)" @close="showModal = !showModal" />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="flex">
 | 
			
		||||
@@ -82,6 +94,8 @@
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import PlaylistAddModal from "./PlaylistAddModal.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        video: {
 | 
			
		||||
@@ -93,6 +107,36 @@ export default {
 | 
			
		||||
        height: { type: String, default: "118" },
 | 
			
		||||
        width: { type: String, default: "210" },
 | 
			
		||||
        hideChannel: { type: Boolean, default: false },
 | 
			
		||||
        index: { type: Number, default: -1 },
 | 
			
		||||
        playlistId: { type: String, default: null },
 | 
			
		||||
        admin: { type: Boolean, default: false },
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            showModal: false,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        removeVideo() {
 | 
			
		||||
            if (confirm(this.$t("actions.delete_playlist_video_confirm"))) {
 | 
			
		||||
                this.$refs.removeButton.disabled = true;
 | 
			
		||||
                this.fetchJson(this.apiUrl() + "/user/playlists/remove", null, {
 | 
			
		||||
                    method: "POST",
 | 
			
		||||
                    body: JSON.stringify({
 | 
			
		||||
                        playlistId: this.playlistId,
 | 
			
		||||
                        index: this.index,
 | 
			
		||||
                    }),
 | 
			
		||||
                    headers: {
 | 
			
		||||
                        Authorization: this.getAuthToken(),
 | 
			
		||||
                        "Content-Type": "application/json",
 | 
			
		||||
                    },
 | 
			
		||||
                }).then(json => {
 | 
			
		||||
                    if (json.error) alert(json.error);
 | 
			
		||||
                    else this.$emit("remove");
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: { PlaylistAddModal },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,8 @@
 | 
			
		||||
        "feed": "Feed",
 | 
			
		||||
        "preferences": "Preferences",
 | 
			
		||||
        "history": "History",
 | 
			
		||||
        "subscriptions": "Subscriptions"
 | 
			
		||||
        "subscriptions": "Subscriptions",
 | 
			
		||||
        "playlists": "Playlists"
 | 
			
		||||
    },
 | 
			
		||||
    "player": {
 | 
			
		||||
        "watch_on": "Watch on {0}"
 | 
			
		||||
@@ -70,7 +71,15 @@
 | 
			
		||||
        "clear_history": "Clear History",
 | 
			
		||||
        "show_replies": "Show Replies",
 | 
			
		||||
        "hide_replies": "Hide Replies",
 | 
			
		||||
        "load_more_replies": "Load more Replies"
 | 
			
		||||
        "load_more_replies": "Load more Replies",
 | 
			
		||||
        "add_to_playlist": "Add to playlist",
 | 
			
		||||
        "remove_from_playlist": "Remove from playlist",
 | 
			
		||||
        "delete_playlist_video_confirm": "Are you sure you would like to remove this video from this playlist?",
 | 
			
		||||
        "create_playlist": "Create Playlist",
 | 
			
		||||
        "delete_playlist": "Delete Playlist",
 | 
			
		||||
        "select_playlist": "Select a Playlist",
 | 
			
		||||
        "delete_playlist_confirm": "Are you sure you want to delete this playlist?",
 | 
			
		||||
        "please_select_playlist": "Please select a playlist"
 | 
			
		||||
    },
 | 
			
		||||
    "comment": {
 | 
			
		||||
        "pinned_by": "Pinned by"
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,9 @@ import {
 | 
			
		||||
    faTv,
 | 
			
		||||
    faLevelUpAlt,
 | 
			
		||||
    faBroadcastTower,
 | 
			
		||||
    faCirclePlus,
 | 
			
		||||
    faCircleMinus,
 | 
			
		||||
    faXmark,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
import { faGithub, faBitcoin, faYoutube } from "@fortawesome/free-brands-svg-icons";
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 | 
			
		||||
@@ -34,6 +37,9 @@ library.add(
 | 
			
		||||
    faLevelUpAlt,
 | 
			
		||||
    faTv,
 | 
			
		||||
    faBroadcastTower,
 | 
			
		||||
    faCirclePlus,
 | 
			
		||||
    faCircleMinus,
 | 
			
		||||
    faXmark,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
import router from "@/router/router.js";
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,11 @@ const routes = [
 | 
			
		||||
        name: "Watch History",
 | 
			
		||||
        component: () => import("../components/HistoryPage.vue"),
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: "/playlists",
 | 
			
		||||
        name: "Playlists",
 | 
			
		||||
        component: () => import("../components/PlaylistsPage.vue"),
 | 
			
		||||
    },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user