mirror of
https://github.com/TeamPiped/Piped.git
synced 2024-08-14 23:57:27 +00:00
Subscription groups
This commit is contained in:
parent
5e955b2286
commit
c217d5e4e3
6 changed files with 200 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;
|
||||
|
|
22
src/components/DefaultValueCheckbox.vue
Normal file
22
src/components/DefaultValueCheckbox.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Wrapper around v-model to allow default values without requiring to use a v-model inside the calling component
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
defaultValue: Boolean,
|
||||
callback: Function,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.value = this.defaultValue;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input type="checkbox" class="checkbox" v-model="value" @change="callback()" />
|
||||
</template>
|
|
@ -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,20 @@ 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 = this.channelGroups.concat({
|
||||
groupName: group.groupName,
|
||||
channels: JSON.parse(group.channels),
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
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,48 @@
|
|||
</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>
|
||||
<DefaultValueCheckbox
|
||||
:default-value="selectedGroup.channels.includes(subscription.url.substr(-11))"
|
||||
:callback="() => checkedChange(subscription)"
|
||||
/>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
</div>
|
||||
</ModalComponent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DefaultValueCheckbox from "./DefaultValueCheckbox.vue";
|
||||
import ModalComponent from "./ModalComponent.vue";
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
subscriptions: [],
|
||||
selectedGroup: {
|
||||
groupName: "",
|
||||
channels: [],
|
||||
},
|
||||
channelGroups: [],
|
||||
showCreateGroupModal: false,
|
||||
showEditGroupModal: false,
|
||||
newGroupName: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -50,6 +105,21 @@ export default {
|
|||
this.subscriptions = json;
|
||||
this.subscriptions.forEach(subscription => (subscription.subscribed = true));
|
||||
});
|
||||
|
||||
this.channelGroups = this.channelGroups.concat(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 = this.channelGroups.concat({
|
||||
groupName: group.groupName,
|
||||
channels: JSON.parse(group.channels),
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
activated() {
|
||||
document.title = "Subscriptions - Piped";
|
||||
|
@ -88,7 +158,6 @@ export default {
|
|||
},
|
||||
exportHandler() {
|
||||
const subscriptions = [];
|
||||
|
||||
this.subscriptions.forEach(subscription => {
|
||||
subscriptions.push({
|
||||
url: "https://www.youtube.com" + subscription.url,
|
||||
|
@ -96,15 +165,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 = this.channelGroups.concat(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, DefaultValueCheckbox },
|
||||
};
|
||||
</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…
Reference in a new issue