Compare commits

..

9 Commits

11 changed files with 144 additions and 43 deletions

View File

@ -56,7 +56,7 @@ onMounted(async () => {
<h3 class="text-2xl font-semibold tracking-tight"> <h3 class="text-2xl font-semibold tracking-tight">
{{ props.item.title }} {{ props.item.title }}
</h3> </h3>
<p class="text-sm"> <p class="text-sm line-clamp-5">
{{ props.item.material_data?.description }} {{ props.item.material_data?.description }}
</p> </p>
<div class="flex items-center pt-2"> <div class="flex items-center pt-2">

View File

@ -18,6 +18,7 @@ export const buttonVariants = cva(
ghost: ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline', link: 'text-primary underline-offset-4 hover:underline',
borderless: 'border-0 cursor-pointer size-20'
}, },
size: { size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3', default: 'h-9 px-4 py-2 has-[>svg]:px-3',

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import type { RouteLocationNormalized } from 'vue-router'
import { updateUrlParameter } from '~/components/util/route'
const props = defineProps<Props>();
const route = useRoute()
const currentEpisode = computed(() => Number(route.query.episode as string))
</script>
<script lang="ts">
function changeEpisode(route: RouteLocationNormalized, props: Props, currentEpisode: number) {
let episodeOffset = 0
if (props.type == EpisodeChangeType.NEXT) {
episodeOffset = 1
}
if (props.type == EpisodeChangeType.PREVIOUS) {
episodeOffset = -1
}
updateUrlParameter(route, 'episode', String(currentEpisode + episodeOffset))
}
interface Props {
type: EpisodeChangeType;
}
export enum EpisodeChangeType {
NEXT = "mage:next-fill",
PREVIOUS = "mage:previous-fill",
}
</script>
<template>
<UiButton v-on:click="changeEpisode(route, props, currentEpisode)" variant="borderless" size="icon">
<Icon :icon="props.type" class="size-5" />
</UiButton>
</template>

View File

@ -1,11 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import Artplayer from "artplayer"; import Artplayer from "artplayer";
import Hls from "hls.js"; import Hls from "hls.js";
import { createVueControlSmart } from "~/components/util/player-control";
import { EpisodeChangeType } from "./ChangeEpisodeButton.vue";
import { UiPlayerChangeEpisodeButton } from "#components";
interface Props { interface Props {
urls: any; urls: any[];
id: string; episodeButton: boolean;
nextEpisodeButton: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -14,30 +16,12 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits(['get-instance']) const emit = defineEmits(['get-instance'])
const options = computed(() => { const options = computed(() => {
const controls = []
if (props.nextEpisodeButton) {
controls.push(
{
name: 'next-episode',
index: 50,
position: 'left',
html: '<button>Test</button>',
tooltip: 'Next episode',
style: {
color: 'red',
},
click: function () {
},
},
)
}
return { return {
url: props.urls[0].url || '', url: props.urls[0].url || '',
type: 'm3u8', type: 'm3u8',
customType: { customType: {
m3u8: playM3u8, m3u8: playM3u8,
}, },
quality: props.urls,
autoSize: true, autoSize: true,
autoMini: true, autoMini: true,
playbackRate: true, playbackRate: true,
@ -47,8 +31,6 @@ const options = computed(() => {
lock: true, lock: true,
autoOrientation: true, autoOrientation: true,
autoPlayback: true, autoPlayback: true,
id: props.id,
controls: controls,
} }
}) })
const artplayerRef = ref(); const artplayerRef = ref();
@ -74,6 +56,24 @@ onMounted(() => {
container: artplayerRef.value, container: artplayerRef.value,
...options.value, ...options.value,
}) })
if (props.episodeButton) {
instance.value.controls.add(
createVueControlSmart(UiPlayerChangeEpisodeButton, {
name: "next-episode",
index: 50,
tooltip: "Next episode",
type: EpisodeChangeType.NEXT,
})
)
instance.value.controls.add(
createVueControlSmart(UiPlayerChangeEpisodeButton, {
name: "previous-episode",
index: 50,
tooltip: "Previous episode",
type: EpisodeChangeType.PREVIOUS,
})
)
}
nextTick(() => { nextTick(() => {
emit('get-instance') emit('get-instance')
}) })
@ -84,6 +84,14 @@ onBeforeUnmount(() => {
instance.value.destroy(false) instance.value.destroy(false)
} }
}) })
watch([instance, () => props.urls], async ([artplayer, urls]) => {
if (!artplayer) return;
urls = urls.reverse()
artplayer.quality = urls;
artplayer.switch = urls[0].url;
instance.value = artplayer;
})
</script> </script>
<template> <template>

View File

@ -0,0 +1,42 @@
import { createApp, h } from 'vue'
export function createVueControlSmart(component, props = {}, events = {}) {
let vueApp = null
return {
name: props.name || 'vue-control',
index: props.index || 0,
position: props.position || 'left',
html: `<div class="vue-control-wrapper" data-vue-control="${component.name}"></div>`,
tooltip: props.tooltip || '',
mounted: function ($control) {
const container = $control.querySelector('.vue-control-wrapper')
if (container && !vueApp) {
vueApp = createApp({
render() {
return h(component, {
...props,
...Object.entries(events).reduce((acc, [eventName, handler]) => {
acc[`on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`] = handler
return acc
}, {})
})
}
})
vueApp.mount(container)
$control._vueApp = vueApp
}
},
destroy: function ($control) {
if ($control._vueApp) {
$control._vueApp.unmount()
$control._vueApp = null
vueApp = null
}
}
}
}

View File

@ -0,0 +1,9 @@
export const updateUrlParameter = async (route, paramName, newValue) => {
await navigateTo({
path: route.path,
query: {
...route.query,
[paramName]: newValue
}
}, { replace: true })
}

View File

@ -8,8 +8,7 @@
<p>Episode: {{ episode }}</p> <p>Episode: {{ episode }}</p>
<div v-if="hlsUrls"> <div v-if="hlsUrls">
<Player :id="mediaId.toString().concat(episode?.toString() || '')" :urls="hlsUrls" :episode="episode" <Player :urls="hlsUrls" :episode="episode" :episodeButton="true" />
:nextEpisodeButton="true" />
</div> </div>
<!-- Loading and Error States --> <!-- Loading and Error States -->
@ -28,10 +27,10 @@ import { Player } from '~/components/ui/player'
import { video, type KodikVideoLinks } from '~/openapi/extractor' import { video, type KodikVideoLinks } from '~/openapi/extractor'
const route = useRoute() const route = useRoute()
const mediaType = route.query.mediaType const mediaType = computed(() => route.query.mediaType as string)
const mediaId = route.query.mediaId const mediaId = computed(() => route.query.mediaId as string)
const mediaHash = route.query.mediaHash const mediaHash = computed(() => route.query.mediaHash as string)
const episode = route.query.episode const episode = computed(() => Number(route.query.episode as string))
const results = ref<KodikVideoLinks | null>(null) const results = ref<KodikVideoLinks | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
@ -39,17 +38,15 @@ const error = ref<unknown>(null)
const hlsUrls = ref<any>(null) const hlsUrls = ref<any>(null)
watchEffect(async () => { watchEffect(async () => {
if (!mediaType || !mediaId || !mediaHash || !episode) return
try { try {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
const videoParams: any = { const videoParams: any = {
mediaType: mediaType as string, mediaType: mediaType.value,
mediaId: mediaId as string, mediaId: mediaId.value,
mediaHash: mediaHash as string, mediaHash: mediaHash.value,
episode: Number(episode as string), episode: episode.value,
quality: '360', quality: '360',
} }
@ -57,11 +54,13 @@ watchEffect(async () => {
results.value = response?.data || null results.value = response?.data || null
if (results.value?.links) { if (results.value?.links) {
hlsUrls.value = Object.entries(results.value.links).map(([quality, links], index) => { console.log(results.value.links)
hlsUrls.value = Object.entries(results.value.links).map(([quality, links], index, arr) => {
const isLatest = index === arr.length - 1
return { return {
html: quality, html: quality,
url: links.find(link => link.src?.includes('.m3u8') || link.type?.includes('hls'))?.src, url: links.find(link => link.src?.includes('.m3u8') || link.type?.includes('hls'))?.src,
default: index === 0 default: isLatest
} }
}) })

View File

@ -29,6 +29,7 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/heroicons": "^1.2.2", "@iconify-json/heroicons": "^1.2.2",
"@iconify-json/mage": "^1.2.4",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"orval": "^7.10.0", "orval": "^7.10.0",
}, },
@ -227,6 +228,8 @@
"@iconify-json/heroicons": ["@iconify-json/heroicons@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qoW4pXr5kTTL6juEjgTs83OJIwpePu7q1tdtKVEdj+i0zyyVHgg/dd9grsXJQnpTpBt6/VwNjrXBvFjRsKPENg=="], "@iconify-json/heroicons": ["@iconify-json/heroicons@1.2.2", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-qoW4pXr5kTTL6juEjgTs83OJIwpePu7q1tdtKVEdj+i0zyyVHgg/dd9grsXJQnpTpBt6/VwNjrXBvFjRsKPENg=="],
"@iconify-json/mage": ["@iconify-json/mage@1.2.4", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-QZnjHtV/RP9/bF6n7bHSnXMPX/MW5dpHqOH2pEHiEytUx76OKrUZ55YBFwrAxPz5f3BoCeWMY1wyEQQD36Bkkw=="],
"@iconify/collections": ["@iconify/collections@1.0.569", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-PclOVcAlvv55Fv5kRJmxk/KMoFLNBMLh0q9LDMlonIPJMUu958VsNw7F7CVurfyEbCf/54i7eF+q6LHqJxeQvg=="], "@iconify/collections": ["@iconify/collections@1.0.569", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-PclOVcAlvv55Fv5kRJmxk/KMoFLNBMLh0q9LDMlonIPJMUu958VsNw7F7CVurfyEbCf/54i7eF+q6LHqJxeQvg=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],

View File

@ -4,7 +4,8 @@ services:
ports: ports:
- 3000:3000 - 3000:3000
networks: networks:
- anyame - anyame-shared
networks: networks:
anyame: anyame-shared:
driver: bridge external: true

View File

@ -1,5 +1,5 @@
// @ts-check // @ts-check
import withNuxt from '.nuxt/eslint.config.mjs' import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt( export default withNuxt(
// Your custom configs here // Your custom configs here

View File

@ -35,6 +35,7 @@
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/heroicons": "^1.2.2", "@iconify-json/heroicons": "^1.2.2",
"@iconify-json/mage": "^1.2.4",
"@nuxtjs/color-mode": "^3.5.2", "@nuxtjs/color-mode": "^3.5.2",
"orval": "^7.10.0" "orval": "^7.10.0"
} }