mirror of
https://github.com/TeamPiped/Piped.git
synced 2026-05-02 21:47:49 +00:00
296 lines
12 KiB
Vue
296 lines
12 KiB
Vue
<template>
|
|
<h1 v-t="'titles.subscriptions'" class="my-4 text-center font-bold" />
|
|
<!-- import / export section -->
|
|
<div class="flex flex-wrap justify-between">
|
|
<div class="flex gap-1">
|
|
<!-- import json/csv -->
|
|
<router-link
|
|
v-t="'actions.import_from_json_csv'"
|
|
to="/import"
|
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
/>
|
|
<!-- export to json -->
|
|
<button
|
|
v-t="'actions.export_to_json'"
|
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
@click="exportHandler"
|
|
/>
|
|
</div>
|
|
<div class="m-1 flex flex-wrap gap-1">
|
|
<!-- import channel groups to json-->
|
|
<div>
|
|
<label
|
|
for="fileSelector"
|
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
v-text="`${$t('actions.import_from_json')} (${$t('titles.channel_groups')})`"
|
|
/>
|
|
<input
|
|
id="fileSelector"
|
|
ref="fileSelector"
|
|
type="file"
|
|
class="hidden"
|
|
multiple="multiple"
|
|
@change="importGroupsHandler"
|
|
/>
|
|
</div>
|
|
|
|
<!-- export channel groups to json -->
|
|
<button
|
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
@click="exportGroupsHandler"
|
|
v-text="`${$t('actions.export_to_json')} (${$t('titles.channel_groups')})`"
|
|
/>
|
|
</div>
|
|
<!-- subscriptions count, only shown if there are any -->
|
|
<div v-if="subscriptions.length > 0" class="flex gap-1 self-center">
|
|
<i18n-t keypath="subscriptions.subscribed_channels_count">{{ subscriptions.length }}</i18n-t>
|
|
</div>
|
|
</div>
|
|
<br />
|
|
<hr />
|
|
<div class="flex w-full flex-wrap">
|
|
<button
|
|
v-for="group in channelGroups"
|
|
:key="group.groupName"
|
|
class="mx-1 inline-block w-max cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
:class="{ 'border-2 border-red-500': selectedGroup === group }"
|
|
@click="selectGroup(group)"
|
|
>
|
|
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
|
|
<div v-if="group.groupName != '' && selectedGroup == group">
|
|
<i-fa6-solid-pen class="mx-2" @click="showEditGroupModal = true" />
|
|
<i-fa6-solid-circle-minus class="mx-2" @click="groupToDelete = group.groupName" />
|
|
</div>
|
|
</button>
|
|
<ConfirmModal
|
|
v-if="groupToDelete != null"
|
|
:message="$t('actions.delete_group_confirm')"
|
|
@close="groupToDelete = null"
|
|
@confirm="deleteGroup(groupToDelete)"
|
|
/>
|
|
<button
|
|
class="mx-1 inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
@click="showCreateGroupModal = true"
|
|
>
|
|
<i-fa6-solid-circle-plus />
|
|
</button>
|
|
</div>
|
|
<br />
|
|
<hr />
|
|
<!-- Subscriptions card list -->
|
|
<div class="max-md:flex-wrap xl:grid xl:grid-cols-5">
|
|
<!-- channel info card -->
|
|
<div
|
|
v-for="subscription in filteredSubscriptions"
|
|
:key="subscription.url"
|
|
class="m-2 rounded-lg border border-gray-500 p-1"
|
|
>
|
|
<router-link :to="subscription.url" class="flex p-2 text-4xl font-bold">
|
|
<img :src="subscription.avatar" class="h-fit rounded-full" width="48" height="48" />
|
|
<span class="mx-2 self-center" v-text="subscription.name" />
|
|
</router-link>
|
|
<!-- subscribe / unsubscribe btn -->
|
|
<button
|
|
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
|
|
class="mt-2 inline-block w-full cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
@click="handleButton(subscription)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<br />
|
|
|
|
<CreateGroupModal
|
|
v-if="showCreateGroupModal"
|
|
:on-create-group="createGroup"
|
|
@close="showCreateGroupModal = false"
|
|
/>
|
|
|
|
<ModalComponent v-if="showEditGroupModal" @close="showEditGroupModal = false">
|
|
<div class="mt-3 mb-5 flex justify-between">
|
|
<input
|
|
v-model="editedGroupName"
|
|
type="text"
|
|
class="h-8 rounded-md bg-gray-300 px-2.5 text-gray-600 focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:bg-dark-400 dark:text-gray-400"
|
|
/>
|
|
<button
|
|
v-t="'actions.okay'"
|
|
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
|
|
:placeholder="$t('actions.group_name')"
|
|
@click="editGroupName()"
|
|
/>
|
|
</div>
|
|
<div class="mt-3 mb-2 flex h-[80vh] flex-col overflow-y-scroll pr-2">
|
|
<div v-for="subscription in subscriptions" :key="subscription.name">
|
|
<div class="mr-3 flex items-center justify-between">
|
|
<a :href="subscription.url" target="_blank" class="flex items-center overflow-hidden">
|
|
<img :src="subscription.avatar" class="size-8 rounded-full" />
|
|
<span class="ml-2">{{ subscription.name }}</span>
|
|
</a>
|
|
<UiCheckbox
|
|
:model-value="selectedGroup.channels.includes(subscription.url.substr(-24))"
|
|
@update:model-value="checkedChange(subscription)"
|
|
/>
|
|
</div>
|
|
<hr />
|
|
</div>
|
|
</div>
|
|
</ModalComponent>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onActivated } from "vue";
|
|
import ModalComponent from "./ModalComponent.vue";
|
|
import CreateGroupModal from "./CreateGroupModal.vue";
|
|
import ConfirmModal from "./ConfirmModal.vue";
|
|
import UiCheckbox from "./ui/Checkbox.vue";
|
|
import { fetchJson, authApiUrl, getAuthToken, isAuthenticated } from "@/composables/useApi.js";
|
|
import { fetchSubscriptions, handleLocalSubscriptions } from "@/composables/useSubscriptions.js";
|
|
import { getChannelGroups, createOrUpdateChannelGroup, deleteChannelGroup } from "@/composables/useChannelGroups.js";
|
|
import { download } from "@/composables/useMisc.js";
|
|
|
|
const fileSelector = ref(null);
|
|
const subscriptions = ref([]);
|
|
const selectedGroup = ref({
|
|
groupName: "",
|
|
channels: [],
|
|
});
|
|
const channelGroups = ref([]);
|
|
const showCreateGroupModal = ref(false);
|
|
const showEditGroupModal = ref(false);
|
|
const editedGroupName = ref("");
|
|
const groupToDelete = ref(null);
|
|
|
|
const filteredSubscriptions = computed(() => {
|
|
return selectedGroup.value.groupName == ""
|
|
? subscriptions.value
|
|
: subscriptions.value.filter(channel => selectedGroup.value.channels.includes(channel.url.substr(-24)));
|
|
});
|
|
|
|
function handleButton(subscription) {
|
|
const channelId = subscription.url.split("/")[2];
|
|
if (isAuthenticated()) {
|
|
fetchJson(authApiUrl() + (subscription.subscribed ? "/unsubscribe" : "/subscribe"), null, {
|
|
method: "POST",
|
|
body: JSON.stringify({
|
|
channelId: channelId,
|
|
}),
|
|
headers: {
|
|
Authorization: getAuthToken(),
|
|
"Content-Type": "application/json",
|
|
},
|
|
});
|
|
} else {
|
|
handleLocalSubscriptions(channelId);
|
|
}
|
|
subscription.subscribed = !subscription.subscribed;
|
|
}
|
|
|
|
function exportHandler() {
|
|
const subs = [];
|
|
subscriptions.value.forEach(subscription => {
|
|
subs.push({
|
|
url: "https://www.youtube.com" + subscription.url,
|
|
name: subscription.name,
|
|
service_id: 0,
|
|
});
|
|
});
|
|
const json = JSON.stringify({
|
|
app_version: "",
|
|
app_version_int: 0,
|
|
subscriptions: subs,
|
|
});
|
|
download(json, "subscriptions.json", "application/json");
|
|
}
|
|
|
|
function selectGroup(group) {
|
|
selectedGroup.value = group;
|
|
editedGroupName.value = group.groupName;
|
|
}
|
|
|
|
function createGroup(newGroupName) {
|
|
if (!newGroupName || channelGroups.value.some(group => group.groupName == newGroupName)) return;
|
|
|
|
const newGroup = {
|
|
groupName: newGroupName,
|
|
channels: [],
|
|
};
|
|
channelGroups.value.push(newGroup);
|
|
createOrUpdateChannelGroup(newGroup);
|
|
|
|
showCreateGroupModal.value = false;
|
|
}
|
|
|
|
function editGroupName() {
|
|
const oldGroupName = selectedGroup.value.groupName;
|
|
const newGroupName = editedGroupName.value;
|
|
|
|
if (!newGroupName || newGroupName == oldGroupName) return;
|
|
if (channelGroups.value.some(group => group.groupName == newGroupName)) return;
|
|
|
|
selectedGroup.value.groupName = newGroupName;
|
|
createOrUpdateChannelGroup(selectedGroup.value);
|
|
deleteChannelGroup(oldGroupName);
|
|
|
|
showEditGroupModal.value = false;
|
|
}
|
|
|
|
function deleteGroup(group) {
|
|
deleteChannelGroup(group);
|
|
channelGroups.value = channelGroups.value.filter(g => g.groupName != group);
|
|
selectedGroup.value = channelGroups.value[0] || {};
|
|
groupToDelete.value = null;
|
|
}
|
|
|
|
function checkedChange(subscription) {
|
|
const channelId = subscription.url.substr(-24);
|
|
selectedGroup.value.channels = selectedGroup.value.channels.includes(channelId)
|
|
? selectedGroup.value.channels.filter(channel => channel != channelId)
|
|
: selectedGroup.value.channels.concat(channelId);
|
|
createOrUpdateChannelGroup(selectedGroup.value);
|
|
}
|
|
|
|
async function importGroupsHandler() {
|
|
const files = fileSelector.value.files;
|
|
for (let file of files) {
|
|
const groups = JSON.parse(await file.text()).groups;
|
|
for (let group of groups) {
|
|
createOrUpdateChannelGroup(group);
|
|
channelGroups.value.push(group);
|
|
}
|
|
}
|
|
}
|
|
|
|
function exportGroupsHandler() {
|
|
const json = {
|
|
format: "Piped",
|
|
version: 1,
|
|
groups: channelGroups.value.slice(1),
|
|
};
|
|
download(JSON.stringify(json), "channel_groups.json", "application/json");
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchSubscriptions().then(json => {
|
|
if (json.error) {
|
|
alert(json.error);
|
|
return;
|
|
}
|
|
|
|
subscriptions.value = json;
|
|
subscriptions.value.forEach(subscription => (subscription.subscribed = true));
|
|
});
|
|
|
|
channelGroups.value.push(selectedGroup.value);
|
|
if (!window.db) return;
|
|
|
|
(async () => {
|
|
const groups = await getChannelGroups();
|
|
channelGroups.value.push(...groups);
|
|
})();
|
|
});
|
|
|
|
onActivated(() => {
|
|
document.title = "Subscriptions - Piped";
|
|
});
|
|
</script>
|