|
|
@@ -1,27 +1,68 @@
|
|
|
<template>
|
|
|
<div class="bg-neutral-900 w-full h-full relative">
|
|
|
<video
|
|
|
+ v-show="playUrl"
|
|
|
ref="video"
|
|
|
playsinline
|
|
|
- src="https://zm-shorts.oss-cn-hangzhou.aliyuncs.com/uploads/cltreekiv0003lr8i0p93a5h5.mp4"
|
|
|
- poster="https://zm-shorts.oss-cn-hangzhou.aliyuncs.com/uploads/cltreg5k20005lr8i8obwfy11.png"
|
|
|
+ :src="playUrl"
|
|
|
+ :poster="series?.cover"
|
|
|
:controls="false"
|
|
|
class="object-contain w-full h-full"
|
|
|
@timeupdate="onTimeUpdate"
|
|
|
+ @play="() => (userPaused = false)"
|
|
|
+ @pause="() => (userPaused = true)"
|
|
|
></video>
|
|
|
+ <img
|
|
|
+ v-show="!playUrl && series"
|
|
|
+ :src="series?.cover"
|
|
|
+ class="object-contain w-full h-full"
|
|
|
+ />
|
|
|
+
|
|
|
<div
|
|
|
v-show="!draggingData.dragging"
|
|
|
class="mask-info absolute w-full h-full top-0 left-0"
|
|
|
>
|
|
|
+ <IonSpinner
|
|
|
+ v-if="!series || !episode"
|
|
|
+ name="crescent"
|
|
|
+ class="absolute left-0 top-0 right-0 bottom-0 m-auto text-lg"
|
|
|
+ />
|
|
|
+ <IonIcon
|
|
|
+ v-if="showPlayBtn"
|
|
|
+ :icon="playCircle"
|
|
|
+ class="absolute left-0 top-0 right-0 bottom-0 m-auto text-6xl opacity-80"
|
|
|
+ @click="onPlay"
|
|
|
+ />
|
|
|
+ <IonIcon
|
|
|
+ v-if="showPauseBtn"
|
|
|
+ :icon="pauseCircle"
|
|
|
+ class="absolute left-0 top-0 right-0 bottom-0 m-auto text-6xl opacity-80"
|
|
|
+ />
|
|
|
+ <div class="tool-bar flex">
|
|
|
+ <div class="px-2 flex items-center" @click="router.back()">
|
|
|
+ <IonIcon
|
|
|
+ :icon="chevronBack"
|
|
|
+ class="text-2xl opacity-80 h-10"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="flex-1"></div>
|
|
|
+ <div class="px-4 flex items-center">
|
|
|
+ <IonIcon
|
|
|
+ :icon="ellipsisHorizontal"
|
|
|
+ class="text-2xl opacity-80 h-10"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<div
|
|
|
class="title-info absolute bottom-0 left-0 right-0 drop-shadow px-4"
|
|
|
+ v-show="series"
|
|
|
>
|
|
|
<div class="pr-20">
|
|
|
<div class="text-base line-clamp-1">
|
|
|
- Never gonna give you up
|
|
|
+ {{ series?.title }}
|
|
|
</div>
|
|
|
<div class="text-sm text-opacity-80 text-white">
|
|
|
- Episode 1
|
|
|
+ Episode {{ episode?.episodeNum }}
|
|
|
</div>
|
|
|
</div>
|
|
|
<div ref="dragTarget" class="py-3">
|
|
|
@@ -42,22 +83,23 @@
|
|
|
</div>
|
|
|
<div
|
|
|
class="dive-into h-[44px] flex items-center text-xs bg-opacity-20 bg-black px-4 rounded"
|
|
|
- @click.stop="showEpisodes"
|
|
|
+ @click.stop="showEpisodesModal = true"
|
|
|
>
|
|
|
- <div class="flex-1">Dive into the story · 66 Episodes</div>
|
|
|
- <img
|
|
|
- src="@/assets/icon_into_small.svg"
|
|
|
- style="width: 10px; height: auto"
|
|
|
- />
|
|
|
+ <IonIcon :icon="filter" class="text-base" />
|
|
|
+ <div class="flex-1 ml-2 text-white text-opacity-80">
|
|
|
+ {{ series?.totalEpisodes }} Episodes
|
|
|
+ </div>
|
|
|
+ <IonIcon :icon="chevronForward" class="text-base" />
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="absolute right-4 bottom-32 flex flex-col">
|
|
|
+
|
|
|
+ <div class="absolute right-4 bottom-32 flex flex-col" v-if="series">
|
|
|
<div class="btn flex flex-col items-center justify-center">
|
|
|
- <img class="w-8 h-8" src="@/assets/icon_save.svg" />
|
|
|
+ <IonIcon :icon="bookmark" class="text-4xl opacity-80" />
|
|
|
<div class="text-xs">Save</div>
|
|
|
</div>
|
|
|
<div class="btn flex flex-col items-center justify-center mt-4">
|
|
|
- <img class="w-8 h-8" src="@/assets/icon_share.svg" />
|
|
|
+ <IonIcon :icon="arrowRedo" class="text-4xl opacity-80" />
|
|
|
<div class="text-xs">Share</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -71,11 +113,13 @@
|
|
|
>
|
|
|
<span
|
|
|
class="inline-block text-center w-16 text-white text-opacity-90"
|
|
|
- >{{ toDuration }}</span
|
|
|
- >/
|
|
|
- <span class="inline-block text-center w-16">
|
|
|
- {{ formatDuration(draggingData.videoDuration) }}</span
|
|
|
>
|
|
|
+ {{ toDuration }}
|
|
|
+ </span>
|
|
|
+ /
|
|
|
+ <span class="inline-block text-center w-16">
|
|
|
+ {{ formatDuration(draggingData.videoDuration) }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
<div class="progress-bar-large h-2 bg-white bg-opacity-20 rounded">
|
|
|
<div
|
|
|
@@ -85,23 +129,153 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <ion-modal
|
|
|
+ ref="modal"
|
|
|
+ class="episodes-modal"
|
|
|
+ :is-open="showEpisodesModal"
|
|
|
+ :initial-breakpoint="1"
|
|
|
+ :breakpoints="[0, 1]"
|
|
|
+ @ionModalDidDismiss="showEpisodesModal = false"
|
|
|
+ >
|
|
|
+ <ion-content>
|
|
|
+ <div class="flex flex-col h-full">
|
|
|
+ <div class="text-lg mt-4 mx-4">{{ series?.title }}</div>
|
|
|
+ <div class="text-sm mt-1 mx-4 text-white text-opacity-80">
|
|
|
+ {{ series?.totalEpisodes }} Episodes
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex flex-wrap overflow-auto mt-4 ml-4 mr-1">
|
|
|
+ <div
|
|
|
+ class="pr-3 w-1/4 mb-3"
|
|
|
+ v-for="(item, n) in episodes || []"
|
|
|
+ :key="item.id"
|
|
|
+ @click="chooseEpisode(n)"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="episode-btn h-10 bg-neutral-800 rounded flex items-center justify-center relative"
|
|
|
+ :class="{ active: item.id === episode?.id }"
|
|
|
+ >
|
|
|
+ <IonIcon
|
|
|
+ v-if="parseInt(item.price) > 0"
|
|
|
+ :icon="lockClosed"
|
|
|
+ class="absolute top-1 right-1 text-sm"
|
|
|
+ />
|
|
|
+ {{ item.episodeNum }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </ion-content>
|
|
|
+ </ion-modal>
|
|
|
+
|
|
|
+ <ion-modal
|
|
|
+ ref="modal"
|
|
|
+ class="pay-modal"
|
|
|
+ :is-open="showPayModal"
|
|
|
+ :initial-breakpoint="1"
|
|
|
+ :breakpoints="[0, 1]"
|
|
|
+ @ionModalDidDismiss="showPayModal = false"
|
|
|
+ >
|
|
|
+ <ion-content>
|
|
|
+ <div class="divide-y divide-neutral-600 text-white">
|
|
|
+ <div class="mb-6">
|
|
|
+ <div
|
|
|
+ class="mt-6 text-center text-sm text-prim text-opacity-80"
|
|
|
+ >
|
|
|
+ You haven't unlocked this episode yet
|
|
|
+ </div>
|
|
|
+ <div class="text-center text-xl mt-2 font-bold">
|
|
|
+ Join the Membership
|
|
|
+ </div>
|
|
|
+ <div class="text-center text-sm mt-1 px-4">
|
|
|
+ unlock and watch all our videos without any limit
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex space-x-4 pt-6 px-4">
|
|
|
+ <div
|
|
|
+ v-for="(item, n) in plans"
|
|
|
+ :key="n"
|
|
|
+ class="h-44 bg-neutral-700 flex-1 rounded-lg flex flex-col items-center border-neutral-500 border-2 overflow-hidden relative"
|
|
|
+ :class="{ '!border-prim': selectedPlan === n }"
|
|
|
+ @click="selectedPlan = n"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="absolute top-0 left-0 bg-prim rounded-br-lg text-xs px-2 py-1"
|
|
|
+ v-if="n === 0"
|
|
|
+ >
|
|
|
+ Exclusive
|
|
|
+ </div>
|
|
|
+ <div class="mt-8 text-xl font-bold flex-1">
|
|
|
+ {{ item.title }}
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <div
|
|
|
+ class="bg-prim bg-opacity-30 rounded-full px-3 py-1 text-opacity-50 text-sm"
|
|
|
+ >
|
|
|
+ {{ item.desc }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ class="h-8 bg-neutral-500 self-stretch text-base font-bold flex items-center justify-center"
|
|
|
+ :class="{ 'bg-prim': selectedPlan === n }"
|
|
|
+ >
|
|
|
+ <ion-label color=""> ${{ item.price }}</ion-label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <IonButton
|
|
|
+ color="tertiary"
|
|
|
+ expand="block"
|
|
|
+ class="mx-4 mt-8 font-bold"
|
|
|
+ >
|
|
|
+ ${{ plans[selectedPlan].price }} Pay Now
|
|
|
+ </IonButton>
|
|
|
+ </ion-content>
|
|
|
+ </ion-modal>
|
|
|
</template>
|
|
|
<script setup lang="ts">
|
|
|
-import { Ref, computed, onMounted, reactive, ref, watch } from "vue";
|
|
|
+import { Ref, computed, nextTick, onMounted, reactive, ref, watch } from "vue";
|
|
|
+import {
|
|
|
+ IonIcon,
|
|
|
+ IonSpinner,
|
|
|
+ useIonRouter,
|
|
|
+ IonModal,
|
|
|
+ IonContent,
|
|
|
+ IonLabel,
|
|
|
+ IonButton,
|
|
|
+} from "@ionic/vue";
|
|
|
import { useDrag } from "@vueuse/gesture";
|
|
|
-import { useElementBounding } from "@vueuse/core";
|
|
|
+import { useElementBounding, useMediaControls } from "@vueuse/core";
|
|
|
+import {
|
|
|
+ chevronForward,
|
|
|
+ chevronBack,
|
|
|
+ arrowRedo,
|
|
|
+ bookmark,
|
|
|
+ ellipsisHorizontal,
|
|
|
+ playCircle,
|
|
|
+ pauseCircle,
|
|
|
+ filter,
|
|
|
+ lockClosed,
|
|
|
+} from "ionicons/icons";
|
|
|
+import http from "@/plugins/http";
|
|
|
|
|
|
const props = defineProps({
|
|
|
active: Boolean,
|
|
|
+ series: Object,
|
|
|
+ episode: Object,
|
|
|
+ episodes: Array<any>,
|
|
|
});
|
|
|
-
|
|
|
+const emit = defineEmits(["chooseEpisode"]);
|
|
|
+const router = useIonRouter();
|
|
|
const video: Ref<HTMLVideoElement | null> = ref(null);
|
|
|
-const progress = ref(20);
|
|
|
+const { playing, currentTime, duration, volume } = useMediaControls(video);
|
|
|
+const userPaused = ref(false);
|
|
|
+const showPauseBtn = ref(false);
|
|
|
+const progress = ref(0);
|
|
|
const loading = ref(false);
|
|
|
const dragTarget = ref();
|
|
|
-const isDragging = ref(false);
|
|
|
const progressBar: Ref<HTMLElement | null> = ref(null);
|
|
|
-
|
|
|
const draggingData = reactive({
|
|
|
barWidth: 0,
|
|
|
videoDuration: 0,
|
|
|
@@ -109,6 +283,29 @@ const draggingData = reactive({
|
|
|
toProgress: 0,
|
|
|
dragging: false,
|
|
|
});
|
|
|
+const playUrl = ref("");
|
|
|
+const needPay = ref(false);
|
|
|
+const showEpisodesModal = ref(false);
|
|
|
+const showPayModal = ref(false);
|
|
|
+const plans = [
|
|
|
+ {
|
|
|
+ title: "7 Days",
|
|
|
+ desc: "Unlimited",
|
|
|
+ price: 0.99,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "1 Month",
|
|
|
+ desc: "Unlimited",
|
|
|
+ price: 19.99,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "3 Months",
|
|
|
+ desc: "Unlimited",
|
|
|
+ price: 49.99,
|
|
|
+ },
|
|
|
+];
|
|
|
+const selectedPlan = ref(0);
|
|
|
+
|
|
|
function formatDuration(duration: number) {
|
|
|
const minutes = Math.floor(duration / 60)
|
|
|
.toString()
|
|
|
@@ -124,7 +321,6 @@ const toDuration = computed(() => {
|
|
|
);
|
|
|
});
|
|
|
const dragHandler = ({ movement: [x, y], dragging }: any) => {
|
|
|
- console.log(x, dragging);
|
|
|
if (dragging && !draggingData.dragging) {
|
|
|
draggingData.barWidth = useElementBounding(progressBar).width.value;
|
|
|
draggingData.videoDuration = 100;
|
|
|
@@ -144,35 +340,88 @@ const dragHandler = ({ movement: [x, y], dragging }: any) => {
|
|
|
draggingData.dragging = false;
|
|
|
}
|
|
|
};
|
|
|
-useDrag(dragHandler, {
|
|
|
- domTarget: dragTarget,
|
|
|
-});
|
|
|
|
|
|
function onTimeUpdate() {
|
|
|
- progress.value = (video.value!.currentTime! / video.value!.duration!) * 100;
|
|
|
+ if (video.value) {
|
|
|
+ progress.value =
|
|
|
+ (video.value!.currentTime! / video.value!.duration!) * 100;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-onMounted(() => {
|
|
|
- if (props.active) {
|
|
|
- video.value?.play();
|
|
|
+function getEpisode() {
|
|
|
+ if (!props.episode) return;
|
|
|
+ loading.value = true;
|
|
|
+ try {
|
|
|
+ http.get(`/episodes/${props.episode?.id}`).then((res) => {
|
|
|
+ if (res.playUrl) {
|
|
|
+ playUrl.value = res.playUrl;
|
|
|
+ } else {
|
|
|
+ needPay.value = true;
|
|
|
+ }
|
|
|
+ if (props.active) {
|
|
|
+ nextTick(() => {
|
|
|
+ onPlay();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
}
|
|
|
+ loading.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getEpisode();
|
|
|
+ useDrag(dragHandler, {
|
|
|
+ domTarget: dragTarget,
|
|
|
+ });
|
|
|
});
|
|
|
+watch(
|
|
|
+ () => props.episode,
|
|
|
+ () => {
|
|
|
+ getEpisode();
|
|
|
+ }
|
|
|
+);
|
|
|
watch(
|
|
|
() => props.active,
|
|
|
(active) => {
|
|
|
if (active) {
|
|
|
- video.value?.play();
|
|
|
+ onPlay();
|
|
|
} else {
|
|
|
- video.value?.pause();
|
|
|
+ playing.value = false;
|
|
|
}
|
|
|
}
|
|
|
);
|
|
|
-function showEpisodes() {
|
|
|
- console.log("showEpisodes");
|
|
|
+
|
|
|
+const showPlayBtn = computed(() => {
|
|
|
+ if (!props.series || loading.value) return false;
|
|
|
+ if (!playUrl.value) return true;
|
|
|
+ if (userPaused.value) return true;
|
|
|
+ if (!playing.value) return true;
|
|
|
+ return false;
|
|
|
+});
|
|
|
+
|
|
|
+function onPlay() {
|
|
|
+ console.log("onPlay", playUrl.value);
|
|
|
+ if (playUrl.value) {
|
|
|
+ playing.value = true;
|
|
|
+ } else if (needPay.value) {
|
|
|
+ showPayModal.value = true;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function chooseEpisode(n: number) {
|
|
|
+ showEpisodesModal.value = false;
|
|
|
+ setTimeout(() => {
|
|
|
+ emit("chooseEpisode", n);
|
|
|
+ }, 100);
|
|
|
}
|
|
|
</script>
|
|
|
<style lang="less" scoped>
|
|
|
.mask-info {
|
|
|
+ .tool-bar {
|
|
|
+ padding-top: var(--ion-safe-area-top);
|
|
|
+ }
|
|
|
.title-info {
|
|
|
margin-bottom: 16px;
|
|
|
margin-bottom: env(safe-area-inset-bottom, 16px);
|
|
|
@@ -211,4 +460,14 @@ function showEpisodes() {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+ion-modal {
|
|
|
+ --height: 50vh;
|
|
|
+}
|
|
|
+.episode-btn.active {
|
|
|
+ background: var(--ion-color-tertiary);
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+.pay-modal {
|
|
|
+ --height: 460px;
|
|
|
+}
|
|
|
</style>
|