Compare commits
9 Commits
8d21620295
...
feature/ne
Author | SHA1 | Date | |
---|---|---|---|
5dc7aff7ed | |||
32e03ad0f7
|
|||
c6296003af
|
|||
1e604922b9 | |||
030fde7348 | |||
c2316a672d
|
|||
2f735587c3 | |||
52db5633c8
|
|||
ece7c650f6
|
@ -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">
|
||||||
|
@ -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',
|
||||||
|
37
app/components/ui/player/ChangeEpisodeButton.vue
Normal file
37
app/components/ui/player/ChangeEpisodeButton.vue
Normal 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>
|
@ -1,10 +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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -19,7 +22,6 @@ const options = computed(() => {
|
|||||||
customType: {
|
customType: {
|
||||||
m3u8: playM3u8,
|
m3u8: playM3u8,
|
||||||
},
|
},
|
||||||
quality: props.urls,
|
|
||||||
autoSize: true,
|
autoSize: true,
|
||||||
autoMini: true,
|
autoMini: true,
|
||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
@ -29,7 +31,6 @@ const options = computed(() => {
|
|||||||
lock: true,
|
lock: true,
|
||||||
autoOrientation: true,
|
autoOrientation: true,
|
||||||
autoPlayback: true,
|
autoPlayback: true,
|
||||||
id: props.id,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const artplayerRef = ref();
|
const artplayerRef = ref();
|
||||||
@ -55,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')
|
||||||
})
|
})
|
||||||
@ -65,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>
|
||||||
|
42
app/components/util/player-control.js
Normal file
42
app/components/util/player-control.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
app/components/util/route.js
Normal file
9
app/components/util/route.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export const updateUrlParameter = async (route, paramName, newValue) => {
|
||||||
|
await navigateTo({
|
||||||
|
path: route.path,
|
||||||
|
query: {
|
||||||
|
...route.query,
|
||||||
|
[paramName]: newValue
|
||||||
|
}
|
||||||
|
}, { replace: true })
|
||||||
|
}
|
@ -8,7 +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" />
|
<Player :urls="hlsUrls" :episode="episode" :episodeButton="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading and Error States -->
|
<!-- Loading and Error States -->
|
||||||
@ -24,49 +24,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { Player } from '~/components/ui/player'
|
import { Player } from '~/components/ui/player'
|
||||||
import { video, type KodikTranslationDTO, type KodikVideoLinks, type VideoParams } 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)
|
||||||
const error = ref<unknown>(null)
|
const error = ref<unknown>(null)
|
||||||
const hlsUrl = ref<string | null>(null)
|
|
||||||
const hlsUrls = ref<any>(null)
|
const hlsUrls = ref<any>(null)
|
||||||
|
|
||||||
const playerOptions = ref({
|
|
||||||
autoplay: false,
|
|
||||||
controls: true,
|
|
||||||
responsive: true,
|
|
||||||
fluid: true,
|
|
||||||
sources: [{
|
|
||||||
src: hlsUrl.value,
|
|
||||||
type: 'application/x-mpegURL'
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
|
|
||||||
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 translationDto: KodikTranslationDTO = {
|
|
||||||
mediaType: mediaType as string,
|
|
||||||
mediaId: mediaId as string,
|
|
||||||
mediaHash: mediaHash as string,
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
3
bun.lock
3
bun.lock
@ -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=="],
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user