mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 11:55:38 +00:00
Upgraded all components to vue 3 & typescript
This commit is contained in:
6
src/components/torrent/ActiveTorrents.vue
Normal file
6
src/components/torrent/ActiveTorrents.vue
Normal file
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<code
|
||||
>Monitor active torrents requested. Requires authentication with owners plex
|
||||
library!</code
|
||||
>
|
||||
</template>
|
||||
157
src/components/torrent/TorrentSearchResults.vue
Normal file
157
src/components/torrent/TorrentSearchResults.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="container" v-if="query?.length">
|
||||
<h2 class="torrent-header-text">
|
||||
Searching for: <span class="query">{{ query }}</span>
|
||||
</h2>
|
||||
|
||||
<loader v-if="loading" type="section" />
|
||||
<div v-else>
|
||||
<div v-if="torrents.length > 0" class="torrent-table">
|
||||
<torrent-table :torrents="torrents" @magnet="addTorrent" />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<div v-else class="no-results">
|
||||
<h2>No results found</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, defineProps } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { sortableSize } from "../../utils";
|
||||
import { searchTorrents, addMagnet } from "../../api";
|
||||
|
||||
import Loader from "@/components/ui/Loader.vue";
|
||||
import TorrentTable from "@/components/torrent/TorrentTable.vue";
|
||||
import type { Ref } from "vue";
|
||||
import type ITorrent from "../../interfaces/ITorrent";
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
tmdb_id?: number;
|
||||
}
|
||||
|
||||
const loading: Ref<boolean> = ref(true);
|
||||
const torrents: Ref<ITorrent[]> = ref([]);
|
||||
const release_types: Ref<string[]> = ref(["all"]);
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const store = useStore();
|
||||
const notifications: {
|
||||
info;
|
||||
success;
|
||||
error;
|
||||
} = inject("notifications");
|
||||
|
||||
fetchTorrents();
|
||||
|
||||
function fetchTorrents() {
|
||||
loading.value = true;
|
||||
|
||||
searchTorrents(props.query)
|
||||
.then(torrentResponse => (torrents.value = torrentResponse?.results))
|
||||
.then(() => updateResultCountDisplay())
|
||||
.finally(() => (loading.value = false));
|
||||
}
|
||||
|
||||
function updateResultCountDisplay() {
|
||||
store.dispatch("torrentModule/setResults", torrents.value);
|
||||
store.dispatch(
|
||||
"torrentModule/setResultCount",
|
||||
torrents.value?.length || -1
|
||||
);
|
||||
}
|
||||
|
||||
function addTorrent(torrent: ITorrent) {
|
||||
const { name, magnet } = torrent;
|
||||
|
||||
notifications.info({
|
||||
title: "Adding torrent 🧲",
|
||||
description: props.query,
|
||||
timeout: 3000
|
||||
});
|
||||
|
||||
addMagnet(magnet, name, props.tmdb_id)
|
||||
.then(resp => {
|
||||
notifications.success({
|
||||
title: "Torrent added 🎉",
|
||||
description: props.query,
|
||||
timeout: 3000
|
||||
});
|
||||
})
|
||||
.catch(resp => {
|
||||
console.log("Error while adding torrent:", resp?.data);
|
||||
notifications.error({
|
||||
title: "Failed to add torrent 🙅♀️",
|
||||
description: "Check console for more info",
|
||||
timeout: 3000
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/elements";
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
max-width: unset !important;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
display: flex;
|
||||
padding-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.torrent-header-text {
|
||||
font-weight: 300;
|
||||
text-transform: uppercase;
|
||||
font-size: 20px;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
.query {
|
||||
font-weight: 500;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@include mobile {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.download {
|
||||
&__icon {
|
||||
fill: $text-color-70;
|
||||
height: 1.2rem;
|
||||
|
||||
&:hover {
|
||||
fill: $text-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.active &__icon {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
250
src/components/torrent/TorrentTable.vue
Normal file
250
src/components/torrent/TorrentTable.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<table>
|
||||
<thead class="table__header noselect">
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
@click="sortTable(column)"
|
||||
:class="column === selectedColumn ? 'active' : null"
|
||||
>
|
||||
{{ column }}
|
||||
<span v-if="prevCol === column && direction">↑</span>
|
||||
<span v-if="prevCol === column && !direction">↓</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="torrent in torrents"
|
||||
class="table__content"
|
||||
:key="torrent.magnet"
|
||||
>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
||||
<td @click="() => emit('magnet', torrent)" class="download">
|
||||
<IconMagnet />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineProps, defineEmits } from "vue";
|
||||
import IconMagnet from "@/icons/IconMagnet.vue";
|
||||
import { sortableSize } from "../../utils";
|
||||
import type { Ref } from "vue";
|
||||
import type ITorrent from "../../interfaces/ITorrent";
|
||||
|
||||
interface Props {
|
||||
torrents: Array<ITorrent>;
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: "magnet", torrent: ITorrent): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emit>();
|
||||
|
||||
const columns: string[] = ["name", "seed", "size", "add"];
|
||||
|
||||
const torrents: Ref<ITorrent[]> = ref(props.torrents);
|
||||
const direction: Ref<boolean> = ref(false);
|
||||
const selectedColumn: Ref<string> = ref(columns[0]);
|
||||
const prevCol: Ref<string> = ref("");
|
||||
|
||||
function expand(event: MouseEvent, text: string) {
|
||||
const elementClicked = event.target as HTMLElement;
|
||||
const tableRow = elementClicked.parentElement;
|
||||
const scopedStyleDataVariable = Object.keys(tableRow.dataset)[0];
|
||||
|
||||
const existingExpandedElement =
|
||||
document.getElementsByClassName("expanded")[0];
|
||||
const clickedSameTwice =
|
||||
existingExpandedElement?.previousSibling?.isEqualNode(tableRow);
|
||||
|
||||
if (existingExpandedElement) {
|
||||
existingExpandedElement.remove();
|
||||
|
||||
// Clicked the same element twice, remove and return
|
||||
// not recreate and collapse
|
||||
if (clickedSameTwice) return;
|
||||
}
|
||||
|
||||
const expandedRow = document.createElement("tr");
|
||||
const expandedCol = document.createElement("td");
|
||||
expandedRow.dataset[scopedStyleDataVariable] = "";
|
||||
expandedCol.dataset[scopedStyleDataVariable] = "";
|
||||
expandedRow.className = "expanded";
|
||||
expandedCol.innerText = text;
|
||||
expandedCol.colSpan = 4;
|
||||
|
||||
expandedRow.appendChild(expandedCol);
|
||||
tableRow.insertAdjacentElement("afterend", expandedRow);
|
||||
}
|
||||
|
||||
function sortTable(col, sameDirection = false) {
|
||||
if (prevCol.value === col && sameDirection === false) {
|
||||
direction.value = !direction.value;
|
||||
}
|
||||
|
||||
if (col === "name") sortName();
|
||||
else if (col === "seed") sortSeed();
|
||||
else if (col === "size") sortSize();
|
||||
|
||||
prevCol.value = col;
|
||||
}
|
||||
|
||||
function sortName() {
|
||||
const torrentsCopy = [...torrents.value];
|
||||
if (direction.value) {
|
||||
torrents.value = torrentsCopy.sort((a, b) => (a.name < b.name ? 1 : -1));
|
||||
} else {
|
||||
torrents.value = torrentsCopy.sort((a, b) => (a.name > b.name ? 1 : -1));
|
||||
}
|
||||
}
|
||||
|
||||
function sortSeed() {
|
||||
const torrentsCopy = [...torrents.value];
|
||||
if (direction.value) {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => parseInt(a.seed) - parseInt(b.seed)
|
||||
);
|
||||
} else {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => parseInt(b.seed) - parseInt(a.seed)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function sortSize() {
|
||||
const torrentsCopy = [...torrents.value];
|
||||
if (direction.value) {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => sortableSize(a.size) - sortableSize(b.size)
|
||||
);
|
||||
} else {
|
||||
torrents.value = torrentsCopy.sort(
|
||||
(a, b) => sortableSize(b.size) - sortableSize(a.size)
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "src/scss/variables";
|
||||
@import "src/scss/media-queries";
|
||||
@import "src/scss/elements";
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
margin-top: 0.5rem;
|
||||
width: 100%;
|
||||
// border-collapse: collapse;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 0.5px solid var(--background-color-40);
|
||||
@include mobile {
|
||||
white-space: nowrap;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
thead {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
color: var(--table-header-text-color);
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: var(--table-background-color);
|
||||
// background-color: black;
|
||||
// color: var(--color-green);
|
||||
letter-spacing: 0.8px;
|
||||
font-size: 1rem;
|
||||
|
||||
th:last-of-type {
|
||||
padding-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
// first column
|
||||
tr td:first-of-type {
|
||||
position: relative;
|
||||
padding: 0 0.3rem;
|
||||
cursor: default;
|
||||
word-break: break-all;
|
||||
border-left: 1px solid var(--table-background-color);
|
||||
|
||||
@include mobile {
|
||||
max-width: 40vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// all columns except first
|
||||
tr td:not(td:first-of-type) {
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// last column
|
||||
tr td:last-of-type {
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
border-right: 1px solid var(--table-background-color);
|
||||
|
||||
svg {
|
||||
width: 21px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding: 0.3rem 0;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
// alternate background color per row
|
||||
tr:nth-child(even) {
|
||||
background-color: var(--background-70);
|
||||
}
|
||||
|
||||
// last element rounded corner border
|
||||
tr:last-of-type {
|
||||
td {
|
||||
border-bottom: 1px solid var(--table-background-color);
|
||||
}
|
||||
|
||||
td:first-of-type {
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
td:last-of-type {
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.expanded {
|
||||
padding: 0.25rem 1rem;
|
||||
max-width: 100%;
|
||||
border-left: 1px solid $text-color;
|
||||
border-right: 1px solid $text-color;
|
||||
border-bottom: 1px solid $text-color;
|
||||
|
||||
td {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
padding: 0.5rem 0.15rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/components/torrent/TruncatedTorrentResults.vue
Normal file
86
src/components/torrent/TruncatedTorrentResults.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<torrent-search-results
|
||||
:query="query"
|
||||
:tmdb_id="tmdb_id"
|
||||
:class="{ truncated: truncated }"
|
||||
><div
|
||||
v-if="truncated"
|
||||
class="load-more"
|
||||
role="button"
|
||||
@click="truncated = false"
|
||||
>
|
||||
<icon-arrow-down />
|
||||
</div>
|
||||
</torrent-search-results>
|
||||
|
||||
<div class="edit-query-btn-container">
|
||||
<seasonedButton @click="openInTorrentPage"
|
||||
>View on torrent page</seasonedButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, defineProps } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import TorrentSearchResults from "@/components/torrent/TorrentSearchResults.vue";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton.vue";
|
||||
import IconArrowDown from "@/icons/IconArrowDown.vue";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
query: string;
|
||||
tmdb_id?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const router = useRouter();
|
||||
|
||||
const truncated: Ref<boolean> = ref(true);
|
||||
|
||||
function openInTorrentPage() {
|
||||
if (!props.query?.length) {
|
||||
router.push("/torrents");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push({ path: "/torrents", query: { query: props.query } });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:global(.truncated .torrent-table) {
|
||||
position: relative;
|
||||
max-height: 500px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
bottom: 0rem;
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--background-color) 20%,
|
||||
var(--background-0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 30px;
|
||||
fill: var(--text-color);
|
||||
}
|
||||
|
||||
.edit-query-btn-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user