mirror of
				https://github.com/TeamPiped/Piped.git
				synced 2024-08-14 23:57:27 +00:00 
			
		
		
		
	Merge pull request #2372 from Bnyro/subscription-groups
Subscription groups
This commit is contained in:
		
						commit
						25d3dcfacd
					
				
					 5 changed files with 181 additions and 8 deletions
				
			
		| 
						 | 
				
			
			@ -55,7 +55,7 @@ export default {
 | 
			
		|||
        });
 | 
			
		||||
 | 
			
		||||
        if ("indexedDB" in window) {
 | 
			
		||||
            const request = indexedDB.open("piped-db", 3);
 | 
			
		||||
            const request = indexedDB.open("piped-db", 4);
 | 
			
		||||
            request.onupgradeneeded = ev => {
 | 
			
		||||
                const db = request.result;
 | 
			
		||||
                console.log("Upgrading object store.");
 | 
			
		||||
| 
						 | 
				
			
			@ -73,6 +73,10 @@ export default {
 | 
			
		|||
                    store.createIndex("playlist_id_idx", "playlistId", { unique: true });
 | 
			
		||||
                    store.createIndex("id_idx", "id", { unique: true, autoIncrement: true });
 | 
			
		||||
                }
 | 
			
		||||
                if (!db.objectStoreNames.contains("channel_groups")) {
 | 
			
		||||
                    const store = db.createObjectStore("channel_groups", { keyPath: "groupName" });
 | 
			
		||||
                    store.createIndex("groupName", "groupName", { unique: true });
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            request.onsuccess = e => {
 | 
			
		||||
                window.db = e.target.result;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,6 +18,19 @@
 | 
			
		|||
        <option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
 | 
			
		||||
    </select>
 | 
			
		||||
 | 
			
		||||
    <label for="group-selector" class="ml-10 mr-2">
 | 
			
		||||
        <strong v-text="`${$t('titles.channel_groups')}:`" />
 | 
			
		||||
    </label>
 | 
			
		||||
    <select id="group-selector" v-model="selectedGroupName" default="" class="select w-auto">
 | 
			
		||||
        <option value="" v-t="`video.all`" />
 | 
			
		||||
        <option
 | 
			
		||||
            v-for="group in channelGroups"
 | 
			
		||||
            :key="group.groupName"
 | 
			
		||||
            :value="group.groupName"
 | 
			
		||||
            v-text="group.groupName"
 | 
			
		||||
        />
 | 
			
		||||
    </select>
 | 
			
		||||
 | 
			
		||||
    <span class="md:float-right">
 | 
			
		||||
        <SortingSelector by-key="uploaded" @apply="order => videos.sort(order)" />
 | 
			
		||||
    </span>
 | 
			
		||||
| 
						 | 
				
			
			@ -25,7 +38,7 @@
 | 
			
		|||
    <hr />
 | 
			
		||||
 | 
			
		||||
    <LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">
 | 
			
		||||
        <template v-for="video in videos" :key="video.url">
 | 
			
		||||
        <template v-for="video in filteredVideos" :key="video.url">
 | 
			
		||||
            <VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" />
 | 
			
		||||
        </template>
 | 
			
		||||
    </LoadingIndicatorPage>
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +63,8 @@ export default {
 | 
			
		|||
            videos: [],
 | 
			
		||||
            availableFilters: ["all", "shorts", "videos"],
 | 
			
		||||
            selectedFilter: "all",
 | 
			
		||||
            selectedGroupName: "",
 | 
			
		||||
            channelGroups: [],
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
| 
						 | 
				
			
			@ -57,6 +72,12 @@ export default {
 | 
			
		|||
            if (_this.authenticated) return _this.authApiUrl() + "/feed/rss?authToken=" + _this.getAuthToken();
 | 
			
		||||
            else return _this.authApiUrl() + "/feed/unauthenticated/rss?channels=" + _this.getUnauthenticatedChannels();
 | 
			
		||||
        },
 | 
			
		||||
        filteredVideos(_this) {
 | 
			
		||||
            const selectedGroup = _this.channelGroups.filter(group => group.groupName == _this.selectedGroupName);
 | 
			
		||||
            return _this.selectedGroupName == ""
 | 
			
		||||
                ? _this.videos
 | 
			
		||||
                : _this.videos.filter(video => selectedGroup[0].channels.includes(video.uploaderUrl.substr(-11)));
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.fetchFeed().then(videos => {
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +87,21 @@ export default {
 | 
			
		|||
        });
 | 
			
		||||
 | 
			
		||||
        this.selectedFilter = this.getPreferenceString("feedFilter") ?? "all";
 | 
			
		||||
 | 
			
		||||
        if (!window.db) return;
 | 
			
		||||
 | 
			
		||||
        const cursor = this.getChannelGroupsCursor();
 | 
			
		||||
        cursor.onsuccess = e => {
 | 
			
		||||
            const cursor = e.target.result;
 | 
			
		||||
            if (cursor) {
 | 
			
		||||
                const group = cursor.value;
 | 
			
		||||
                this.channelGroups.push({
 | 
			
		||||
                    groupName: group.groupName,
 | 
			
		||||
                    channels: JSON.parse(group.channels),
 | 
			
		||||
                });
 | 
			
		||||
                cursor.continue();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    activated() {
 | 
			
		||||
        document.title = this.$t("titles.feed") + " - Piped";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,12 +15,32 @@
 | 
			
		|||
    </div>
 | 
			
		||||
    <br />
 | 
			
		||||
    <hr />
 | 
			
		||||
    <div class="w-full flex flex-wrap">
 | 
			
		||||
        <button
 | 
			
		||||
            v-for="group in channelGroups"
 | 
			
		||||
            class="btn mx-1 w-max"
 | 
			
		||||
            :class="{ selected: selectedGroup === group }"
 | 
			
		||||
            :key="group.groupName"
 | 
			
		||||
            @click="selectedGroup = group"
 | 
			
		||||
        >
 | 
			
		||||
            <span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
 | 
			
		||||
            <div v-if="group.groupName != '' && selectedGroup == group">
 | 
			
		||||
                <font-awesome-icon class="mx-2" icon="edit" @click="showEditGroupModal = true" />
 | 
			
		||||
                <font-awesome-icon class="mx-2" icon="circle-minus" @click="deleteGroup(group)" />
 | 
			
		||||
            </div>
 | 
			
		||||
        </button>
 | 
			
		||||
        <button class="btn mx-1">
 | 
			
		||||
            <font-awesome-icon icon="circle-plus" @click="showCreateGroupModal = true" />
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <br />
 | 
			
		||||
    <hr />
 | 
			
		||||
    <!-- Subscriptions card list -->
 | 
			
		||||
    <div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
 | 
			
		||||
        <!-- channel info card -->
 | 
			
		||||
        <div
 | 
			
		||||
            class="col m-2 p-1 border rounded-lg border-gray-500"
 | 
			
		||||
            v-for="subscription in subscriptions"
 | 
			
		||||
            v-for="subscription in filteredSubscriptions"
 | 
			
		||||
            :key="subscription.url"
 | 
			
		||||
        >
 | 
			
		||||
            <router-link :to="subscription.url" class="flex p-2 font-bold text-4x4">
 | 
			
		||||
| 
						 | 
				
			
			@ -36,13 +56,49 @@
 | 
			
		|||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <br />
 | 
			
		||||
 | 
			
		||||
    <ModalComponent v-if="showCreateGroupModal" @close="showCreateGroupModal = !showCreateGroupModal">
 | 
			
		||||
        <h2 v-t="'actions.create_group'" />
 | 
			
		||||
        <div class="flex flex-col">
 | 
			
		||||
            <input class="input my-4" type="text" v-model="newGroupName" :placeholder="$t('actions.group_name')" />
 | 
			
		||||
            <button class="ml-auto btn w-max" v-t="'actions.create_group'" @click="createGroup()" />
 | 
			
		||||
        </div>
 | 
			
		||||
    </ModalComponent>
 | 
			
		||||
 | 
			
		||||
    <ModalComponent v-if="showEditGroupModal" @close="showEditGroupModal = false">
 | 
			
		||||
        <h2>{{ selectedGroup.groupName }}</h2>
 | 
			
		||||
        <div class="flex flex-col mt-3 mb-2">
 | 
			
		||||
            <div v-for="subscription in subscriptions" :key="subscription.name">
 | 
			
		||||
                <div class="flex justify-between">
 | 
			
		||||
                    <span>{{ subscription.name }}</span>
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="checkbox"
 | 
			
		||||
                        class="checkbox"
 | 
			
		||||
                        :checked="selectedGroup.channels.includes(subscription.url.substr(-11))"
 | 
			
		||||
                        @change="checkedChange(subscription)"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                <hr />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </ModalComponent>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import ModalComponent from "./ModalComponent.vue";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            subscriptions: [],
 | 
			
		||||
            selectedGroup: {
 | 
			
		||||
                groupName: "",
 | 
			
		||||
                channels: [],
 | 
			
		||||
            },
 | 
			
		||||
            channelGroups: [],
 | 
			
		||||
            showCreateGroupModal: false,
 | 
			
		||||
            showEditGroupModal: false,
 | 
			
		||||
            newGroupName: "",
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
| 
						 | 
				
			
			@ -50,6 +106,22 @@ export default {
 | 
			
		|||
            this.subscriptions = json;
 | 
			
		||||
            this.subscriptions.forEach(subscription => (subscription.subscribed = true));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.channelGroups.push(this.selectedGroup);
 | 
			
		||||
 | 
			
		||||
        if (!window.db) return;
 | 
			
		||||
        const cursor = this.getChannelGroupsCursor();
 | 
			
		||||
        cursor.onsuccess = e => {
 | 
			
		||||
            const cursor = e.target.result;
 | 
			
		||||
            if (cursor) {
 | 
			
		||||
                const group = cursor.value;
 | 
			
		||||
                this.channelGroups.push({
 | 
			
		||||
                    groupName: group.groupName,
 | 
			
		||||
                    channels: JSON.parse(group.channels),
 | 
			
		||||
                });
 | 
			
		||||
                cursor.continue();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    activated() {
 | 
			
		||||
        document.title = "Subscriptions - Piped";
 | 
			
		||||
| 
						 | 
				
			
			@ -88,7 +160,6 @@ export default {
 | 
			
		|||
        },
 | 
			
		||||
        exportHandler() {
 | 
			
		||||
            const subscriptions = [];
 | 
			
		||||
 | 
			
		||||
            this.subscriptions.forEach(subscription => {
 | 
			
		||||
                subscriptions.push({
 | 
			
		||||
                    url: "https://www.youtube.com" + subscription.url,
 | 
			
		||||
| 
						 | 
				
			
			@ -96,15 +167,53 @@ export default {
 | 
			
		|||
                    service_id: 0,
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const json = JSON.stringify({
 | 
			
		||||
                app_version: "",
 | 
			
		||||
                app_version_int: 0,
 | 
			
		||||
                subscriptions: subscriptions,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.download(json, "subscriptions.json", "application/json");
 | 
			
		||||
        },
 | 
			
		||||
        createGroup() {
 | 
			
		||||
            if (!this.newGroupName || this.channelGroups.some(group => group.groupName == this.newGroupName)) return;
 | 
			
		||||
 | 
			
		||||
            const newGroup = {
 | 
			
		||||
                groupName: this.newGroupName,
 | 
			
		||||
                channels: [],
 | 
			
		||||
            };
 | 
			
		||||
            this.channelGroups.push(newGroup);
 | 
			
		||||
            this.createOrUpdateChannelGroup(newGroup);
 | 
			
		||||
 | 
			
		||||
            this.newGroupName = "";
 | 
			
		||||
 | 
			
		||||
            this.showCreateGroupModal = false;
 | 
			
		||||
        },
 | 
			
		||||
        deleteGroup(group) {
 | 
			
		||||
            this.deleteChannelGroup(group.groupName);
 | 
			
		||||
            this.channelGroups = this.channelGroups.filter(g => g != group);
 | 
			
		||||
            this.selectedGroup = this.channelGroups[0];
 | 
			
		||||
        },
 | 
			
		||||
        checkedChange(subscription) {
 | 
			
		||||
            const channelId = subscription.url.substr(-11);
 | 
			
		||||
            this.selectedGroup.channels = this.selectedGroup.channels.includes(channelId)
 | 
			
		||||
                ? this.selectedGroup.channels.filter(channel => channel != channelId)
 | 
			
		||||
                : this.selectedGroup.channels.concat(channelId);
 | 
			
		||||
            this.createOrUpdateChannelGroup(this.selectedGroup);
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        filteredSubscriptions(_this) {
 | 
			
		||||
            return _this.selectedGroup.groupName == ""
 | 
			
		||||
                ? _this.subscriptions
 | 
			
		||||
                : _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: { ModalComponent },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
.selected {
 | 
			
		||||
    border: 0.1rem outset red;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,8 @@
 | 
			
		|||
        "player": "Player",
 | 
			
		||||
        "livestreams": "Livestreams",
 | 
			
		||||
        "channels": "Channels",
 | 
			
		||||
        "bookmarks": "Bookmarks"
 | 
			
		||||
        "bookmarks": "Bookmarks",
 | 
			
		||||
        "channel_groups": "Channel groups"
 | 
			
		||||
    },
 | 
			
		||||
    "player": {
 | 
			
		||||
        "watch_on": "Watch on {0}"
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +132,9 @@
 | 
			
		|||
        "playlist_bookmarked": "Bookmarked",
 | 
			
		||||
        "dismiss": "Dismiss",
 | 
			
		||||
        "show_more": "Show more",
 | 
			
		||||
        "show_less": "Show less"
 | 
			
		||||
        "show_less": "Show less",
 | 
			
		||||
        "create_group": "Create group",
 | 
			
		||||
        "group_name": "Group name"
 | 
			
		||||
    },
 | 
			
		||||
    "comment": {
 | 
			
		||||
        "pinned_by": "Pinned by {author}",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										21
									
								
								src/main.js
									
										
									
									
									
								
							
							
						
						
									
										21
									
								
								src/main.js
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -21,6 +21,7 @@ import {
 | 
			
		|||
    faServer,
 | 
			
		||||
    faDonate,
 | 
			
		||||
    faBookmark,
 | 
			
		||||
    faEdit,
 | 
			
		||||
} from "@fortawesome/free-solid-svg-icons";
 | 
			
		||||
import { faGithub, faBitcoin, faYoutube } from "@fortawesome/free-brands-svg-icons";
 | 
			
		||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +49,7 @@ library.add(
 | 
			
		|||
    faServer,
 | 
			
		||||
    faDonate,
 | 
			
		||||
    faBookmark,
 | 
			
		||||
    faEdit,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
import router from "@/router/router.js";
 | 
			
		||||
| 
						 | 
				
			
			@ -271,6 +273,25 @@ const mixin = {
 | 
			
		|||
                )
 | 
			
		||||
                .replaceAll("\n", "<br>");
 | 
			
		||||
        },
 | 
			
		||||
        getChannelGroupsCursor() {
 | 
			
		||||
            if (!window.db) return;
 | 
			
		||||
            var tx = window.db.transaction("channel_groups", "readonly");
 | 
			
		||||
            var store = tx.objectStore("channel_groups");
 | 
			
		||||
            return store.index("groupName").openCursor();
 | 
			
		||||
        },
 | 
			
		||||
        createOrUpdateChannelGroup(group) {
 | 
			
		||||
            var tx = window.db.transaction("channel_groups", "readwrite");
 | 
			
		||||
            var store = tx.objectStore("channel_groups");
 | 
			
		||||
            store.put({
 | 
			
		||||
                groupName: group.groupName,
 | 
			
		||||
                channels: JSON.stringify(group.channels),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        deleteChannelGroup(groupName) {
 | 
			
		||||
            var tx = window.db.transaction("channel_groups", "readwrite");
 | 
			
		||||
            var store = tx.objectStore("channel_groups");
 | 
			
		||||
            store.delete(groupName);
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        authenticated(_this) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue