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) {
|
if ("indexedDB" in window) {
|
||||||
const request = indexedDB.open("piped-db", 3);
|
const request = indexedDB.open("piped-db", 4);
|
||||||
request.onupgradeneeded = ev => {
|
request.onupgradeneeded = ev => {
|
||||||
const db = request.result;
|
const db = request.result;
|
||||||
console.log("Upgrading object store.");
|
console.log("Upgrading object store.");
|
||||||
|
@ -73,6 +73,10 @@ export default {
|
||||||
store.createIndex("playlist_id_idx", "playlistId", { unique: true });
|
store.createIndex("playlist_id_idx", "playlistId", { unique: true });
|
||||||
store.createIndex("id_idx", "id", { unique: true, autoIncrement: 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 => {
|
request.onsuccess = e => {
|
||||||
window.db = e.target.result;
|
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}`" />
|
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
|
||||||
</select>
|
</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">
|
<span class="md:float-right">
|
||||||
<SortingSelector by-key="uploaded" @apply="order => videos.sort(order)" />
|
<SortingSelector by-key="uploaded" @apply="order => videos.sort(order)" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -25,7 +38,7 @@
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">
|
<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" />
|
<VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" />
|
||||||
</template>
|
</template>
|
||||||
</LoadingIndicatorPage>
|
</LoadingIndicatorPage>
|
||||||
|
@ -50,6 +63,8 @@ export default {
|
||||||
videos: [],
|
videos: [],
|
||||||
availableFilters: ["all", "shorts", "videos"],
|
availableFilters: ["all", "shorts", "videos"],
|
||||||
selectedFilter: "all",
|
selectedFilter: "all",
|
||||||
|
selectedGroupName: "",
|
||||||
|
channelGroups: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -57,6 +72,12 @@ export default {
|
||||||
if (_this.authenticated) return _this.authApiUrl() + "/feed/rss?authToken=" + _this.getAuthToken();
|
if (_this.authenticated) return _this.authApiUrl() + "/feed/rss?authToken=" + _this.getAuthToken();
|
||||||
else return _this.authApiUrl() + "/feed/unauthenticated/rss?channels=" + _this.getUnauthenticatedChannels();
|
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() {
|
mounted() {
|
||||||
this.fetchFeed().then(videos => {
|
this.fetchFeed().then(videos => {
|
||||||
|
@ -66,6 +87,20 @@ export default {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.selectedFilter = this.getPreferenceString("feedFilter") ?? "all";
|
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() {
|
activated() {
|
||||||
document.title = this.$t("titles.feed") + " - Piped";
|
document.title = this.$t("titles.feed") + " - Piped";
|
||||||
|
|
|
@ -15,12 +15,32 @@
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<hr />
|
<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 -->
|
<!-- Subscriptions card list -->
|
||||||
<div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
|
<div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
|
||||||
<!-- channel info card -->
|
<!-- channel info card -->
|
||||||
<div
|
<div
|
||||||
class="col m-2 p-1 border rounded-lg border-gray-500"
|
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"
|
:key="subscription.url"
|
||||||
>
|
>
|
||||||
<router-link :to="subscription.url" class="flex p-2 font-bold text-4x4">
|
<router-link :to="subscription.url" class="flex p-2 font-bold text-4x4">
|
||||||
|
@ -36,13 +56,48 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import DefaultValueCheckbox from "./DefaultValueCheckbox.vue";
|
||||||
|
import ModalComponent from "./ModalComponent.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
subscriptions: [],
|
subscriptions: [],
|
||||||
|
selectedGroup: {
|
||||||
|
groupName: "",
|
||||||
|
channels: [],
|
||||||
|
},
|
||||||
|
channelGroups: [],
|
||||||
|
showCreateGroupModal: false,
|
||||||
|
showEditGroupModal: false,
|
||||||
|
newGroupName: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -50,6 +105,21 @@ export default {
|
||||||
this.subscriptions = json;
|
this.subscriptions = json;
|
||||||
this.subscriptions.forEach(subscription => (subscription.subscribed = true));
|
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() {
|
activated() {
|
||||||
document.title = "Subscriptions - Piped";
|
document.title = "Subscriptions - Piped";
|
||||||
|
@ -88,7 +158,6 @@ export default {
|
||||||
},
|
},
|
||||||
exportHandler() {
|
exportHandler() {
|
||||||
const subscriptions = [];
|
const subscriptions = [];
|
||||||
|
|
||||||
this.subscriptions.forEach(subscription => {
|
this.subscriptions.forEach(subscription => {
|
||||||
subscriptions.push({
|
subscriptions.push({
|
||||||
url: "https://www.youtube.com" + subscription.url,
|
url: "https://www.youtube.com" + subscription.url,
|
||||||
|
@ -96,15 +165,53 @@ export default {
|
||||||
service_id: 0,
|
service_id: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const json = JSON.stringify({
|
const json = JSON.stringify({
|
||||||
app_version: "",
|
app_version: "",
|
||||||
app_version_int: 0,
|
app_version_int: 0,
|
||||||
subscriptions: subscriptions,
|
subscriptions: subscriptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.download(json, "subscriptions.json", "application/json");
|
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>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.selected {
|
||||||
|
border: 0.1rem outset red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -13,7 +13,8 @@
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
"livestreams": "Livestreams",
|
"livestreams": "Livestreams",
|
||||||
"channels": "Channels",
|
"channels": "Channels",
|
||||||
"bookmarks": "Bookmarks"
|
"bookmarks": "Bookmarks",
|
||||||
|
"channel_groups": "Channel groups"
|
||||||
},
|
},
|
||||||
"player": {
|
"player": {
|
||||||
"watch_on": "Watch on {0}"
|
"watch_on": "Watch on {0}"
|
||||||
|
@ -131,7 +132,9 @@
|
||||||
"playlist_bookmarked": "Bookmarked",
|
"playlist_bookmarked": "Bookmarked",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"show_more": "Show more",
|
"show_more": "Show more",
|
||||||
"show_less": "Show less"
|
"show_less": "Show less",
|
||||||
|
"create_group": "Create group",
|
||||||
|
"group_name": "Group name"
|
||||||
},
|
},
|
||||||
"comment": {
|
"comment": {
|
||||||
"pinned_by": "Pinned by {author}",
|
"pinned_by": "Pinned by {author}",
|
||||||
|
|
21
src/main.js
21
src/main.js
|
@ -21,6 +21,7 @@ import {
|
||||||
faServer,
|
faServer,
|
||||||
faDonate,
|
faDonate,
|
||||||
faBookmark,
|
faBookmark,
|
||||||
|
faEdit,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faGithub, faBitcoin, faYoutube } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faBitcoin, faYoutube } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||||
|
@ -48,6 +49,7 @@ library.add(
|
||||||
faServer,
|
faServer,
|
||||||
faDonate,
|
faDonate,
|
||||||
faBookmark,
|
faBookmark,
|
||||||
|
faEdit,
|
||||||
);
|
);
|
||||||
|
|
||||||
import router from "@/router/router.js";
|
import router from "@/router/router.js";
|
||||||
|
@ -271,6 +273,25 @@ const mixin = {
|
||||||
)
|
)
|
||||||
.replaceAll("\n", "<br>");
|
.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: {
|
computed: {
|
||||||
authenticated(_this) {
|
authenticated(_this) {
|
||||||
|
|
Loading…
Reference in a new issue