mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 18:07:47 +00:00
feat: rebrand site to CineK
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
# KR Phim
|
||||
# CineK
|
||||
|
||||
KR Phim is a Korean movie streaming web app built with Nuxt and Tailwind CSS. It aggregates Korean-only movie data from the OPhim, NguonC, and KKPhim APIs, normalizes the responses on the server, and presents the content with a Netflix-inspired Nuxt-green interface.
|
||||
CineK is a Korean movie streaming web app built with Nuxt and Tailwind CSS. It uses NguonC movie data, normalizes the responses on the server, and presents the content with a blue Netflix-inspired interface.
|
||||
|
||||
## Features
|
||||
|
||||
- Korean-only movie catalogue from OPhim, NguonC, and KKPhim.
|
||||
- Korean-only movie catalogue from NguonC.
|
||||
- Netflix-style home page with a large hero carousel.
|
||||
- Floating movie preview cards on home page hover.
|
||||
- Browse page with search, type filters, and pagination.
|
||||
@@ -36,8 +36,6 @@ server/
|
||||
api/movies/[slug].get.ts Movie detail API endpoint
|
||||
utils/movies.ts API adapters and normalization helpers
|
||||
API_nguonc.md NguonC API notes
|
||||
API_ophim.md OPhim API notes
|
||||
API_kkphim.md KKPhim API notes
|
||||
```
|
||||
|
||||
## Setup
|
||||
@@ -104,7 +102,7 @@ Stop the container:
|
||||
docker compose down
|
||||
```
|
||||
|
||||
The Compose service is named `kr-phim` and maps host port `3002` to container port `3002`.
|
||||
The Compose service is named `cinek` and maps host port `3002` to container port `3002`.
|
||||
|
||||
## Available Routes
|
||||
|
||||
@@ -113,20 +111,16 @@ The Compose service is named `kr-phim` and maps host port `3002` to container po
|
||||
- `/phim?type=series` - Browse Korean series.
|
||||
- `/phim?type=single` - Browse Korean movies.
|
||||
- `/phim?q=keyword` - Search Korean movies.
|
||||
- `/phim/[slug]?source=ophim` - Movie detail page from OPhim.
|
||||
- `/phim/[slug]?source=nguonc` - Movie detail page from NguonC.
|
||||
- `/phim/[slug]?source=kkphim` - Movie detail page from KKPhim.
|
||||
- `/phim/[slug]` - Movie detail page from NguonC.
|
||||
|
||||
## API Notes
|
||||
|
||||
The app does not call external movie APIs directly from the browser. Instead, Nuxt server endpoints request and normalize movie data:
|
||||
|
||||
- OPhim list/detail data is handled in `server/utils/movies.ts`.
|
||||
- NguonC list/detail data is handled in `server/utils/movies.ts`.
|
||||
- KKPhim list/detail data is handled in `server/utils/movies.ts`.
|
||||
- Only Korean movies are kept in the normalized result.
|
||||
|
||||
If one source is unavailable, the list endpoint still returns data from the other sources when possible and includes source status metadata.
|
||||
The public server endpoints use NguonC as the single movie API source.
|
||||
|
||||
## Scripts
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtLoadingIndicator color="#facc15" :height="2" />
|
||||
<NuxtPage />
|
||||
<AppFooter />
|
||||
<ScrollToTop />
|
||||
|
||||
+26
-9
@@ -3,12 +3,12 @@
|
||||
|
||||
@theme {
|
||||
--font-sans: "Be Vietnam Pro", ui-sans-serif, system-ui, sans-serif;
|
||||
--color-nuxt-50: #ecfff7;
|
||||
--color-nuxt-100: #d3ffec;
|
||||
--color-nuxt-300: #7dffc8;
|
||||
--color-nuxt-400: #00dc82;
|
||||
--color-nuxt-500: #00bd71;
|
||||
--color-nuxt-950: #061f17;
|
||||
--color-cinek-50: #fefce8;
|
||||
--color-cinek-100: #fef9c3;
|
||||
--color-cinek-300: #fde047;
|
||||
--color-cinek-400: #facc15;
|
||||
--color-cinek-500: #eab308;
|
||||
--color-cinek-950: #422006;
|
||||
}
|
||||
|
||||
html {
|
||||
@@ -18,8 +18,8 @@ html {
|
||||
body {
|
||||
min-width: 320px;
|
||||
background:
|
||||
radial-gradient(circle at 15% 0%, rgba(0, 220, 130, 0.26), transparent 34rem),
|
||||
linear-gradient(135deg, #061f17 0%, #09231b 46%, #020617 100%);
|
||||
radial-gradient(circle at 15% 0%, rgba(234, 179, 8, 0.22), transparent 34rem),
|
||||
linear-gradient(135deg, #201a05 0%, #17130a 46%, #020617 100%);
|
||||
color: #f8fbff;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,30 @@ input {
|
||||
font-family: "Be Vietnam Pro", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
a[href],
|
||||
[role="button"],
|
||||
input[type="button"]:not(:disabled),
|
||||
input[type="submit"]:not(:disabled),
|
||||
input[type="reset"]:not(:disabled),
|
||||
label[for],
|
||||
select:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
input:disabled,
|
||||
select:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 0.7rem;
|
||||
width: 0.7rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 220, 130, 0.55);
|
||||
background: rgba(234, 179, 8, 0.68);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const linkGroups = [
|
||||
v-for="item in socialLinks"
|
||||
:key="item.label"
|
||||
type="button"
|
||||
class="grid size-12 place-items-center rounded-full bg-emerald-400 text-slate-950 shadow-lg shadow-emerald-950/30 transition hover:-translate-y-0.5 hover:bg-white"
|
||||
class="grid size-12 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-lg shadow-yellow-950/30 transition hover:-translate-y-0.5 hover:bg-white"
|
||||
:aria-label="item.label"
|
||||
>
|
||||
<component :is="item.icon" class="size-5" />
|
||||
@@ -47,7 +47,7 @@ const linkGroups = [
|
||||
v-for="link in group"
|
||||
:key="link"
|
||||
to="/phim"
|
||||
class="block text-slate-100 transition hover:text-emerald-200"
|
||||
class="block text-slate-100 transition hover:text-yellow-200"
|
||||
>
|
||||
{{ link }}
|
||||
</NuxtLink>
|
||||
@@ -55,11 +55,11 @@ const linkGroups = [
|
||||
</div>
|
||||
|
||||
<p class="mt-10 max-w-4xl text-sm leading-7 text-slate-300">
|
||||
KRPHIM - Trang xem phim Hàn Quốc online chất lượng cao miễn phí Vietsub, thuyết minh, lồng tiếng full HD. Kho phim mới cập nhật liên tục từ OPhim, NguồnC và KKPhim với giao diện xanh Nuxt hiện đại.
|
||||
CineK - Trang xem phim Hàn Quốc online chất lượng cao với Vietsub, thuyết minh, lồng tiếng và trải nghiệm xem mượt trên mọi thiết bị. Kho phim bộ, phim lẻ và nội dung mới được cập nhật thường xuyên.
|
||||
</p>
|
||||
|
||||
<p class="mt-7 text-sm text-slate-400">
|
||||
© {{ currentYear }} KRPHIM
|
||||
© {{ currentYear }} CineK
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Bell,
|
||||
Chrome,
|
||||
@@ -183,22 +183,23 @@ watch(user, (currentUser) => {
|
||||
<AppLogo />
|
||||
|
||||
<div class="hidden lg:flex lg:items-center lg:gap-7 lg:text-sm lg:font-semibold lg:text-slate-200">
|
||||
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to" class="shrink-0 transition hover:text-emerald-200"
|
||||
:class="isActive(item.to) ? 'text-emerald-200' : 'text-slate-200'">
|
||||
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to"
|
||||
class="shrink-0 transition hover:text-yellow-200"
|
||||
:class="isActive(item.to) ? 'text-yellow-200' : 'text-slate-200'">
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="ml-auto flex w-full max-w-xs items-center rounded-full border border-white/10 bg-white/8 px-4 py-2 shadow-2xl shadow-emerald-950/20 sm:max-w-sm"
|
||||
class="ml-auto flex w-full max-w-xs items-center rounded-full border border-white/10 bg-white/8 px-4 py-2 shadow-2xl shadow-yellow-950/20 sm:max-w-sm"
|
||||
@submit.prevent="submitSearch">
|
||||
<Search class="mr-3 size-4 shrink-0 text-emerald-200" />
|
||||
<Search class="mr-3 size-4 shrink-0 text-yellow-200" />
|
||||
<input v-model="keyword" type="search" placeholder="Tìm phim Hàn Quốc..."
|
||||
class="w-full bg-transparent text-sm text-white outline-none placeholder:text-slate-400">
|
||||
</form>
|
||||
|
||||
<div class="group/member relative hidden shrink-0 items-center gap-2.5 md:flex" @mouseenter="memberMenuOpen = true"
|
||||
@mouseleave="memberMenuOpen = false">
|
||||
<div class="group/member relative hidden shrink-0 items-center gap-2.5 md:flex"
|
||||
@mouseenter="memberMenuOpen = true" @mouseleave="memberMenuOpen = false">
|
||||
<button v-if="user" type="button"
|
||||
class="grid size-9 cursor-pointer place-items-center rounded-full bg-white/10 text-white transition hover:bg-white/16"
|
||||
aria-label="Thông báo">
|
||||
@@ -206,12 +207,12 @@ watch(user, (currentUser) => {
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="inline-flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full bg-white px-2.5 pl-1 text-xs font-black text-slate-950 shadow-xl shadow-black/20 transition hover:bg-emerald-100"
|
||||
class="inline-flex h-9 shrink-0 cursor-pointer items-center gap-1.5 rounded-full bg-white px-2.5 pl-1 text-xs font-black text-slate-950 shadow-xl shadow-black/20 transition hover:bg-yellow-100"
|
||||
:aria-label="user ? 'Tài khoản thành viên' : 'Đăng nhập thành viên'" @click="handleMemberClick">
|
||||
<Loader2 v-if="authLoading" class="mx-1 size-4 animate-spin" />
|
||||
<template v-else-if="user">
|
||||
<img :src="memberAvatar" :alt="memberName"
|
||||
class="size-7 rounded-full object-cover ring-2 ring-emerald-200/80">
|
||||
class="size-7 rounded-full object-cover ring-2 ring-yellow-200/80">
|
||||
</template>
|
||||
<User v-else class="ml-1 size-4 fill-current" />
|
||||
<span class="max-w-24 truncate">{{ user ? memberName : 'Thành viên' }}</span>
|
||||
@@ -227,7 +228,7 @@ watch(user, (currentUser) => {
|
||||
</NuxtLink>
|
||||
<div class="my-2 border-t border-white/10" />
|
||||
<button type="button"
|
||||
class="flex h-12 w-full cursor-pointer items-center gap-3 px-5 text-left font-black text-emerald-300 transition hover:bg-white/8 hover:text-white"
|
||||
class="flex h-12 w-full cursor-pointer items-center gap-3 px-5 text-left font-black text-yellow-300 transition hover:bg-white/8 hover:text-white"
|
||||
@click="handleSignOut">
|
||||
<LogOut class="size-4 shrink-0" />
|
||||
Đăng xuất
|
||||
@@ -258,17 +259,16 @@ watch(user, (currentUser) => {
|
||||
</div>
|
||||
<nav class="flex flex-col gap-1 p-4">
|
||||
<NuxtLink v-for="item in navItems" :key="item.to" :to="item.to"
|
||||
class="rounded-lg px-4 py-3 text-base font-semibold text-slate-200 transition hover:bg-white/8 hover:text-emerald-200"
|
||||
:class="isActive(item.to) ? 'bg-white/8 text-emerald-200' : ''" @click="closeMobileMenu">
|
||||
class="rounded-lg px-4 py-3 text-base font-semibold text-slate-200 transition hover:bg-white/8 hover:text-yellow-200"
|
||||
:class="isActive(item.to) ? 'bg-white/8 text-yellow-200' : ''" @click="closeMobileMenu">
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
<div class="border-t border-white/10 p-4">
|
||||
<button type="button"
|
||||
class="inline-flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-black text-slate-950 transition hover:bg-emerald-100"
|
||||
class="inline-flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-black text-slate-950 transition hover:bg-yellow-100"
|
||||
@click="handleMemberClick">
|
||||
<img v-if="user" :src="memberAvatar" :alt="memberName"
|
||||
class="size-7 rounded-full object-cover">
|
||||
<img v-if="user" :src="memberAvatar" :alt="memberName" class="size-7 rounded-full object-cover">
|
||||
<User v-else class="size-4 fill-current" />
|
||||
<span>{{ user ? memberName : 'Đăng nhập thành viên' }}</span>
|
||||
</button>
|
||||
@@ -280,7 +280,7 @@ watch(user, (currentUser) => {
|
||||
</NuxtLink>
|
||||
<div class="border-t border-white/10" />
|
||||
<button type="button"
|
||||
class="flex h-11 w-full cursor-pointer items-center gap-3 px-4 text-left text-sm font-black text-emerald-300 transition hover:bg-white/8 hover:text-white"
|
||||
class="flex h-11 w-full cursor-pointer items-center gap-3 px-4 text-left text-sm font-black text-yellow-300 transition hover:bg-white/8 hover:text-white"
|
||||
@click="handleSignOut">
|
||||
<LogOut class="size-4 shrink-0" />
|
||||
Đăng xuất
|
||||
@@ -302,7 +302,7 @@ watch(user, (currentUser) => {
|
||||
<X class="size-5" />
|
||||
</button>
|
||||
|
||||
<div class="auth-poster-panel hidden min-h-[32rem] items-end p-8 md:flex">
|
||||
<div class="auth-poster-panel hidden min-h-128 items-end p-8 md:flex">
|
||||
<div>
|
||||
<AppLogo />
|
||||
</div>
|
||||
@@ -310,11 +310,12 @@ watch(user, (currentUser) => {
|
||||
|
||||
<div class="bg-slate-950 px-5 py-10 sm:px-10 md:px-16 md:py-16">
|
||||
<div v-if="user">
|
||||
<p class="text-sm font-bold text-emerald-300">Thành viên</p>
|
||||
<p class="text-sm font-bold text-yellow-300">Thành viên</p>
|
||||
<h2 class="mt-2 text-2xl font-black">Tài khoản của bạn</h2>
|
||||
<p class="mt-6 rounded-md bg-white/8 px-4 py-3 text-sm font-semibold text-slate-100">{{ user.email }}</p>
|
||||
<p class="mt-6 rounded-md bg-white/8 px-4 py-3 text-sm font-semibold text-slate-100">{{ user.email }}
|
||||
</p>
|
||||
<button type="button"
|
||||
class="mt-5 inline-flex h-12 w-full items-center justify-center gap-2 rounded-md bg-emerald-300 text-sm font-black text-slate-950 transition hover:bg-white"
|
||||
class="mt-5 inline-flex h-12 w-full items-center justify-center gap-2 rounded-md bg-yellow-300 text-sm font-black text-slate-950 transition hover:bg-white"
|
||||
@click="handleSignOut">
|
||||
<LogOut class="size-4" />
|
||||
Đăng xuất
|
||||
@@ -326,14 +327,14 @@ watch(user, (currentUser) => {
|
||||
<p class="mt-5 text-sm text-slate-300">
|
||||
<template v-if="authMode === 'login'">
|
||||
Nếu bạn chưa có tài khoản,
|
||||
<button type="button" class="font-black text-emerald-300 hover:text-white"
|
||||
<button type="button" class="font-black text-yellow-300 hover:text-white"
|
||||
@click="authMode = 'register'; authError = ''; authMessage = ''">
|
||||
đăng ký ngay
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
Nếu bạn đã có tài khoản,
|
||||
<button type="button" class="font-black text-emerald-300 hover:text-white"
|
||||
<button type="button" class="font-black text-yellow-300 hover:text-white"
|
||||
@click="authMode = 'login'; authError = ''; authMessage = ''">
|
||||
đăng nhập ngay
|
||||
</button>
|
||||
@@ -368,7 +369,7 @@ watch(user, (currentUser) => {
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="inline-flex h-12 w-full items-center justify-center gap-2 rounded-md bg-emerald-300 text-sm font-black text-slate-950 transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||
class="inline-flex h-12 w-full items-center justify-center gap-2 rounded-md bg-yellow-300 text-sm font-black text-slate-950 transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:disabled="submittingAuth">
|
||||
<Loader2 v-if="submittingAuth" class="size-4 animate-spin" />
|
||||
<User v-else class="size-4 fill-current" />
|
||||
@@ -376,12 +377,12 @@ watch(user, (currentUser) => {
|
||||
</button>
|
||||
|
||||
<button v-if="authMode === 'login'" type="button"
|
||||
class="mt-7 block w-full text-center text-sm font-bold text-white hover:text-emerald-300"
|
||||
class="mt-7 block w-full text-center text-sm font-bold text-white hover:text-yellow-300"
|
||||
@click="handleResetPassword">
|
||||
Quên mật khẩu?
|
||||
</button>
|
||||
|
||||
<p v-if="authMessage" class="mt-4 rounded-md bg-emerald-400/12 px-3 py-2 text-sm text-emerald-100">
|
||||
<p v-if="authMessage" class="mt-4 rounded-md bg-yellow-400/12 px-3 py-2 text-sm text-yellow-100">
|
||||
{{ authMessage }}
|
||||
</p>
|
||||
<p v-if="authError" class="mt-4 rounded-md bg-red-500/12 px-3 py-2 text-sm text-red-100">
|
||||
@@ -441,9 +442,9 @@ watch(user, (currentUser) => {
|
||||
.auth-poster-panel {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(2, 6, 23, 0.45), rgba(2, 6, 23, 0.92)),
|
||||
linear-gradient(135deg, rgba(0, 220, 130, 0.12) 0 18%, transparent 18% 100%),
|
||||
radial-gradient(circle at 22% 18%, rgba(0, 220, 130, 0.24), transparent 8rem),
|
||||
linear-gradient(120deg, #061f17 0%, #0f172a 54%, #020617 100%);
|
||||
linear-gradient(135deg, rgba(234, 179, 8, 0.14) 0 18%, transparent 18% 100%),
|
||||
radial-gradient(circle at 22% 18%, rgba(250, 204, 21, 0.22), transparent 8rem),
|
||||
linear-gradient(120deg, #422006 0%, #0f172a 54%, #020617 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -451,14 +452,14 @@ watch(user, (currentUser) => {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(115deg, transparent 0 10%, rgba(0, 220, 130, 0.12) 10% 10.6%, transparent 10.6% 100%),
|
||||
linear-gradient(115deg, transparent 0 10%, rgba(234, 179, 8, 0.16) 10% 10.6%, transparent 10.6% 100%),
|
||||
repeating-linear-gradient(105deg, rgba(255, 255, 255, 0.06) 0 7.5rem, transparent 7.5rem 8rem),
|
||||
repeating-linear-gradient(15deg, rgba(0, 220, 130, 0.08) 0 10rem, transparent 10rem 10.6rem);
|
||||
repeating-linear-gradient(15deg, rgba(250, 204, 21, 0.1) 0 10rem, transparent 10rem 10.6rem);
|
||||
content: "";
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.auth-poster-panel > div {
|
||||
.auth-poster-panel>div {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Play } from 'lucide-vue-next'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="group inline-flex h-8 min-w-[100px] shrink-0 flex-row flex-nowrap items-center gap-2 whitespace-nowrap sm:h-10 sm:min-w-[132px] sm:gap-2.5"
|
||||
aria-label="KR PHIM"
|
||||
>
|
||||
<span class="grid size-7 shrink-0 place-items-center rounded-lg border border-emerald-200/80 bg-black/30 text-white shadow-lg shadow-black/20 transition group-hover:border-emerald-300 group-hover:text-emerald-200 sm:size-9">
|
||||
<NuxtLink to="/"
|
||||
class="group inline-flex h-8 min-w-24 shrink-0 flex-row flex-nowrap items-center gap-2 whitespace-nowrap sm:h-10 sm:min-w-29.5 sm:gap-2.5"
|
||||
aria-label="CineK">
|
||||
<span
|
||||
class="grid size-7 shrink-0 place-items-center rounded-lg border border-yellow-200/80 bg-black/30 text-white shadow-lg shadow-black/20 transition group-hover:border-yellow-300 group-hover:text-yellow-200 sm:size-9">
|
||||
<Play class="ml-0.5 size-4 fill-current sm:size-5" />
|
||||
</span>
|
||||
<span class="inline-flex items-baseline text-xl font-black uppercase leading-none tracking-tight sm:text-2xl">
|
||||
<span class="text-emerald-300">KR</span><span class="text-white">PHIM</span>
|
||||
<span class="inline-flex items-baseline text-xl font-black leading-none tracking-normal sm:text-2xl">
|
||||
<span class="text-white">Cine</span><span class="text-yellow-300">K</span>
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Circle, Heart, Play } from 'lucide-vue-next'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -79,15 +79,15 @@ onBeforeUnmount(() => {
|
||||
class="group relative block h-78 w-44 shrink-0 snap-start transition duration-300 hover:z-30 hover:-translate-y-1 sm:h-82 sm:w-52 xl:w-56"
|
||||
@mouseenter="showPreview" @mouseleave="scheduleHide" @focus="showPreview" @blur="scheduleHide">
|
||||
<div
|
||||
class="absolute inset-0 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:ring-emerald-300/60">
|
||||
class="absolute inset-0 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:ring-yellow-300/60">
|
||||
<img :src="movie.thumb || movie.poster" :alt="movie.name"
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div class="absolute inset-x-0 bottom-0 bg-linear-to-t from-slate-950 via-slate-950/70 to-transparent p-3">
|
||||
<p class="line-clamp-2 text-sm font-bold leading-snug text-white">{{ movie.name }}</p>
|
||||
<p class="mt-1 truncate text-xs text-emerald-100">{{ movie.episode || movie.year || movie.quality }}</p>
|
||||
<p class="mt-1 truncate text-xs text-yellow-100">{{ movie.episode || movie.year || movie.quality }}</p>
|
||||
</div>
|
||||
<span v-if="movie.sources?.length && movie.sources.length > 1"
|
||||
class="absolute left-2 top-2 rounded bg-emerald-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
{{ movie.sources.length }} server
|
||||
</span>
|
||||
</div>
|
||||
@@ -98,11 +98,11 @@ onBeforeUnmount(() => {
|
||||
enter-to-class="scale-100 opacity-100" leave-active-class="transition duration-120 ease-in"
|
||||
leave-from-class="scale-100 opacity-100" leave-to-class="scale-95 opacity-0">
|
||||
<NuxtLink v-if="isPreviewVisible" :to="movieLink"
|
||||
class="fixed z-90 hidden origin-top-left overflow-hidden rounded-xl bg-[#071411] text-white shadow-2xl shadow-black/60 ring-1 ring-emerald-300/45 sm:block"
|
||||
class="fixed z-90 hidden origin-top-left overflow-hidden rounded-xl bg-[#07111d] text-white shadow-2xl shadow-black/60 ring-1 ring-yellow-300/45 sm:block"
|
||||
:style="previewStyle" @mouseenter="keepPreview" @mouseleave="scheduleHide">
|
||||
<div class="relative h-44 overflow-hidden">
|
||||
<img :src="movie.poster || movie.thumb" :alt="movie.name" class="h-full w-full object-cover">
|
||||
<div class="absolute inset-0 bg-linear-to-t from-[#071411] via-[#071411]/30 to-transparent" />
|
||||
<div class="absolute inset-0 bg-linear-to-t from-[#07111d] via-[#07111d]/30 to-transparent" />
|
||||
<div class="absolute inset-x-0 bottom-0 p-4">
|
||||
<h3 class="line-clamp-2 text-xl font-black leading-tight text-white">
|
||||
{{ movie.name }}
|
||||
@@ -116,7 +116,7 @@ onBeforeUnmount(() => {
|
||||
<div class="p-4 pt-3">
|
||||
<div class="grid grid-cols-[1fr_6.25rem_6.25rem] gap-2">
|
||||
<span
|
||||
class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-emerald-400 px-4 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
class="inline-flex h-10 items-center justify-center gap-2 rounded-md bg-yellow-400 px-4 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
<Play class="size-4 fill-current" />
|
||||
Xem ngay
|
||||
</span>
|
||||
@@ -131,7 +131,7 @@ onBeforeUnmount(() => {
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs font-black">
|
||||
<span v-if="movie.quality" class="rounded bg-black/28 px-2.5 py-1.5 text-white ring-1 ring-white/10">{{
|
||||
movie.quality }}</span>
|
||||
<span v-if="movie.lang" class="rounded bg-emerald-300 px-2.5 py-1.5 text-slate-950">{{ movie.lang }}</span>
|
||||
<span v-if="movie.lang" class="rounded bg-yellow-300 px-2.5 py-1.5 text-slate-950">{{ movie.lang }}</span>
|
||||
<span v-if="movie.time" class="rounded bg-white px-2.5 py-1.5 text-slate-950">{{ movie.time }}</span>
|
||||
<span v-if="movie.episode" class="rounded bg-black/28 px-2.5 py-1.5 text-white ring-1 ring-white/10">{{
|
||||
movie.episode }}</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ArrowUp } from 'lucide-vue-next'
|
||||
|
||||
const isVisible = ref(false)
|
||||
@@ -33,7 +33,7 @@ onBeforeUnmount(() => {
|
||||
<button
|
||||
v-if="isVisible"
|
||||
type="button"
|
||||
class="fixed bottom-6 right-5 z-50 grid size-12 place-items-center rounded-full bg-emerald-400 text-slate-950 shadow-2xl shadow-emerald-950/40 ring-1 ring-white/20 transition hover:-translate-y-0.5 hover:bg-white sm:bottom-8 sm:right-8"
|
||||
class="fixed bottom-6 right-5 z-50 grid size-12 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-2xl shadow-yellow-950/40 ring-1 ring-white/20 transition hover:-translate-y-0.5 hover:bg-white sm:bottom-8 sm:right-8"
|
||||
aria-label="Lên đầu trang"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
|
||||
@@ -8,8 +8,10 @@ export type LibraryMovieItem = {
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
const favoriteKey = 'kr-phim-favorites'
|
||||
const watchLaterKey = 'kr-phim-watch-later'
|
||||
const favoriteKey = 'cinek-favorites'
|
||||
const watchLaterKey = 'cinek-watch-later'
|
||||
const legacyFavoriteKey = 'kr-phim-favorites'
|
||||
const legacyWatchLaterKey = 'kr-phim-watch-later'
|
||||
|
||||
function normalizeLibraryItem(item: any): LibraryMovieItem | null {
|
||||
if (!item?.slug || !item?.name) return null
|
||||
@@ -27,11 +29,11 @@ function normalizeLibraryItem(item: any): LibraryMovieItem | null {
|
||||
}
|
||||
}
|
||||
|
||||
function readLocalItems(key: string) {
|
||||
function readLocalItems(key: string, legacyKey?: string) {
|
||||
if (!import.meta.client) return []
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(key)
|
||||
const raw = window.localStorage.getItem(key) || (legacyKey ? window.localStorage.getItem(legacyKey) : null)
|
||||
const items = raw ? JSON.parse(raw) : []
|
||||
|
||||
return Array.isArray(items)
|
||||
@@ -47,12 +49,18 @@ function writeLocalItems(key: string, items: LibraryMovieItem[]) {
|
||||
window.localStorage.setItem(key, JSON.stringify(items))
|
||||
}
|
||||
|
||||
function legacyKeyFor(key: string) {
|
||||
if (key === favoriteKey) return legacyFavoriteKey
|
||||
if (key === watchLaterKey) return legacyWatchLaterKey
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function useMovieLibrary() {
|
||||
const { $supabase } = useNuxtApp()
|
||||
const { user } = useSupabaseAuth()
|
||||
|
||||
async function loadItems(key: string, table: string, limit = 30) {
|
||||
const localItems = readLocalItems(key)
|
||||
const localItems = readLocalItems(key, legacyKeyFor(key))
|
||||
|
||||
if (user.value) {
|
||||
const { data, error } = await $supabase
|
||||
@@ -82,7 +90,7 @@ export function useMovieLibrary() {
|
||||
|
||||
const nextItems = [
|
||||
normalizedItem,
|
||||
...readLocalItems(key).filter((savedItem) => !(savedItem.slug === normalizedItem.slug && savedItem.source === normalizedItem.source)),
|
||||
...readLocalItems(key, legacyKeyFor(key)).filter((savedItem) => !(savedItem.slug === normalizedItem.slug && savedItem.source === normalizedItem.source)),
|
||||
].slice(0, 50)
|
||||
|
||||
writeLocalItems(key, nextItems)
|
||||
@@ -108,7 +116,7 @@ export function useMovieLibrary() {
|
||||
}
|
||||
|
||||
async function removeItem(key: string, table: string, item: LibraryMovieItem) {
|
||||
const nextItems = readLocalItems(key)
|
||||
const nextItems = readLocalItems(key, legacyKeyFor(key))
|
||||
.filter((savedItem) => !(savedItem.slug === item.slug && savedItem.source === item.source))
|
||||
writeLocalItems(key, nextItems)
|
||||
|
||||
@@ -123,7 +131,7 @@ export function useMovieLibrary() {
|
||||
}
|
||||
|
||||
async function isSaved(key: string, table: string, item: LibraryMovieItem) {
|
||||
const localSaved = readLocalItems(key)
|
||||
const localSaved = readLocalItems(key, legacyKeyFor(key))
|
||||
.some((savedItem) => savedItem.slug === item.slug && savedItem.source === item.source)
|
||||
|
||||
if (!user.value) return localSaved
|
||||
|
||||
@@ -13,7 +13,8 @@ export type WatchHistoryItem = {
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
const watchHistoryKey = 'kr-phim-watch-history'
|
||||
const watchHistoryKey = 'cinek-watch-history'
|
||||
const legacyWatchHistoryKey = 'kr-phim-watch-history'
|
||||
|
||||
function normalizeHistoryItem(item: any): WatchHistoryItem | null {
|
||||
const slug = item?.slug
|
||||
@@ -43,7 +44,7 @@ function readLocalHistory() {
|
||||
if (!import.meta.client) return []
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(watchHistoryKey)
|
||||
const raw = window.localStorage.getItem(watchHistoryKey) || window.localStorage.getItem(legacyWatchHistoryKey)
|
||||
const history = raw ? JSON.parse(raw) : []
|
||||
|
||||
return Array.isArray(history)
|
||||
@@ -149,7 +150,10 @@ export function useWatchHistory() {
|
||||
}
|
||||
|
||||
async function clearWatchHistory() {
|
||||
if (import.meta.client) window.localStorage.removeItem(watchHistoryKey)
|
||||
if (import.meta.client) {
|
||||
window.localStorage.removeItem(watchHistoryKey)
|
||||
window.localStorage.removeItem(legacyWatchHistoryKey)
|
||||
}
|
||||
|
||||
if (user.value) {
|
||||
await $supabase
|
||||
|
||||
+196
-160
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { BookPlus, ChevronLeft, ChevronRight, Heart, Info, Play, Trash2 } from 'lucide-vue-next'
|
||||
import { ChevronLeft, ChevronRight, Heart, Info, Play, Trash2 } from 'lucide-vue-next'
|
||||
import type { WatchHistoryItem } from '~/composables/useWatchHistory'
|
||||
|
||||
const { data, pending, error } = await useFetch('/api/movies', {
|
||||
const { data, pending, error } = useFetch('/api/movies', {
|
||||
query: {
|
||||
page: 1,
|
||||
},
|
||||
lazy: true,
|
||||
default: () => ({
|
||||
items: [],
|
||||
sources: [],
|
||||
}),
|
||||
})
|
||||
|
||||
const movies = computed(() => data.value?.items ?? [])
|
||||
@@ -36,6 +41,25 @@ const heroDescription = computed(() => {
|
||||
return fallback.length ? fallback.join('. ') : 'Phim Hàn Quốc Vietsub mới cập nhật.'
|
||||
})
|
||||
|
||||
const rows = computed(() => [
|
||||
{
|
||||
title: 'Mới cập nhật',
|
||||
to: '/phim',
|
||||
items: movies.value.slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'Phim bộ Hàn Quốc',
|
||||
to: '/phim?type=series',
|
||||
items: movies.value.filter((movie: any) => movie.type !== 'single').slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'Phim lẻ & đặc biệt',
|
||||
to: '/phim?type=single',
|
||||
items: movies.value.filter((movie: any) => movie.type === 'single' || movie.episode === 'Full').slice(0, 12),
|
||||
},
|
||||
])
|
||||
const topTrending = computed(() => uniqueHeroMovies(movies.value).slice(0, 10))
|
||||
|
||||
function comparableMovieText(value?: string) {
|
||||
return String(value || '')
|
||||
.normalize('NFD')
|
||||
@@ -96,65 +120,6 @@ function previousHero() {
|
||||
heroIndex.value = (heroIndex.value - 1 + heroSlides.value.length) % heroSlides.value.length
|
||||
}
|
||||
|
||||
watch(heroSlides, (slides) => {
|
||||
if (heroIndex.value >= slides.length) {
|
||||
heroIndex.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
watch(hero, async (currentHero) => {
|
||||
if (!currentHero?.slug) {
|
||||
heroDetail.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const cacheKey = `${currentHero.source}:${currentHero.slug}`
|
||||
if (heroDetailCache.has(cacheKey)) {
|
||||
heroDetail.value = heroDetailCache.get(cacheKey)
|
||||
return
|
||||
}
|
||||
|
||||
heroDetail.value = null
|
||||
try {
|
||||
const detail = await $fetch(`/api/movies/${currentHero.slug}`, {
|
||||
query: {
|
||||
source: currentHero.source,
|
||||
srcs: movieSourcesQuery(currentHero) || undefined,
|
||||
},
|
||||
})
|
||||
heroDetailCache.set(cacheKey, detail)
|
||||
|
||||
if (hero.value?.slug === currentHero.slug && hero.value?.source === currentHero.source) {
|
||||
heroDetail.value = detail
|
||||
}
|
||||
} catch {
|
||||
heroDetail.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
initAuth()
|
||||
heroTimer = setInterval(nextHero, 6500)
|
||||
loadWatchHistory()
|
||||
window.addEventListener('storage', loadWatchHistory)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (heroTimer) clearInterval(heroTimer)
|
||||
if (import.meta.client) window.removeEventListener('storage', loadWatchHistory)
|
||||
})
|
||||
|
||||
async function loadWatchHistory() {
|
||||
if (!import.meta.client) return
|
||||
watchHistory.value = await fetchWatchHistory(12)
|
||||
}
|
||||
|
||||
async function clearWatchHistory() {
|
||||
if (!import.meta.client) return
|
||||
await removeWatchHistory()
|
||||
watchHistory.value = []
|
||||
}
|
||||
|
||||
function watchHistoryLink(item: WatchHistoryItem) {
|
||||
return {
|
||||
path: `/xem/${item.slug}`,
|
||||
@@ -196,128 +161,156 @@ function watchProgressLabel(item: WatchHistoryItem) {
|
||||
return item.episodeName || `Tập ${(item.episodeIndex || 0) + 1}`
|
||||
}
|
||||
|
||||
const rows = computed(() => [
|
||||
{
|
||||
title: 'Mới cập nhật',
|
||||
to: '/phim',
|
||||
items: movies.value.slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'Phim bộ Hàn Quốc',
|
||||
to: '/phim?type=series',
|
||||
items: movies.value.filter((movie: any) => movie.type !== 'single').slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'Phim lẻ & đặc biệt',
|
||||
to: '/phim?type=single',
|
||||
items: movies.value.filter((movie: any) => movie.type === 'single' || movie.episode === 'Full').slice(0, 12),
|
||||
},
|
||||
])
|
||||
async function loadWatchHistory() {
|
||||
if (!import.meta.client) return
|
||||
watchHistory.value = await fetchWatchHistory(12)
|
||||
}
|
||||
|
||||
const apiRows = computed(() => [
|
||||
{
|
||||
title: 'Mới cập nhật từ tất cả nguồn',
|
||||
to: '/phim',
|
||||
items: movies.value.slice(0, 12),
|
||||
async function clearWatchHistory() {
|
||||
if (!import.meta.client) return
|
||||
await removeWatchHistory()
|
||||
watchHistory.value = []
|
||||
}
|
||||
|
||||
watch(heroSlides, (slides) => {
|
||||
if (heroIndex.value >= slides.length) {
|
||||
heroIndex.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
watch(hero, async (currentHero) => {
|
||||
if (!currentHero?.slug) {
|
||||
heroDetail.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const cacheKey = `${currentHero.source}:${currentHero.slug}`
|
||||
if (heroDetailCache.has(cacheKey)) {
|
||||
heroDetail.value = heroDetailCache.get(cacheKey)
|
||||
return
|
||||
}
|
||||
|
||||
heroDetail.value = null
|
||||
try {
|
||||
const detail = await $fetch(`/api/movies/${currentHero.slug}`, {
|
||||
query: {
|
||||
source: currentHero.source,
|
||||
srcs: movieSourcesQuery(currentHero) || undefined,
|
||||
},
|
||||
{
|
||||
title: 'OPhim',
|
||||
to: '/phim?source=ophim',
|
||||
items: movies.value.filter((movie: any) => movie.source === 'ophim').slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'NguonC',
|
||||
to: '/phim?source=nguonc',
|
||||
items: movies.value.filter((movie: any) => movie.source === 'nguonc').slice(0, 12),
|
||||
},
|
||||
{
|
||||
title: 'KKPhim',
|
||||
to: '/phim?source=kkphim',
|
||||
items: movies.value.filter((movie: any) => movie.source === 'kkphim').slice(0, 12),
|
||||
},
|
||||
])
|
||||
})
|
||||
heroDetailCache.set(cacheKey, detail)
|
||||
|
||||
if (hero.value?.slug === currentHero.slug && hero.value?.source === currentHero.source) {
|
||||
heroDetail.value = detail
|
||||
}
|
||||
} catch {
|
||||
heroDetail.value = null
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(user, () => {
|
||||
loadWatchHistory()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initAuth()
|
||||
heroTimer = setInterval(nextHero, 6500)
|
||||
loadWatchHistory()
|
||||
window.addEventListener('storage', loadWatchHistory)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (heroTimer) clearInterval(heroTimer)
|
||||
if (import.meta.client) window.removeEventListener('storage', loadWatchHistory)
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'KR Phim - Phim Hàn Quốc',
|
||||
title: 'CineK - Xem phim Hàn Quốc online',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Xem phim Hàn Quốc Vietsub từ OPhim, NguồnC và KKPhim với giao diện xanh Nuxt hiện đại.',
|
||||
content: 'Xem phim Hàn Quốc online tại CineK với kho phim bộ, phim lẻ, show mới cập nhật, hỗ trợ Vietsub, thuyết minh và lồng tiếng.',
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'CineK - Xem phim Hàn Quốc online',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: 'Kho phim Hàn Quốc online mới cập nhật, trải nghiệm xem mượt trên điện thoại, máy tính bảng và desktop.',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="min-h-screen overflow-hidden">
|
||||
<main class="min-h-screen overflow-hidden bg-black">
|
||||
<AppHeader />
|
||||
|
||||
<section v-if="hero"
|
||||
class="relative h-130 min-h-130 overflow-hidden pt-20 sm:h-160 sm:min-h-160 sm:pt-24 lg:h-190 lg:min-h-190 lg:pt-16">
|
||||
class="relative h-155 min-h-155 overflow-hidden pt-20 sm:h-170 sm:min-h-170 sm:pt-24 lg:h-180 lg:min-h-190 lg:pt-0">
|
||||
<TransitionGroup name="hero-fade">
|
||||
<img v-for="(slide, index) in heroSlides" v-show="index === heroIndex" :key="`${slide.source}-${slide.slug}`"
|
||||
:src="slide.poster || slide.thumb" :alt="slide.name" class="absolute inset-0 h-full w-full object-cover">
|
||||
:src="slide.poster || slide.thumb" :alt="slide.name"
|
||||
class="absolute inset-0 h-full w-full object-cover object-center opacity-72 lg:object-[72%_center]">
|
||||
</TransitionGroup>
|
||||
<div class="absolute inset-0 bg-linear-to-r from-slate-950 via-slate-950/76 to-slate-950/10" />
|
||||
<div class="absolute inset-0 bg-linear-to-t from-slate-950 via-slate-950/20 to-slate-950/30" />
|
||||
<div class="absolute inset-y-0 right-0 hidden w-1/2 bg-linear-to-l from-emerald-950/18 to-transparent lg:block" />
|
||||
|
||||
<div class="absolute inset-0 bg-linear-to-r from-black via-black/76 to-black/14" />
|
||||
<div class="absolute inset-0 bg-linear-to-t from-black via-black/16 to-black/34" />
|
||||
|
||||
<div
|
||||
class="relative mx-auto grid h-full max-w-390 content-center px-4 pb-20 pt-12 sm:px-6 sm:pb-24 sm:pt-16 lg:px-8 lg:pb-28 xl:px-10">
|
||||
<div class="max-w-5xl">
|
||||
<p class="mb-2 text-xs font-semibold text-emerald-200 sm:mb-3 sm:text-sm">
|
||||
Chỉ phim Hàn Quốc
|
||||
</p>
|
||||
<h1 class="max-w-5xl text-2xl font-black leading-tight text-white drop-shadow-2xl sm:text-4xl lg:text-6xl">
|
||||
class="relative mx-auto h-full max-w-390 px-4 pb-22 pt-20 sm:px-6 sm:pb-26 sm:pt-14 lg:px-8 lg:pb-0 lg:pt-38 xl:px-10">
|
||||
<div class="max-w-140">
|
||||
<h1 class="text-4xl font-black leading-tight text-white drop-shadow-2xl sm:text-5xl lg:text-[50px]">
|
||||
{{ hero.name }}
|
||||
</h1>
|
||||
<p class="mt-2 max-w-2xl text-sm text-emerald-100 sm:mt-3 sm:text-lg">
|
||||
<p class="mt-3 max-w-2xl text-sm font-black text-yellow-300 sm:text-base">
|
||||
{{ hero.originName || 'Kho phim Hàn Vietsub mới cập nhật' }}
|
||||
</p>
|
||||
<div class="mt-3 flex flex-wrap gap-1.5 text-xs font-bold text-slate-100 sm:mt-5 sm:gap-2 sm:text-sm">
|
||||
<span v-if="hero.rating" class="rounded bg-black/35 px-2 py-1.5 ring-1 ring-white/10 sm:px-3 sm:py-2">IMDb
|
||||
{{ hero.rating.toFixed(1) }}</span>
|
||||
<span v-if="hero.quality" class="rounded bg-emerald-300 px-2 py-1.5 text-slate-950 sm:px-3 sm:py-2">{{
|
||||
hero.quality }}</span>
|
||||
<span v-if="hero.lang" class="rounded bg-white px-2 py-1.5 text-slate-950 sm:px-3 sm:py-2">{{ hero.lang
|
||||
}}</span>
|
||||
<span v-if="hero.year" class="rounded bg-black/35 px-2 py-1.5 ring-1 ring-white/10 sm:px-3 sm:py-2">{{
|
||||
hero.year }}</span>
|
||||
<span v-if="hero.episode" class="rounded bg-black/35 px-2 py-1.5 ring-1 ring-white/10 sm:px-3 sm:py-2">{{
|
||||
hero.episode }}</span>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-1.5 text-[13px] font-black text-white sm:gap-2">
|
||||
<span v-if="hero.rating"
|
||||
class="rounded-md bg-yellow-400 px-2.5 py-1.5 text-slate-950 shadow-lg shadow-yellow-950/30">
|
||||
TMDb {{ hero.rating.toFixed(1) }}
|
||||
</span>
|
||||
<span v-if="hero.quality" class="rounded-md border border-white/20 bg-white/15 px-2.5 py-1.5">
|
||||
{{ hero.quality }}
|
||||
</span>
|
||||
<span v-if="hero.lang" class="rounded-md border border-white/20 bg-white/15 px-2.5 py-1.5">
|
||||
{{ hero.lang }}
|
||||
</span>
|
||||
<span v-if="hero.year" class="rounded-md border border-white/20 bg-white/15 px-2.5 py-1.5">
|
||||
{{ hero.year }}
|
||||
</span>
|
||||
<span v-if="hero.episode" class="rounded-md border border-white/20 bg-white/15 px-2.5 py-1.5">
|
||||
{{ hero.episode }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hero.categories?.length"
|
||||
class="mt-3 flex flex-wrap gap-1.5 text-xs font-semibold text-white sm:mt-4 sm:gap-2 sm:text-sm">
|
||||
|
||||
<div v-if="hero.categories?.length" class="mt-2 flex flex-wrap gap-1.5 text-[11px] font-black text-white">
|
||||
<span v-for="category in hero.categories.slice(0, 4)" :key="category"
|
||||
class="rounded bg-white/12 px-2 py-1 backdrop-blur sm:px-3 sm:py-1.5">
|
||||
class="rounded-md bg-white/12 px-2.5 py-1.5 backdrop-blur">
|
||||
{{ category }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-3 line-clamp-3 max-w-2xl text-xs leading-6 text-slate-100 sm:mt-5 sm:text-sm sm:leading-7">
|
||||
|
||||
<p class="mt-6 line-clamp-3 max-w-130 text-base font-bold leading-7 text-white drop-shadow">
|
||||
{{ heroDescription }}
|
||||
</p>
|
||||
<div class="mt-6 flex items-center gap-3 sm:mt-8 sm:gap-4">
|
||||
|
||||
<div class="mt-9 flex items-center gap-3">
|
||||
<NuxtLink :to="movieLink(hero)"
|
||||
class="grid size-12 shrink-0 place-items-center rounded-full bg-emerald-300 text-slate-950 shadow-2xl shadow-emerald-950/50 transition hover:scale-105 hover:bg-white sm:size-16"
|
||||
class="grid size-14 shrink-0 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-2xl shadow-yellow-950/50 transition hover:scale-105 hover:bg-yellow-300 sm:size-16"
|
||||
aria-label="Xem ngay">
|
||||
<Play class="size-6 fill-current sm:size-7" />
|
||||
<Play class="ml-1 size-7 fill-current" />
|
||||
</NuxtLink>
|
||||
<button type="button"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full border border-white/20 bg-black/28 text-white backdrop-blur transition hover:bg-white/16 sm:size-12"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full bg-white/15 text-white backdrop-blur transition hover:bg-white/25 sm:size-11"
|
||||
aria-label="Yêu thích">
|
||||
<Heart class="size-4 sm:size-5" />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full border border-white/20 bg-black/28 text-white backdrop-blur transition hover:bg-white/16 sm:size-12"
|
||||
aria-label="Thêm vào thư viện">
|
||||
<BookPlus class="size-4 sm:size-5" />
|
||||
</button>
|
||||
<NuxtLink :to="movieLink(hero)"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full border border-white/20 bg-black/28 text-white backdrop-blur transition hover:bg-white/16 sm:size-12"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full bg-white/15 text-white backdrop-blur transition hover:bg-white/25 sm:size-11"
|
||||
aria-label="Thông tin phim">
|
||||
<Info class="size-4 sm:size-5" />
|
||||
</NuxtLink>
|
||||
@@ -325,28 +318,24 @@ useHead({
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="absolute bottom-8 right-4 hidden max-w-[calc(100%-2rem)] items-center gap-2 sm:flex sm:bottom-12 sm:right-6 sm:gap-3 lg:right-8">
|
||||
class="absolute bottom-14 right-4 hidden max-w-[calc(100%-2rem)] items-center gap-3 sm:flex sm:right-6 lg:bottom-18 lg:right-8 xl:right-10">
|
||||
<button type="button"
|
||||
class="hidden size-9 shrink-0 place-items-center rounded-full border border-white/15 bg-black/35 text-white backdrop-blur transition hover:bg-white/15 sm:size-10 sm:grid"
|
||||
class="hidden size-9 shrink-0 place-items-center rounded-full border border-white/15 bg-black/35 text-white backdrop-blur transition hover:bg-white/15"
|
||||
aria-label="Phim trước" @click="previousHero">
|
||||
<ChevronLeft class="size-4 sm:size-5" />
|
||||
</button>
|
||||
|
||||
<div class="no-scrollbar flex gap-2 overflow-x-auto pb-2 sm:gap-3">
|
||||
<div class="no-scrollbar flex gap-3 overflow-x-auto pb-2">
|
||||
<button v-for="(slide, index) in heroSlides" :key="`${slide.source}-${slide.slug}-thumb`" type="button"
|
||||
class="relative h-12 w-20 shrink-0 overflow-hidden rounded-md border bg-slate-900 shadow-lg shadow-black/30 transition sm:h-16 sm:w-32"
|
||||
:class="index === heroIndex ? 'border-emerald-300 ring-2 ring-emerald-300/45' : 'border-white/15 opacity-70 hover:opacity-100'"
|
||||
class="relative h-14.5 w-26 shrink-0 overflow-hidden rounded-md border bg-slate-900 shadow-lg shadow-black/30 transition"
|
||||
:class="index === heroIndex ? 'border-white ring-2 ring-white/40' : 'border-white/15 opacity-70 hover:opacity-100'"
|
||||
@click="selectHero(index)">
|
||||
<img :src="slide.thumb || slide.poster" :alt="slide.name" class="h-full w-full object-cover">
|
||||
<span
|
||||
class="absolute inset-x-0 bottom-0 truncate bg-black/55 px-1 py-0.5 text-[9px] font-semibold text-white sm:text-[10px]">
|
||||
{{ slide.name }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button"
|
||||
class="hidden size-9 shrink-0 place-items-center rounded-full border border-white/15 bg-black/35 text-white backdrop-blur transition hover:bg-white/15 sm:size-10 sm:grid"
|
||||
class="hidden size-9 shrink-0 place-items-center rounded-full border border-white/15 bg-black/35 text-white backdrop-blur transition hover:bg-white/15"
|
||||
aria-label="Phim tiếp theo" @click="nextHero">
|
||||
<ChevronRight class="size-4 sm:size-5" />
|
||||
</button>
|
||||
@@ -354,14 +343,35 @@ useHead({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else class="mx-auto flex min-h-screen max-w-7xl items-center px-4 pt-36 sm:px-6 lg:px-8 lg:pt-24">
|
||||
<div>
|
||||
<h1 class="text-4xl font-black text-white">KR Phim</h1>
|
||||
<section v-else-if="pending"
|
||||
class="mx-auto flex min-h-screen max-w-390 items-center px-4 pt-36 sm:px-6 lg:px-8 lg:pt-24 xl:px-10">
|
||||
<div class="w-full">
|
||||
<div class="h-12 w-4/5 animate-pulse rounded bg-white/10 sm:h-14" />
|
||||
<div class="mt-4 h-5 w-1/2 animate-pulse rounded bg-yellow-300/20" />
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
<div class="h-8 w-16 animate-pulse rounded-md bg-yellow-300/20" />
|
||||
<div class="h-8 w-18 animate-pulse rounded-md bg-white/10" />
|
||||
<div class="h-8 w-14 animate-pulse rounded-md bg-white/10" />
|
||||
<div class="h-8 w-20 animate-pulse rounded-md bg-white/10" />
|
||||
</div>
|
||||
<div class="mt-8 space-y-3">
|
||||
<div class="h-4 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-11/12 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-4/5 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
<div class="mt-9 flex items-center gap-3">
|
||||
<div class="size-14 animate-pulse rounded-full bg-yellow-300/25 sm:size-16" />
|
||||
<div class="size-10 animate-pulse rounded-full bg-white/10 sm:size-11" />
|
||||
<div class="size-10 animate-pulse rounded-full bg-white/10 sm:size-11" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden">
|
||||
<h1 class="text-4xl font-black text-white">CineK</h1>
|
||||
<p class="mt-3 text-slate-300">Đang tải kho phim Hàn Quốc...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="relative z-10 mx-auto -mt-10 max-w-390 px-4 pb-16 sm:px-6 lg:px-8 xl:px-10">
|
||||
<div class="relative z-10 mx-auto max-w-390 px-4 pb-16 pt-8 sm:px-6 lg:px-8 xl:px-10">
|
||||
<div v-if="sourceStatus.length" class="hidden">
|
||||
<span v-for="source in sourceStatus" :key="source.name"
|
||||
class="rounded-full border border-white/10 bg-white/8 px-3 py-1">
|
||||
@@ -373,11 +383,38 @@ useHead({
|
||||
Không tải được dữ liệu phim. Vui lòng thử lại sau.
|
||||
</p>
|
||||
|
||||
<section v-if="topTrending.length" class="mb-12">
|
||||
<div class="mb-5 flex items-center gap-3">
|
||||
<h2 class="text-2xl font-black uppercase text-white">
|
||||
TOP 10 <span class="text-yellow-400">CineK</span>
|
||||
</h2>
|
||||
<span class="hidden text-xs font-black uppercase tracking-[0.35em] text-slate-500 sm:inline">
|
||||
Thịnh hành hôm nay
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="no-scrollbar -mx-4 ml-0.5 flex snap-x gap-4 overflow-x-auto px-4 pb-5 sm:ml-0">
|
||||
<div v-for="(movie, index) in topTrending" :key="`${movie.source}-${movie.slug}-trending`"
|
||||
class="w-44 shrink-0 snap-start sm:w-52 xl:w-56">
|
||||
<HomeMovieCard :movie="movie" />
|
||||
<div class="mt-2 grid grid-cols-[2.2rem_1fr] gap-2">
|
||||
<span class="text-4xl font-black italic leading-none text-yellow-400">{{ index + 1 }}</span>
|
||||
<span class="min-w-0 pt-1">
|
||||
<span class="block truncate text-sm font-black text-white">{{ movie.name }}</span>
|
||||
<span class="mt-0.5 block truncate text-xs font-semibold text-slate-500">
|
||||
{{ movie.originName || movie.quality || movie.year }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="watchHistory.length" class="mb-12">
|
||||
<div class="mb-4 flex items-center justify-between gap-4">
|
||||
<h2 class="text-2xl font-extrabold text-white">Tiếp Tục Xem</h2>
|
||||
<h2 class="text-2xl font-extrabold text-white">Tiếp tục xem</h2>
|
||||
<button type="button"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full border border-white/20 bg-white/8 text-white transition hover:border-emerald-300/60 hover:bg-white/14"
|
||||
class="grid size-10 shrink-0 place-items-center rounded-full border border-white/20 bg-white/8 text-white transition hover:border-yellow-400/60 hover:bg-white/14"
|
||||
aria-label="Xóa lịch sử xem" @click="clearWatchHistory">
|
||||
<Trash2 class="size-4" />
|
||||
</button>
|
||||
@@ -387,14 +424,13 @@ useHead({
|
||||
<NuxtLink v-for="item in watchHistory" :key="`${item.source}-${item.slug}`" :to="watchHistoryLink(item)"
|
||||
class="group block w-44 shrink-0 snap-start sm:w-52 xl:w-56">
|
||||
<div
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-emerald-300/60">
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-yellow-400/60">
|
||||
<img :src="item.thumb || item.poster" :alt="item.name"
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div class="absolute inset-x-0 bottom-0 h-1 bg-white/18">
|
||||
<span class="block h-full bg-emerald-300" :style="{ width: `${watchProgressPercent(item)}%` }" />
|
||||
<span class="block h-full bg-yellow-400" :style="{ width: `${watchProgressPercent(item)}%` }" />
|
||||
</div>
|
||||
<span
|
||||
class="absolute left-2 top-2 rounded bg-emerald-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
<span class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
{{ item.episodeName || `Tập ${(item.episodeIndex || 0) + 1}` }}
|
||||
</span>
|
||||
<span
|
||||
@@ -418,7 +454,7 @@ useHead({
|
||||
:id="index === 0 ? 'latest' : index === 1 ? 'series' : 'movies'" :key="row.title" class="mb-12">
|
||||
<div class="mb-4 flex items-end justify-between gap-4">
|
||||
<h2 class="text-2xl font-extrabold text-white">{{ row.title }}</h2>
|
||||
<NuxtLink :to="row.to" class="text-sm font-semibold text-emerald-200 hover:text-white">
|
||||
<NuxtLink :to="row.to" class="text-sm font-semibold text-yellow-300 hover:text-white">
|
||||
Xem thêm
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
@@ -119,11 +119,19 @@ function selectNextDay() {
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'Lịch chiếu - KR Phim',
|
||||
title: 'Lịch cập nhật phim - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Lịch cập nhật tập mới từ các nguồn phim OPhim, NguonC và KKPhim.',
|
||||
content: 'Theo dõi lịch cập nhật phim Hàn Quốc mới trên CineK, xem nhanh các tập phim và nội dung mới theo từng ngày.',
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'Lịch cập nhật phim - CineK',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: 'Lịch phim Hàn Quốc mới cập nhật theo ngày, giúp bạn không bỏ lỡ tập mới và phim mới trên CineK.',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -273,7 +281,7 @@ useHead({
|
||||
}
|
||||
|
||||
.schedule-day-tab.active {
|
||||
border-top-color: #00dc82;
|
||||
border-top-color: #facc15;
|
||||
background: rgba(255, 255, 255, 0.09);
|
||||
}
|
||||
|
||||
@@ -294,7 +302,7 @@ useHead({
|
||||
}
|
||||
|
||||
.schedule-day-tab.active .schedule-day-name {
|
||||
color: #00dc82;
|
||||
color: #facc15;
|
||||
}
|
||||
|
||||
.schedule-arrow {
|
||||
@@ -360,7 +368,7 @@ useHead({
|
||||
}
|
||||
|
||||
.schedule-card:hover {
|
||||
border-color: rgba(110, 231, 183, 0.42);
|
||||
border-color: rgba(125, 211, 252, 0.42);
|
||||
background: #424650;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
@@ -419,7 +427,7 @@ useHead({
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-bottom: 16px;
|
||||
color: #6ee7b7;
|
||||
color: #fde047;
|
||||
}
|
||||
|
||||
.schedule-empty p {
|
||||
|
||||
+15
-9
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Clock3, Trash2 } from 'lucide-vue-next'
|
||||
import type { WatchHistoryItem } from '~/composables/useWatchHistory'
|
||||
|
||||
@@ -65,7 +65,13 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Lịch sử xem - KR Phim',
|
||||
title: 'Lịch sử xem - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Xem lại lịch sử xem phim trên CineK và tiếp tục các tập phim Hàn Quốc đang xem dở.',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -76,7 +82,7 @@ useHead({
|
||||
<section class="mx-auto max-w-390 px-4 pb-16 pt-28 sm:px-6 lg:px-8 lg:pt-32 xl:px-10">
|
||||
<div class="mb-6 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-black uppercase text-emerald-300">Thành viên</p>
|
||||
<p class="text-sm font-black uppercase text-yellow-300">Thành viên</p>
|
||||
<h1 class="mt-2 text-3xl font-black sm:text-4xl">Lịch sử xem</h1>
|
||||
</div>
|
||||
<button v-if="historyItems.length" type="button"
|
||||
@@ -95,20 +101,20 @@ useHead({
|
||||
<NuxtLink v-for="item in historyItems" :key="`${item.source}-${item.slug}`" :to="watchHistoryLink(item)"
|
||||
class="group block min-w-0">
|
||||
<div
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-emerald-300/60">
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-yellow-300/60">
|
||||
<img v-if="item.thumb || item.poster" :src="item.thumb || item.poster" :alt="item.name"
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div v-else class="grid h-full w-full place-items-center bg-white/8">
|
||||
<Clock3 class="size-10 text-emerald-300" />
|
||||
<Clock3 class="size-10 text-yellow-300" />
|
||||
</div>
|
||||
<div class="absolute inset-x-0 bottom-0 h-1 bg-white/18">
|
||||
<span class="block h-full bg-emerald-300" :style="{ width: `${watchProgressPercent(item)}%` }" />
|
||||
<span class="block h-full bg-yellow-300" :style="{ width: `${watchProgressPercent(item)}%` }" />
|
||||
</div>
|
||||
<span
|
||||
class="absolute bottom-3 right-3 rounded bg-black/70 px-2 py-1 text-[11px] font-black text-white opacity-0 transition group-hover:opacity-100">
|
||||
Xem tiếp
|
||||
</span>
|
||||
<span class="absolute left-2 top-2 rounded bg-emerald-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
<span class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
{{ item.episodeName || `Tập ${(item.episodeIndex || 0) + 1}` }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -121,13 +127,13 @@ useHead({
|
||||
|
||||
<div v-else
|
||||
class="flex min-h-80 flex-col items-center justify-center rounded-lg border border-white/10 bg-white/6 p-6 text-center">
|
||||
<Clock3 class="size-12 text-emerald-300" />
|
||||
<Clock3 class="size-12 text-yellow-300" />
|
||||
<h2 class="mt-4 text-xl font-black">Chưa có lịch sử xem</h2>
|
||||
<p class="mt-2 max-w-md text-sm leading-6 text-slate-300">
|
||||
Phim bạn đã xem sẽ xuất hiện ở đây để bạn có thể xem tiếp nhanh hơn.
|
||||
</p>
|
||||
<NuxtLink to="/phim"
|
||||
class="mt-5 inline-flex h-11 items-center justify-center rounded-md bg-emerald-300 px-5 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
class="mt-5 inline-flex h-11 items-center justify-center rounded-md bg-yellow-300 px-5 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
Duyệt phim
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
+43
-29
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ChevronDown, Heart, Play, Plus, Share2, Star } from 'lucide-vue-next'
|
||||
|
||||
const route = useRoute()
|
||||
const requestedSource = computed(() => String(route.query.source || 'ophim'))
|
||||
const requestedSource = computed(() => String(route.query.source || 'nguonc'))
|
||||
const requestedSources = computed(() => typeof route.query.srcs === 'string' ? route.query.srcs : '')
|
||||
const selectedServer = ref(0)
|
||||
const movieInfoOpen = ref(false)
|
||||
@@ -28,7 +28,7 @@ const { data: movie, pending, error } = await useFetch(() => `/api/movies/${rout
|
||||
|
||||
const servers = computed(() => movie.value?.servers ?? [])
|
||||
const activeServer = computed(() => servers.value[selectedServer.value])
|
||||
const activeSource = computed(() => String(activeServer.value?.source || movie.value?.source || route.query.source || 'ophim'))
|
||||
const activeSource = computed(() => String(activeServer.value?.source || movie.value?.source || route.query.source || 'nguonc'))
|
||||
const activeSourceSlug = computed(() => String(activeServer.value?.sourceSlug || movie.value?.slug || route.params.slug))
|
||||
const activeSourceServerIndex = computed(() => Number(activeServer.value?.sourceServerIndex ?? 0))
|
||||
const sourceOptions = computed(() =>
|
||||
@@ -144,7 +144,7 @@ async function shareMovie() {
|
||||
if (!import.meta.client || !movie.value) return
|
||||
|
||||
const shareUrl = window.location.href
|
||||
const shareTitle = `${movie.value.name} - KR Phim`
|
||||
const shareTitle = `${movie.value.name} - CineK`
|
||||
|
||||
try {
|
||||
if (navigator.share) {
|
||||
@@ -181,11 +181,25 @@ watch([libraryItem, user], () => {
|
||||
})
|
||||
|
||||
useHead(() => ({
|
||||
title: movie.value ? `${movie.value.name} - KR Phim` : 'Đang tải phim - KR Phim',
|
||||
title: movie.value ? `${movie.value.name} - Xem phim online - CineK` : 'Đang tải phim - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: movie.value?.content || 'Xem phim Hàn Quốc Vietsub.',
|
||||
content: movie.value
|
||||
? `Xem thông tin phim ${movie.value.name}${movie.value.originName ? ` (${movie.value.originName})` : ''}, danh sách tập, diễn viên và các server xem phim tại CineK.`
|
||||
: 'Xem phim Hàn Quốc online tại CineK với Vietsub, thuyết minh và lồng tiếng.',
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: movie.value ? `${movie.value.name} - CineK` : 'CineK - Xem phim Hàn Quốc online',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: movie.value?.content || 'Xem thông tin phim, trailer, diễn viên và danh sách tập mới trên CineK.',
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: movie.value?.poster || movie.value?.thumb || '/icon.png',
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -204,7 +218,7 @@ useHead(() => ({
|
||||
<div v-else-if="error || !movie" class="mx-auto flex min-h-screen max-w-4xl items-center px-4 pt-36 lg:pt-24">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black">Không mở được phim</h1>
|
||||
<p class="mt-3 text-slate-300">Nguồn phim có thể đang chặn request hoặc phim chưa có tập xem.</p>
|
||||
<p class="mt-3 text-slate-300">Phim có thể đang được cập nhật hoặc hiện chưa có tập xem.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -223,7 +237,7 @@ useHead(() => ({
|
||||
class="mx-auto aspect-2/3 w-29 rounded-md object-cover shadow-xl shadow-black/40 sm:w-52 lg:w-full">
|
||||
|
||||
<h1 class="mt-4 text-[1.35rem] font-black leading-tight sm:text-2xl lg:mt-5">{{ movie.name }}</h1>
|
||||
<p v-if="movie.originName" class="mt-1 text-sm font-bold text-slate-100 sm:text-emerald-200">{{
|
||||
<p v-if="movie.originName" class="mt-1 text-sm font-bold text-slate-100 sm:text-yellow-200">{{
|
||||
movie.originName }}</p>
|
||||
|
||||
<div class="mt-3 hidden flex-wrap justify-center gap-2 text-xs font-black sm:flex lg:justify-start">
|
||||
@@ -231,7 +245,7 @@ useHead(() => ({
|
||||
<span v-if="movie.episode" class="rounded bg-white/12 px-2 py-1">{{ movie.episode }}</span>
|
||||
<span v-if="movie.time" class="rounded bg-white/12 px-2 py-1">{{ movie.time }}</span>
|
||||
<span v-if="movie.rating" class="inline-flex items-center gap-1 rounded bg-white/12 px-2 py-1">
|
||||
<Star class="size-3 fill-emerald-300 text-emerald-300" />
|
||||
<Star class="size-3 fill-yellow-300 text-yellow-300" />
|
||||
{{ movie.rating.toFixed(1) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -258,7 +272,7 @@ useHead(() => ({
|
||||
<section
|
||||
class="mt-0 rounded-none border-y border-white/10 bg-transparent px-0 pb-0 pt-3 shadow-none sm:mt-0 sm:rounded-md sm:border sm:bg-[#15161b]/95 sm:p-5 sm:shadow-2xl sm:shadow-black/40 lg:p-5">
|
||||
<button type="button"
|
||||
class="mx-auto flex items-center gap-1 pb-3 text-sm font-bold text-emerald-200 sm:hidden"
|
||||
class="mx-auto flex items-center gap-1 pb-3 text-sm font-bold text-yellow-200 sm:hidden"
|
||||
:aria-expanded="movieInfoOpen" @click="movieInfoOpen = !movieInfoOpen">
|
||||
<span>Thông tin phim</span>
|
||||
<ChevronDown class="size-4 transition" :class="movieInfoOpen ? 'rotate-180' : ''" />
|
||||
@@ -280,11 +294,11 @@ useHead(() => ({
|
||||
</div>
|
||||
|
||||
<div class="hidden">
|
||||
<p class="text-sm font-black text-white">Nguồn API</p>
|
||||
<p class="text-sm font-black text-white">Nguồn xem</p>
|
||||
<div class="flex gap-2 overflow-x-auto pb-1 sm:pb-0">
|
||||
<NuxtLink v-for="source in sourceOptions" :key="source.value" :to="sourceLink(source.value)"
|
||||
class="shrink-0 rounded-md px-4 py-2 text-sm font-black transition"
|
||||
:class="activeSource === source.value ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
:class="activeSource === source.value ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
{{ source.label }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
@@ -294,7 +308,7 @@ useHead(() => ({
|
||||
class="flex flex-col gap-4 border-b border-white/10 py-4 sm:flex-row sm:items-center sm:justify-between sm:py-5">
|
||||
<div class="flex flex-wrap justify-center gap-3 sm:justify-start">
|
||||
<NuxtLink :to="firstWatchLink"
|
||||
class="inline-flex h-12 w-full items-center justify-center gap-2 rounded-full bg-linear-to-r from-emerald-400 to-emerald-300 px-7 text-sm font-black text-slate-950 transition hover:from-emerald-300 hover:to-white sm:w-auto">
|
||||
class="inline-flex h-12 w-full items-center justify-center gap-2 rounded-full bg-linear-to-r from-yellow-400 to-yellow-300 px-7 text-sm font-black text-slate-950 transition hover:from-yellow-300 hover:to-white sm:w-auto">
|
||||
<Play class="size-4 fill-current" />
|
||||
Xem Ngay
|
||||
</NuxtLink>
|
||||
@@ -302,42 +316,42 @@ useHead(() => ({
|
||||
|
||||
<div class="flex flex-col gap-3 sm:items-end">
|
||||
<div class="hidden">
|
||||
<span class="text-xs font-black text-slate-300">Nguồn API</span>
|
||||
<span class="text-xs font-black text-slate-300">Nguồn xem</span>
|
||||
<NuxtLink v-for="source in sourceOptions" :key="source.value" :to="sourceLink(source.value)"
|
||||
class="shrink-0 rounded-md px-4 py-2 text-sm font-black transition"
|
||||
:class="activeSource === source.value ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
:class="activeSource === source.value ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
{{ source.label }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 px-8 sm:justify-center sm:px-0">
|
||||
<button type="button"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold transition hover:text-emerald-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:class="isFavoriteMovie ? 'text-emerald-300' : 'text-white'" aria-label="Yêu thích"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold transition hover:text-yellow-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:class="isFavoriteMovie ? 'text-yellow-300' : 'text-white'" aria-label="Yêu thích"
|
||||
:disabled="actionBusy" @click="toggleFavorite">
|
||||
<Heart class="size-5" :class="isFavoriteMovie ? 'fill-current' : ''" />
|
||||
<span>{{ isFavoriteMovie ? 'Đã thích' : 'Yêu thích' }}</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold text-white transition hover:text-emerald-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold text-white transition hover:text-yellow-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
aria-label="Thêm vào" :disabled="actionBusy" @click="addToWatchLater">
|
||||
<Plus class="size-5" />
|
||||
<span>Thêm vào</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold text-white transition hover:text-emerald-200"
|
||||
class="flex cursor-pointer flex-col items-center gap-1 text-xs font-bold text-white transition hover:text-yellow-200"
|
||||
aria-label="Chia sẻ" @click="shareMovie">
|
||||
<Share2 class="size-5" />
|
||||
<span>Chia sẻ</span>
|
||||
</button>
|
||||
<div v-if="movie.rating"
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-emerald-400 px-3 py-1.5 text-sm font-black text-slate-950">
|
||||
class="inline-flex items-center gap-1.5 rounded-full bg-yellow-400 px-3 py-1.5 text-sm font-black text-slate-950">
|
||||
<Star class="size-3.5 fill-current" />
|
||||
{{ movie.rating.toFixed(1) }}
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="actionMessage"
|
||||
class="mt-2 text-center text-xs font-bold text-emerald-200 sm:text-right">
|
||||
class="mt-2 text-center text-xs font-bold text-yellow-200 sm:text-right">
|
||||
{{ actionMessage }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -346,10 +360,10 @@ useHead(() => ({
|
||||
<div
|
||||
class="mt-3 flex justify-between gap-6 border-b border-white/10 px-6 text-sm font-black sm:mt-0 sm:justify-center sm:px-0 sm:pt-5">
|
||||
<button type="button" class="px-5 pb-3 sm:px-0"
|
||||
:class="activeTab === 'episodes' ? 'border-b-2 border-emerald-300 text-emerald-200' : 'text-slate-300'"
|
||||
:class="activeTab === 'episodes' ? 'border-b-2 border-yellow-300 text-yellow-200' : 'text-slate-300'"
|
||||
@click="activeTab = 'episodes'">Tập phim</button>
|
||||
<button type="button" class="px-5 pb-3 sm:px-0"
|
||||
:class="activeTab === 'actors' ? 'border-b-2 border-emerald-300 text-emerald-200' : 'text-slate-300'"
|
||||
:class="activeTab === 'actors' ? 'border-b-2 border-yellow-300 text-yellow-200' : 'text-slate-300'"
|
||||
@click="activeTab = 'actors'">Diễn viên</button>
|
||||
</div>
|
||||
|
||||
@@ -359,7 +373,7 @@ useHead(() => ({
|
||||
<div v-if="servers.length > 1" class="mt-3 flex gap-2 overflow-x-auto pb-2">
|
||||
<button v-for="(server, index) in servers" :key="server.name" type="button"
|
||||
class="shrink-0 rounded px-4 py-2 text-sm font-black"
|
||||
:class="selectedServer === index ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'"
|
||||
:class="selectedServer === index ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'"
|
||||
@click="selectedServer = index">
|
||||
{{ serverLabel(server, index) }}
|
||||
</button>
|
||||
@@ -369,13 +383,13 @@ useHead(() => ({
|
||||
class="mt-3 grid grid-cols-3 gap-2 rounded-md border border-white/10 p-3 sm:grid-cols-3 lg:grid-cols-6">
|
||||
<NuxtLink v-for="(episode, index) in activeServer.episodes" :key="`${episode.name}-${index}`"
|
||||
:to="episodeLink(index)"
|
||||
class="rounded-md bg-white/10 px-4 py-3 text-center text-sm font-black text-white transition first:bg-emerald-400 first:text-slate-950 hover:bg-emerald-400 hover:text-slate-950">
|
||||
class="rounded-md bg-white/10 px-4 py-3 text-center text-sm font-black text-white transition first:bg-yellow-400 first:text-slate-950 hover:bg-yellow-400 hover:text-slate-950">
|
||||
{{ formatEpisodeName(episode.name, index) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<p v-else class="mt-4 rounded-md border border-white/10 bg-slate-900/70 p-4 text-sm text-slate-300">
|
||||
Chưa có tập xem từ nguồn này.
|
||||
Chưa có tập xem từ server này.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -388,13 +402,13 @@ useHead(() => ({
|
||||
<img v-if="actor.avatar" :src="actor.avatar" :alt="actor.name"
|
||||
class="size-14 shrink-0 rounded-md object-cover">
|
||||
<div v-else
|
||||
class="grid size-14 shrink-0 place-items-center rounded-md bg-emerald-400/18 text-lg font-black text-emerald-100 ring-1 ring-emerald-300/20">
|
||||
class="grid size-14 shrink-0 place-items-center rounded-md bg-yellow-400/18 text-lg font-black text-yellow-100 ring-1 ring-yellow-300/20">
|
||||
{{ actorInitial(actor.name) }}
|
||||
</div>
|
||||
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-black text-white">{{ actor.name }}</p>
|
||||
<p class="mt-1 truncate text-xs font-semibold text-emerald-200">
|
||||
<p class="mt-1 truncate text-xs font-semibold text-yellow-200">
|
||||
{{ actor.originalName || 'Tên quốc tế đang cập nhật' }}
|
||||
</p>
|
||||
<p class="mt-1 truncate text-xs font-semibold text-slate-400">
|
||||
@@ -405,7 +419,7 @@ useHead(() => ({
|
||||
</div>
|
||||
|
||||
<p v-else class="mt-4 rounded-md border border-white/10 bg-slate-900/70 p-4 text-sm text-slate-300">
|
||||
Nguồn này chưa có dữ liệu diễn viên.
|
||||
Mục này chưa có dữ liệu diễn viên.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+21
-19
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { ChevronLeft, ChevronRight, Search } from 'lucide-vue-next'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -8,12 +8,6 @@ const page = ref(Math.max(Number(route.query.page || 1), 1))
|
||||
const keyword = ref(typeof route.query.q === 'string' ? route.query.q : '')
|
||||
const debouncedKeyword = ref(keyword.value.trim())
|
||||
const type = ref(typeof route.query.type === 'string' ? route.query.type : 'all')
|
||||
const sourceOptions = [
|
||||
{ label: 'Tất cả API', value: 'all' },
|
||||
{ label: 'OPhim', value: 'ophim' },
|
||||
{ label: 'NguonC', value: 'nguonc' },
|
||||
{ label: 'KKPhim', value: 'kkphim' },
|
||||
]
|
||||
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
@@ -93,11 +87,19 @@ onBeforeUnmount(() => {
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Duyệt phim Hàn Quốc - KR Phim',
|
||||
title: 'Kho phim Hàn Quốc - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Tìm kiếm và lọc phim Hàn Quốc Vietsub trên KR Phim.',
|
||||
content: 'Duyệt kho phim Hàn Quốc online trên CineK: tìm phim bộ, phim lẻ, phim mới cập nhật, Vietsub, thuyết minh và lồng tiếng.',
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'Kho phim Hàn Quốc - CineK',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: 'Tìm kiếm và lọc phim Hàn Quốc online theo thể loại, tập mới và nội dung mới cập nhật.',
|
||||
},
|
||||
],
|
||||
})
|
||||
@@ -110,7 +112,7 @@ useHead({
|
||||
<section class="mx-auto max-w-390 px-4 pb-16 pt-36 sm:px-6 lg:px-8 lg:pt-28 xl:px-10">
|
||||
<div class="mb-8 flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-emerald-200">Kho phim Hàn Quốc</p>
|
||||
<p class="text-sm font-semibold text-yellow-200">Kho phim Hàn Quốc</p>
|
||||
<h1 class="mt-2 text-3xl font-black text-white sm:text-5xl">
|
||||
Duyệt phim
|
||||
</h1>
|
||||
@@ -118,8 +120,8 @@ useHead({
|
||||
|
||||
<div class="flex w-full flex-col gap-3 sm:flex-row lg:max-w-3xl">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center rounded-full border border-white/10 bg-white/8 px-4 py-3 shadow-2xl shadow-emerald-950/20">
|
||||
<Search class="mr-3 size-4 shrink-0 text-emerald-200" />
|
||||
class="flex min-w-0 flex-1 items-center rounded-full border border-white/10 bg-white/8 px-4 py-3 shadow-2xl shadow-yellow-950/20">
|
||||
<Search class="mr-3 size-4 shrink-0 text-yellow-200" />
|
||||
<input v-model="keyword" type="search" placeholder="Tìm phim Hàn Quốc..."
|
||||
class="w-full bg-transparent text-sm text-white outline-none placeholder:text-slate-400">
|
||||
</div>
|
||||
@@ -127,15 +129,15 @@ useHead({
|
||||
<div
|
||||
class="grid grid-cols-3 rounded-full border border-white/10 bg-white/8 p-1 text-sm font-bold text-slate-200">
|
||||
<button type="button" class="rounded-full px-4 py-2 transition"
|
||||
:class="type === 'all' ? 'bg-emerald-300 text-slate-950' : 'hover:text-white'" @click="type = 'all'">
|
||||
:class="type === 'all' ? 'bg-yellow-300 text-slate-950' : 'hover:text-white'" @click="type = 'all'">
|
||||
Tất cả
|
||||
</button>
|
||||
<button type="button" class="rounded-full px-4 py-2 transition"
|
||||
:class="type === 'series' ? 'bg-emerald-300 text-slate-950' : 'hover:text-white'" @click="type = 'series'">
|
||||
:class="type === 'series' ? 'bg-yellow-300 text-slate-950' : 'hover:text-white'" @click="type = 'series'">
|
||||
Phim bộ
|
||||
</button>
|
||||
<button type="button" class="rounded-full px-4 py-2 transition"
|
||||
:class="type === 'single' ? 'bg-emerald-300 text-slate-950' : 'hover:text-white'" @click="type = 'single'">
|
||||
:class="type === 'single' ? 'bg-yellow-300 text-slate-950' : 'hover:text-white'" @click="type = 'single'">
|
||||
Phim lẻ
|
||||
</button>
|
||||
</div>
|
||||
@@ -161,14 +163,14 @@ useHead({
|
||||
<NuxtLink v-for="movie in filteredMovies" :key="`${movie.source}-${movie.slug}`"
|
||||
:to="movieLink(movie)" class="group">
|
||||
<div
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-emerald-300/60">
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-yellow-300/60">
|
||||
<img :src="movie.thumb || movie.poster" :alt="movie.name"
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div class="absolute inset-x-0 bottom-0 bg-linear-to-t from-slate-950 via-slate-950/70 to-transparent p-3">
|
||||
<p class="line-clamp-2 text-sm font-bold leading-snug text-white">{{ movie.name }}</p>
|
||||
<p class="mt-1 truncate text-xs text-emerald-100">{{ movie.episode || movie.year || movie.quality }}</p>
|
||||
<p class="mt-1 truncate text-xs text-yellow-100">{{ movie.episode || movie.year || movie.quality }}</p>
|
||||
</div>
|
||||
<span v-if="movie.sources?.length > 1" class="absolute left-2 top-2 rounded bg-emerald-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
<span v-if="movie.sources?.length > 1" class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
{{ movie.sources.length }} server
|
||||
</span>
|
||||
</div>
|
||||
@@ -189,7 +191,7 @@ useHead({
|
||||
<span class="text-sm text-slate-300">Trang {{ page }}<template v-if="totalPages"> / {{ totalPages
|
||||
}}</template></span>
|
||||
<button type="button"
|
||||
class="grid size-11 place-items-center rounded-full bg-emerald-300 text-slate-950 hover:bg-white"
|
||||
class="grid size-11 place-items-center rounded-full bg-yellow-300 text-slate-950 hover:bg-white"
|
||||
aria-label="Trang sau" @click="nextPage">
|
||||
<ChevronRight class="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { Clock3, Heart, Mail, UserRound } from 'lucide-vue-next'
|
||||
|
||||
const { user, loading, initAuth } = useSupabaseAuth()
|
||||
@@ -21,7 +21,13 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Trang cá nhân - KR Phim',
|
||||
title: 'Trang cá nhân - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Quản lý tài khoản CineK, đồng bộ lịch sử xem, danh sách yêu thích và các phim muốn xem sau.',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -31,7 +37,7 @@ useHead({
|
||||
|
||||
<section class="mx-auto max-w-390 px-4 pb-16 pt-28 sm:px-6 lg:px-8 lg:pt-32 xl:px-10">
|
||||
<div class="mb-6">
|
||||
<p class="text-sm font-black uppercase text-emerald-300">Thành viên</p>
|
||||
<p class="text-sm font-black uppercase text-yellow-300">Thành viên</p>
|
||||
<h1 class="mt-2 text-3xl font-black sm:text-4xl">Trang cá nhân</h1>
|
||||
</div>
|
||||
|
||||
@@ -43,15 +49,15 @@ useHead({
|
||||
<section class="rounded-lg border border-white/10 bg-white/6 p-5 sm:p-6">
|
||||
<div class="flex flex-col gap-5 sm:flex-row sm:items-center">
|
||||
<img :src="memberAvatar" :alt="memberName"
|
||||
class="size-24 shrink-0 rounded-full object-cover ring-4 ring-emerald-300/30">
|
||||
class="size-24 shrink-0 rounded-full object-cover ring-4 ring-yellow-300/30">
|
||||
<div class="min-w-0">
|
||||
<h2 class="truncate text-2xl font-black">{{ memberName }}</h2>
|
||||
<p class="mt-2 inline-flex items-center gap-2 text-sm font-semibold text-slate-300">
|
||||
<Mail class="size-4 text-emerald-300" />
|
||||
<Mail class="size-4 text-yellow-300" />
|
||||
{{ user.email }}
|
||||
</p>
|
||||
<p class="mt-4 max-w-2xl text-sm leading-6 text-slate-400">
|
||||
Tài khoản này dùng để đồng bộ lịch sử xem và các dữ liệu cá nhân của bạn trên KR Phim.
|
||||
Tài khoản này dùng để đồng bộ lịch sử xem và các dữ liệu cá nhân của bạn trên CineK.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +66,7 @@ useHead({
|
||||
<aside class="grid gap-3 sm:grid-cols-2 lg:grid-cols-1">
|
||||
<NuxtLink to="/lich-su"
|
||||
class="flex items-center gap-3 rounded-lg border border-white/10 bg-white/6 p-4 transition hover:bg-white/10">
|
||||
<span class="grid size-10 place-items-center rounded-md bg-emerald-300 text-slate-950">
|
||||
<span class="grid size-10 place-items-center rounded-md bg-yellow-300 text-slate-950">
|
||||
<Clock3 class="size-5" />
|
||||
</span>
|
||||
<span>
|
||||
@@ -70,7 +76,7 @@ useHead({
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/yeu-thich"
|
||||
class="flex items-center gap-3 rounded-lg border border-white/10 bg-white/6 p-4 transition hover:bg-white/10">
|
||||
<span class="grid size-10 place-items-center rounded-md bg-white/10 text-emerald-300">
|
||||
<span class="grid size-10 place-items-center rounded-md bg-white/10 text-yellow-300">
|
||||
<Heart class="size-5 fill-current" />
|
||||
</span>
|
||||
<span>
|
||||
@@ -82,7 +88,7 @@ useHead({
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-lg border border-white/10 bg-white/6 p-6">
|
||||
<UserRound class="size-10 text-emerald-300" />
|
||||
<UserRound class="size-10 text-yellow-300" />
|
||||
<h2 class="mt-4 text-xl font-black">Bạn chưa đăng nhập</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-300">
|
||||
Bấm nút Thành viên trên header để đăng nhập hoặc đăng ký tài khoản.
|
||||
|
||||
+205
-86
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type Hls from 'hls.js'
|
||||
import {
|
||||
FastForward,
|
||||
@@ -76,11 +76,12 @@ const {
|
||||
saveWatchLater,
|
||||
} = useMovieLibrary()
|
||||
|
||||
const { data: movie, pending, error } = await useFetch(() => `/api/movies/${route.params.slug}`, {
|
||||
const { data: movie, pending, error } = useFetch(() => `/api/movies/${route.params.slug}`, {
|
||||
query: computed(() => ({
|
||||
source: route.query.source,
|
||||
srcs: route.query.srcs,
|
||||
})),
|
||||
lazy: true,
|
||||
watch: [() => route.query.source, () => route.query.srcs],
|
||||
})
|
||||
|
||||
@@ -735,6 +736,9 @@ function handlePlayerTap(event: MouseEvent) {
|
||||
|
||||
function handleTouchStart(event: TouchEvent) {
|
||||
const touch = event.touches[0]
|
||||
|
||||
if (!touch) return
|
||||
|
||||
touchStartX = touch.clientX
|
||||
touchStartY = touch.clientY
|
||||
touchStartAt = Date.now()
|
||||
@@ -742,7 +746,10 @@ function handleTouchStart(event: TouchEvent) {
|
||||
|
||||
function handleTouchEnd(event: TouchEvent) {
|
||||
if (!videoRef.value) return
|
||||
|
||||
const touch = event.changedTouches[0]
|
||||
if (!touch) return
|
||||
|
||||
const deltaX = touch.clientX - touchStartX
|
||||
const deltaY = touch.clientY - touchStartY
|
||||
const elapsed = Date.now() - touchStartAt
|
||||
@@ -754,7 +761,11 @@ function handleTouchEnd(event: TouchEvent) {
|
||||
}
|
||||
|
||||
if (Math.abs(deltaY) > 48 && Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||
const nextVolume = Math.min(Math.max(videoVolume.value + (deltaY < 0 ? 0.1 : -0.1), 0), 1)
|
||||
const nextVolume = Math.min(
|
||||
Math.max(videoVolume.value + (deltaY < 0 ? 0.1 : -0.1), 0),
|
||||
1
|
||||
)
|
||||
|
||||
videoVolume.value = Number(nextVolume.toFixed(1))
|
||||
videoRef.value.volume = videoVolume.value
|
||||
videoRef.value.muted = videoVolume.value === 0
|
||||
@@ -826,7 +837,7 @@ async function shareMovie() {
|
||||
try {
|
||||
if (navigator.share) {
|
||||
await navigator.share({
|
||||
title: `${movie.value.name} - KR Phim`,
|
||||
title: `${movie.value.name} - CineK`,
|
||||
text: activeEpisode.value?.name || movie.value.originName || movie.value.name,
|
||||
url: window.location.href,
|
||||
})
|
||||
@@ -891,11 +902,25 @@ watch([libraryItem, user], () => {
|
||||
})
|
||||
|
||||
useHead(() => ({
|
||||
title: movie.value ? `Xem ${movie.value.name} - ${activeEpisode.value?.name || 'Tập 1'} - KR Phim` : 'Đang tải phim - KR Phim',
|
||||
title: movie.value ? `Xem ${movie.value.name} - ${activeEpisode.value?.name || 'Tập 1'} - CineK` : 'Đang tải phim - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: movie.value?.content || 'Xem phim Hàn Quốc Vietsub.',
|
||||
content: movie.value
|
||||
? `Xem ${movie.value.name} ${activeEpisode.value?.name || 'Tập 1'} online tại CineK, hỗ trợ Vietsub, thuyết minh, lồng tiếng và nhiều server xem.`
|
||||
: 'Xem phim Hàn Quốc online tại CineK với trình phát mượt, hỗ trợ nhiều server xem.',
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: movie.value ? `Xem ${movie.value.name} - CineK` : 'CineK - Xem phim Hàn Quốc online',
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: movie.value?.content || 'Xem phim Hàn Quốc online với trải nghiệm mượt trên CineK.',
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: movie.value?.poster || movie.value?.thumb || '/icon.png',
|
||||
},
|
||||
],
|
||||
}))
|
||||
@@ -905,21 +930,89 @@ useHead(() => ({
|
||||
<main class="min-h-screen bg-[#08090d] text-white">
|
||||
<AppHeader />
|
||||
|
||||
<div v-if="pending" class="mx-auto min-h-screen max-w-390 px-4 pt-36 sm:px-6 lg:px-8 lg:pt-28 xl:px-10">
|
||||
<div class="aspect-video animate-pulse rounded-md bg-white/10" />
|
||||
<Transition name="watch-page" mode="out-in">
|
||||
<section v-if="pending && !movie" key="loading"
|
||||
class="mx-auto max-w-390 px-3 pb-16 pt-24 sm:px-6 sm:pt-36 lg:px-8 lg:pt-28 xl:px-10">
|
||||
<div class="h-5 w-28 animate-pulse rounded bg-white/10" />
|
||||
|
||||
<div class="mt-4 overflow-hidden rounded-md border border-white/10 bg-black shadow-2xl shadow-black/40">
|
||||
<div class="border-b border-white/10 bg-slate-950/92 p-2 sm:hidden">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="h-10 animate-pulse rounded-md bg-white/10" />
|
||||
<div class="h-10 animate-pulse rounded-md bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative aspect-video overflow-hidden bg-slate-950">
|
||||
<div class="absolute left-3 top-3 hidden h-8 w-36 animate-pulse rounded-md bg-white/10 sm:block" />
|
||||
<div class="absolute right-3 top-3 hidden h-8 w-44 animate-pulse rounded-md bg-white/10 sm:block" />
|
||||
<div class="absolute inset-0 grid place-items-center">
|
||||
<Loader2 class="size-10 animate-spin text-yellow-300" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-7 border-t border-white/10 px-4 py-4 sm:justify-start">
|
||||
<div class="h-4 w-20 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-20 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-20 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="error || !movie" class="mx-auto flex min-h-screen max-w-4xl items-center px-4 pt-36 lg:pt-24">
|
||||
<div class="mt-5 grid gap-6 lg:grid-cols-[minmax(0,1fr)_24rem]">
|
||||
<section>
|
||||
<div class="flex gap-4">
|
||||
<div class="hidden h-28 w-20 animate-pulse rounded-md bg-white/10 sm:block" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="h-7 w-3/4 animate-pulse rounded bg-white/10" />
|
||||
<div class="mt-3 h-4 w-1/2 animate-pulse rounded bg-yellow-300/20" />
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<div class="h-7 w-14 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-7 w-20 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-7 w-16 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 space-y-3">
|
||||
<div class="h-4 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-11/12 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-4/5 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
<div class="mt-5 h-12 animate-pulse rounded-md bg-yellow-300/20" />
|
||||
<div class="mt-6 border-t border-white/10 pt-4">
|
||||
<div class="h-9 w-32 animate-pulse rounded bg-white/10" />
|
||||
<div class="mt-5 grid grid-cols-2 gap-2 sm:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8">
|
||||
<div v-for="index in 12" :key="index" class="h-14 animate-pulse rounded-md bg-white/10" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<aside class="hidden rounded-md border border-white/10 bg-white/5 p-5 sm:block lg:self-start">
|
||||
<div class="h-5 w-32 animate-pulse rounded bg-white/10" />
|
||||
<div class="mt-5 space-y-4">
|
||||
<div class="h-4 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-5/6 animate-pulse rounded bg-white/10" />
|
||||
<div class="h-4 w-2/3 animate-pulse rounded bg-white/10" />
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div v-else-if="error || !movie" key="error"
|
||||
class="mx-auto flex min-h-screen max-w-4xl items-center px-4 pt-36 lg:pt-24">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black">Không mở được phim</h1>
|
||||
<p class="mt-3 text-slate-300">Nguồn phim có thể đang chặn request hoặc phim chưa có tập xem.</p>
|
||||
<p class="mt-3 text-slate-300">Phim có thể đang được cập nhật hoặc hiện chưa có tập xem.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="mx-auto max-w-390 px-3 pb-16 pt-24 sm:px-6 sm:pt-36 lg:px-8 lg:pt-28 xl:px-10">
|
||||
<NuxtLink :to="{ path: `/phim/${route.params.slug}`, query: { source: route.query.source, srcs: route.query.srcs } }"
|
||||
class="text-sm font-bold text-slate-100 hover:text-emerald-200">
|
||||
<section v-else key="content"
|
||||
class="relative mx-auto max-w-390 px-3 pb-16 pt-24 sm:px-6 sm:pt-36 lg:px-8 lg:pt-28 xl:px-10">
|
||||
<div v-if="pending"
|
||||
class="pointer-events-none fixed inset-x-0 top-0 z-50 h-0.5 overflow-hidden bg-yellow-400/10">
|
||||
<span class="kr-loading-line block h-full w-1/3 rounded-full bg-yellow-300" />
|
||||
</div>
|
||||
|
||||
<NuxtLink
|
||||
:to="{ path: `/phim/${route.params.slug}`, query: { source: route.query.source, srcs: route.query.srcs } }"
|
||||
class="text-sm font-bold text-slate-100 hover:text-yellow-200">
|
||||
‹ Chi tiết phim
|
||||
</NuxtLink>
|
||||
|
||||
@@ -928,38 +1021,40 @@ useHead(() => ({
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button type="button"
|
||||
class="rounded-md px-3 py-2 text-sm font-black transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'embed' ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
:class="playerMode === 'embed' ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
:disabled="!canUseEmbed" @click="selectPlayerMode('embed')">
|
||||
Nhúng
|
||||
</button>
|
||||
<button type="button"
|
||||
class="rounded-md px-3 py-2 text-sm font-black transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'hls' ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
:class="playerMode === 'hls' ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
:disabled="!canUseHls" @click="selectPlayerMode('hls')">
|
||||
HLS
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="servers.length > 1" class="no-scrollbar mt-2 flex gap-2 overflow-x-auto">
|
||||
<!-- <div v-if="servers.length > 1" class="no-scrollbar mt-2 flex gap-2 overflow-x-auto">
|
||||
<button v-for="(server, index) in servers" :key="`${server.name}-${index}-mobile-player`" type="button"
|
||||
class="shrink-0 rounded-md px-3 py-2 text-xs font-black transition"
|
||||
:class="selectedServer === index ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
:class="selectedServer === index ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-slate-100'"
|
||||
@click="selectServer(index)">
|
||||
{{ serverLabel(server, index) }}
|
||||
</button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="relative aspect-video bg-slate-950">
|
||||
<div
|
||||
class="absolute left-3 top-3 z-20 hidden rounded-md border border-white/10 bg-black/65 p-1 text-xs font-black text-white backdrop-blur sm:inline-flex">
|
||||
<button type="button" class="rounded px-3 py-1.5 transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'embed' ? 'bg-emerald-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
<button type="button"
|
||||
class="rounded px-3 py-1.5 transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'embed' ? 'bg-yellow-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
:disabled="!canUseEmbed" @click="selectPlayerMode('embed')">
|
||||
Nhúng
|
||||
</button>
|
||||
<button type="button" class="rounded px-3 py-1.5 transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'hls' ? 'bg-emerald-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
<button type="button"
|
||||
class="rounded px-3 py-1.5 transition disabled:cursor-not-allowed disabled:opacity-40"
|
||||
:class="playerMode === 'hls' ? 'bg-yellow-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
:disabled="!canUseHls" @click="selectPlayerMode('hls')">
|
||||
HLS
|
||||
</button>
|
||||
@@ -970,13 +1065,22 @@ useHead(() => ({
|
||||
<div class="no-scrollbar flex max-w-full gap-1 overflow-x-auto">
|
||||
<button v-for="(server, index) in servers" :key="`${server.name}-${index}-player`" type="button"
|
||||
class="shrink-0 rounded px-3 py-1.5 transition"
|
||||
:class="selectedServer === index ? 'bg-emerald-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
:class="selectedServer === index ? 'bg-yellow-400 text-slate-950' : 'text-slate-200 hover:bg-white/10'"
|
||||
@click="selectServer(index)">
|
||||
{{ serverLabel(server, index) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pending"
|
||||
class="pointer-events-none absolute inset-0 z-30 grid place-items-center bg-black/45 text-yellow-200 backdrop-blur-[1px]">
|
||||
<div
|
||||
class="inline-flex items-center gap-3 rounded-full border border-yellow-300/25 bg-black/70 px-4 py-2 text-xs font-black shadow-xl shadow-black/30">
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
Đang tải phim
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<iframe v-if="playerMode === 'embed' && playerUrl && hasStarted" :src="playerUrl"
|
||||
:title="`${movie.name} - ${activeEpisode?.name || 'Tập phim'}`" class="h-full w-full"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
@@ -986,16 +1090,17 @@ useHead(() => ({
|
||||
@mouseleave="controlsVisible = false" @touchstart.passive="handleTouchStart"
|
||||
@touchend.passive="handleTouchEnd">
|
||||
<video ref="videoRef" class="h-full w-full bg-black object-contain" playsinline
|
||||
@loadedmetadata="updateVideoMetadata" @timeupdate="updateVideoTime"
|
||||
@canplay="handleVideoCanPlay" @waiting="handleVideoWaiting"
|
||||
@playing="handleVideoPlaying" @play="isVideoPlaying = true; showControlsTemporarily()"
|
||||
@loadedmetadata="updateVideoMetadata" @timeupdate="updateVideoTime" @canplay="handleVideoCanPlay"
|
||||
@waiting="handleVideoWaiting" @playing="handleVideoPlaying"
|
||||
@play="isVideoPlaying = true; showControlsTemporarily()"
|
||||
@pause="isVideoPlaying = false; controlsVisible = true" @ended="handleVideoEnded"
|
||||
@error="handleVideoError" />
|
||||
|
||||
<div class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/70 via-transparent to-black/35" />
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 bg-linear-to-t from-black/70 via-transparent to-black/35" />
|
||||
|
||||
<div v-if="isVideoBuffering"
|
||||
class="pointer-events-none absolute inset-0 z-10 grid place-items-center bg-black/20 text-emerald-200">
|
||||
class="pointer-events-none absolute inset-0 z-10 grid place-items-center bg-black/20 text-yellow-200">
|
||||
<Loader2 class="size-10 animate-spin" />
|
||||
</div>
|
||||
|
||||
@@ -1005,13 +1110,14 @@ useHead(() => ({
|
||||
</div>
|
||||
|
||||
<button v-if="videoCurrentTime < skipIntroSeconds && durationSeconds > 180" type="button"
|
||||
class="absolute bottom-24 right-4 z-20 rounded-md bg-emerald-400 px-4 py-2 text-xs font-black text-slate-950 shadow-lg transition hover:bg-white"
|
||||
class="absolute bottom-24 right-4 z-20 rounded-md border border-yellow-300/80 bg-black/55 px-4 py-2 text-xs font-black text-yellow-200 shadow-lg shadow-black/30 backdrop-blur transition hover:border-yellow-200 hover:bg-yellow-400 hover:text-slate-950"
|
||||
@click="skipIntro">
|
||||
Bỏ intro
|
||||
</button>
|
||||
|
||||
<button v-if="skipOutroSeconds && videoCurrentTime > skipOutroSeconds - 45 && hasNextEpisode" type="button"
|
||||
class="absolute bottom-24 right-4 z-20 inline-flex items-center gap-2 rounded-md bg-emerald-400 px-4 py-2 text-xs font-black text-slate-950 shadow-lg transition hover:bg-white"
|
||||
<button v-if="skipOutroSeconds && videoCurrentTime > skipOutroSeconds - 45 && hasNextEpisode"
|
||||
type="button"
|
||||
class="absolute bottom-24 right-4 z-20 inline-flex items-center gap-2 rounded-md border border-yellow-300/80 bg-black/55 px-4 py-2 text-xs font-black text-yellow-200 shadow-lg shadow-black/30 backdrop-blur transition hover:border-yellow-200 hover:bg-yellow-400 hover:text-slate-950"
|
||||
@click="playNextEpisode(true)">
|
||||
<SkipForward class="size-4" /> Tập tiếp
|
||||
</button>
|
||||
@@ -1023,7 +1129,7 @@ useHead(() => ({
|
||||
@pointerdown="handlePlayerPointerDown" @pointerup="stopHoldSpeed" @pointercancel="stopHoldSpeed"
|
||||
@pointerleave="stopHoldSpeed" @contextmenu.prevent>
|
||||
<span
|
||||
class="grid size-12 place-items-center rounded-full bg-emerald-400 text-slate-950 shadow-xl shadow-emerald-950/30 sm:size-16">
|
||||
class="grid size-12 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-xl shadow-yellow-950/30 sm:size-16">
|
||||
<Pause v-if="isVideoPlaying" class="size-5 fill-current sm:size-7" />
|
||||
<Play v-else class="size-5 fill-current sm:size-7" />
|
||||
</span>
|
||||
@@ -1043,41 +1149,34 @@ useHead(() => ({
|
||||
<span>{{ formatPlayerTime(videoCurrentTime) }}</span>
|
||||
<span>{{ formatPlayerTime(durationSeconds) }}</span>
|
||||
</div>
|
||||
<input class="kr-hls-range" type="range" min="0" :max="durationSeconds || 0"
|
||||
:value="videoCurrentTime" step="1" @input="seekHlsPlayer">
|
||||
<input class="kr-hls-range" type="range" min="0" :max="durationSeconds || 0" :value="videoCurrentTime"
|
||||
step="1" @input="seekHlsPlayer">
|
||||
<div class="kr-hls-actions">
|
||||
<div class="kr-hls-actions-left">
|
||||
<button type="button"
|
||||
class="kr-hls-control-button is-primary"
|
||||
<button type="button" class="kr-hls-control-button is-primary"
|
||||
:aria-label="isVideoPlaying ? 'Tạm dừng' : 'Phát phim'" @click="toggleHlsPlayback">
|
||||
<Pause v-if="isVideoPlaying" class="fill-current" />
|
||||
<Play v-else class="fill-current" />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="kr-hls-control-button"
|
||||
aria-label="Lùi 10 giây" @click="seekBy(-10)">
|
||||
<button type="button" class="kr-hls-control-button" aria-label="Lùi 10 giây" @click="seekBy(-10)">
|
||||
<Rewind />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="kr-hls-control-button"
|
||||
aria-label="Tua 10 giây" @click="seekBy(10)">
|
||||
<button type="button" class="kr-hls-control-button" aria-label="Tua 10 giây" @click="seekBy(10)">
|
||||
<FastForward />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="kr-hls-control-button"
|
||||
:aria-label="isVideoMuted ? 'Bật âm' : 'Tắt âm'" @click="toggleMute">
|
||||
<button type="button" class="kr-hls-control-button" :aria-label="isVideoMuted ? 'Bật âm' : 'Tắt âm'"
|
||||
@click="toggleMute">
|
||||
<VolumeX v-if="isVideoMuted" />
|
||||
<Volume2 v-else />
|
||||
</button>
|
||||
<input class="kr-volume-range hidden sm:block" type="range" min="0" max="1"
|
||||
step="0.05" :value="videoVolume" @input="changeVolume">
|
||||
<input class="kr-volume-range hidden sm:block" type="range" min="0" max="1" step="0.05"
|
||||
:value="videoVolume" @input="changeVolume">
|
||||
</div>
|
||||
|
||||
<div class="kr-hls-actions-right">
|
||||
<div class="relative">
|
||||
<button type="button"
|
||||
class="kr-hls-control-button"
|
||||
:class="isSettingsOpen ? 'bg-emerald-400 text-slate-950' : ''" aria-label="Cài đặt player"
|
||||
<button type="button" class="kr-hls-control-button"
|
||||
:class="isSettingsOpen ? 'bg-yellow-400 text-slate-950' : ''" aria-label="Cài đặt player"
|
||||
@click="isSettingsOpen = !isSettingsOpen; controlsVisible = true">
|
||||
<Settings />
|
||||
</button>
|
||||
@@ -1105,25 +1204,23 @@ useHead(() => ({
|
||||
:value="selectedQuality" :disabled="!qualityLevels.length" aria-label="Chất lượng"
|
||||
@change="changeQuality">
|
||||
<option class="bg-slate-950" value="-1">Auto</option>
|
||||
<option v-for="level in qualityLevels" :key="level.level" class="bg-slate-950" :value="level.level">
|
||||
<option v-for="level in qualityLevels" :key="level.level" class="bg-slate-950"
|
||||
:value="level.level">
|
||||
{{ level.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="kr-hls-control-button kr-hls-desktop-button"
|
||||
<button type="button" class="kr-hls-control-button kr-hls-desktop-button"
|
||||
aria-label="Picture in Picture" @click="togglePictureInPicture">
|
||||
<PictureInPicture2 />
|
||||
</button>
|
||||
<button v-if="hasNextEpisode" type="button"
|
||||
class="kr-hls-control-button kr-hls-desktop-button"
|
||||
<button v-if="hasNextEpisode" type="button" class="kr-hls-control-button kr-hls-desktop-button"
|
||||
aria-label="Tập tiếp theo" @click="playNextEpisode(true)">
|
||||
<SkipForward />
|
||||
</button>
|
||||
<button type="button"
|
||||
class="kr-hls-control-button"
|
||||
aria-label="Toàn màn hình" @click="toggleFullscreen">
|
||||
<button type="button" class="kr-hls-control-button" aria-label="Toàn màn hình"
|
||||
@click="toggleFullscreen">
|
||||
<Maximize />
|
||||
</button>
|
||||
</div>
|
||||
@@ -1136,7 +1233,7 @@ useHead(() => ({
|
||||
<div class="absolute inset-0 bg-black/45" />
|
||||
<div class="absolute inset-0 grid place-items-center">
|
||||
<span
|
||||
class="grid size-16 place-items-center rounded-full bg-emerald-400 text-slate-950 shadow-xl shadow-emerald-950/30">
|
||||
class="grid size-16 place-items-center rounded-full bg-yellow-400 text-slate-950 shadow-xl shadow-yellow-950/30">
|
||||
<Play class="size-7 fill-current" />
|
||||
</span>
|
||||
<span v-if="playerMode === 'hls' && !canUseHls"
|
||||
@@ -1150,25 +1247,26 @@ useHead(() => ({
|
||||
<div
|
||||
class="flex items-center justify-center gap-7 border-t border-white/10 px-4 py-4 text-xs font-bold text-slate-100 sm:justify-start sm:gap-4 sm:py-3">
|
||||
<button type="button"
|
||||
class="inline-flex cursor-pointer items-center gap-2 hover:text-emerald-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:class="isFavoriteMovie ? 'text-emerald-300' : ''" :disabled="actionBusy" @click="toggleFavorite">
|
||||
class="inline-flex cursor-pointer items-center gap-2 hover:text-yellow-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:class="isFavoriteMovie ? 'text-yellow-300' : ''" :disabled="actionBusy" @click="toggleFavorite">
|
||||
<Heart class="size-4 sm:size-3" :class="isFavoriteMovie ? 'fill-current' : ''" />
|
||||
{{ isFavoriteMovie ? 'Đã thích' : 'Yêu thích' }}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="inline-flex cursor-pointer items-center gap-2 hover:text-emerald-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
class="inline-flex cursor-pointer items-center gap-2 hover:text-yellow-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
:disabled="actionBusy" @click="addToWatchLater">
|
||||
<Plus class="size-4 sm:size-3" /> Thêm vào
|
||||
</button>
|
||||
<button type="button" class="inline-flex cursor-pointer items-center gap-2 hover:text-emerald-200"
|
||||
<button type="button" class="inline-flex cursor-pointer items-center gap-2 hover:text-yellow-200"
|
||||
@click="shareMovie">
|
||||
<Share2 class="size-4 sm:size-3" /> Chia sẻ
|
||||
</button>
|
||||
<span v-if="actionMessage" class="hidden text-xs font-bold text-emerald-200 sm:inline">
|
||||
<span v-if="actionMessage" class="hidden text-xs font-bold text-yellow-200 sm:inline">
|
||||
{{ actionMessage }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="actionMessage" class="border-t border-white/10 px-4 pb-3 text-center text-xs font-bold text-emerald-200 sm:hidden">
|
||||
<p v-if="actionMessage"
|
||||
class="border-t border-white/10 px-4 pb-3 text-center text-xs font-bold text-yellow-200 sm:hidden">
|
||||
{{ actionMessage }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -1180,13 +1278,13 @@ useHead(() => ({
|
||||
class="hidden h-28 w-20 rounded-md object-cover sm:block">
|
||||
<div>
|
||||
<h1 class="text-[1.35rem] font-black leading-tight sm:text-2xl">{{ movie.name }}</h1>
|
||||
<p v-if="movie.originName" class="mt-1 text-sm font-bold text-emerald-200">{{ movie.originName }}</p>
|
||||
<p v-if="movie.originName" class="mt-1 text-sm font-bold text-yellow-200">{{ movie.originName }}</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-xs font-black">
|
||||
<span v-if="movie.quality" class="rounded bg-white/12 px-2 py-1">{{ movie.quality }}</span>
|
||||
<span v-if="movie.episode" class="rounded bg-white/12 px-2 py-1">{{ movie.episode }}</span>
|
||||
<span v-if="movie.time" class="rounded bg-white/12 px-2 py-1">{{ movie.time }}</span>
|
||||
<span v-if="movie.rating"
|
||||
class="inline-flex items-center gap-1 rounded bg-emerald-400 px-2 py-1 text-slate-950">
|
||||
class="inline-flex items-center gap-1 rounded bg-yellow-400 px-2 py-1 text-slate-950">
|
||||
<Star class="size-3 fill-current" />
|
||||
{{ movie.rating.toFixed(1) }}
|
||||
</span>
|
||||
@@ -1196,14 +1294,14 @@ useHead(() => ({
|
||||
|
||||
<p v-if="movie.content" class="mt-4 text-sm leading-7 text-slate-200">{{ movie.content }}</p>
|
||||
|
||||
<div class="mt-5 rounded-md bg-emerald-400 px-4 py-3 text-sm font-black text-slate-950">
|
||||
<div class="mt-5 rounded-md bg-yellow-400 px-4 py-3 text-sm font-black text-slate-950">
|
||||
Đang xem {{ formatEpisodeName(activeEpisode?.name, selectedEpisode) }}
|
||||
</div>
|
||||
|
||||
<div class="mt-6 border-t border-white/10 pt-4">
|
||||
<div class="hidden">
|
||||
<div>
|
||||
<p class="text-xs font-black uppercase tracking-wide text-emerald-200">Chá»n server</p>
|
||||
<p class="text-xs font-black uppercase tracking-wide text-yellow-200">Chá»n server</p>
|
||||
<h2 class="mt-1 text-[1.08rem] font-black sm:text-xl">Danh sách táºp</h2>
|
||||
</div>
|
||||
<p class="text-xs font-bold text-slate-400">
|
||||
@@ -1214,7 +1312,7 @@ useHead(() => ({
|
||||
<div v-if="servers.length > 1" class="hidden">
|
||||
<button v-for="(server, index) in servers" :key="`${server.name}-${index}-episode-server`" type="button"
|
||||
class="shrink-0 rounded px-4 py-2 text-sm font-black"
|
||||
:class="selectedServer === index ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'"
|
||||
:class="selectedServer === index ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'"
|
||||
@click="selectServer(index)">
|
||||
{{ serverLabel(server, index) }}
|
||||
</button>
|
||||
@@ -1222,11 +1320,10 @@ useHead(() => ({
|
||||
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div class="inline-flex shrink-0 items-center gap-3 text-white">
|
||||
<Menu class="size-5 text-emerald-300" />
|
||||
<Menu class="size-5 text-yellow-300" />
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 border-r border-white/15 pr-5 text-xl font-black">
|
||||
Phần 1
|
||||
<span class="text-xs text-slate-400">v</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1249,8 +1346,8 @@ useHead(() => ({
|
||||
<div class="hidden shrink-0 items-center gap-2 text-xs font-bold text-slate-300 lg:flex">
|
||||
<span>Rút gọn</span>
|
||||
<span
|
||||
class="relative inline-flex h-5 w-9 items-center rounded-full bg-emerald-400/18 ring-1 ring-emerald-300/35">
|
||||
<span class="ml-auto mr-1 size-3 rounded-full bg-emerald-300" />
|
||||
class="relative inline-flex h-5 w-9 items-center rounded-full bg-yellow-400/18 ring-1 ring-yellow-300/35">
|
||||
<span class="ml-auto mr-1 size-3 rounded-full bg-yellow-300" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1260,7 +1357,7 @@ useHead(() => ({
|
||||
<NuxtLink v-for="(episode, index) in activeServer.episodes" :key="`${episode.name}-${index}`"
|
||||
:to="episodeLink(index)"
|
||||
class="inline-flex h-14 items-center justify-center gap-2 rounded-md px-3 text-center text-sm font-black transition"
|
||||
:class="selectedEpisode === index ? 'bg-emerald-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
:class="selectedEpisode === index ? 'bg-yellow-400 text-slate-950' : 'bg-white/10 text-white hover:bg-white/16'">
|
||||
<Play class="size-3 fill-current" />
|
||||
{{ formatEpisodeName(episode.name, index) }}
|
||||
</NuxtLink>
|
||||
@@ -1285,11 +1382,36 @@ useHead(() => ({
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</Transition>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.watch-page-enter-active,
|
||||
.watch-page-leave-active {
|
||||
transition: opacity 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.watch-page-enter-from,
|
||||
.watch-page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.kr-loading-line {
|
||||
animation: kr-loading-slide 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes kr-loading-slide {
|
||||
0% {
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(320%);
|
||||
}
|
||||
}
|
||||
|
||||
.kr-hls-controls {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
@@ -1346,7 +1468,7 @@ useHead(() => ({
|
||||
}
|
||||
|
||||
.kr-hls-control-button:hover {
|
||||
color: rgb(52 211 153);
|
||||
color: rgb(250 204 21);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
@@ -1378,8 +1500,7 @@ useHead(() => ({
|
||||
height: 4px;
|
||||
appearance: none;
|
||||
border-radius: 999px;
|
||||
background:
|
||||
linear-gradient(90deg, rgb(52 211 153) v-bind('`${progressPercent}%`'), rgb(255 255 255 / 0.18) 0);
|
||||
background: linear-gradient(90deg, rgb(250 204 21) v-bind('`${progressPercent}%`'), rgb(255 255 255 / 0.18) 0);
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
@@ -1387,8 +1508,7 @@ useHead(() => ({
|
||||
.kr-volume-range {
|
||||
width: 112px;
|
||||
height: 3px;
|
||||
background:
|
||||
linear-gradient(90deg, white v-bind('`${videoVolume * 100}%`'), rgb(255 255 255 / 0.25) 0);
|
||||
background: linear-gradient(90deg, white v-bind('`${videoVolume * 100}%`'), rgb(255 255 255 / 0.25) 0);
|
||||
}
|
||||
|
||||
.kr-hls-range::-webkit-slider-thumb,
|
||||
@@ -1398,7 +1518,7 @@ useHead(() => ({
|
||||
appearance: none;
|
||||
border-radius: 999px;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgb(52 211 153 / 0.22);
|
||||
box-shadow: 0 0 0 3px rgb(250 204 21 / 0.25);
|
||||
}
|
||||
|
||||
.kr-hls-range::-moz-range-thumb,
|
||||
@@ -1408,7 +1528,7 @@ useHead(() => ({
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgb(52 211 153 / 0.22);
|
||||
box-shadow: 0 0 0 3px rgb(250 204 21 / 0.25);
|
||||
}
|
||||
|
||||
.kr-volume-range::-webkit-slider-thumb {
|
||||
@@ -1430,8 +1550,7 @@ useHead(() => ({
|
||||
z-index: 12;
|
||||
height: 4px;
|
||||
pointer-events: none;
|
||||
background:
|
||||
linear-gradient(90deg, rgb(52 211 153) v-bind('`${progressPercent}%`'), rgb(255 255 255 / 0.24) 0);
|
||||
background: linear-gradient(90deg, rgb(250 204 21) v-bind('`${progressPercent}%`'), rgb(255 255 255 / 0.24) 0);
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
|
||||
+14
-8
@@ -32,7 +32,13 @@ watch(user, () => {
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: 'Yêu thích - KR Phim',
|
||||
title: 'Yêu thích - CineK',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Danh sách phim yêu thích của bạn trên CineK, giúp lưu lại các phim Hàn Quốc muốn xem hoặc xem lại nhanh hơn.',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -42,7 +48,7 @@ useHead({
|
||||
|
||||
<section class="mx-auto max-w-390 px-4 pb-16 pt-28 sm:px-6 lg:px-8 lg:pt-32 xl:px-10">
|
||||
<div class="mb-6">
|
||||
<p class="text-sm font-black uppercase text-emerald-300">Thư viện</p>
|
||||
<p class="text-sm font-black uppercase text-yellow-300">Thư viện</p>
|
||||
<h1 class="mt-2 text-3xl font-black sm:text-4xl">Yêu thích</h1>
|
||||
</div>
|
||||
|
||||
@@ -54,13 +60,13 @@ useHead({
|
||||
<NuxtLink v-for="item in favoriteItems" :key="`${item.source}-${item.slug}`" :to="movieLink(item)"
|
||||
class="group block min-w-0">
|
||||
<div
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-emerald-300/60">
|
||||
class="relative aspect-2/3 overflow-hidden rounded-md bg-slate-900 shadow-xl shadow-black/25 ring-1 ring-white/10 transition duration-300 group-hover:-translate-y-1 group-hover:ring-yellow-300/60">
|
||||
<img v-if="item.thumb || item.poster" :src="item.thumb || item.poster" :alt="item.name"
|
||||
class="h-full w-full object-cover transition duration-500 group-hover:scale-105">
|
||||
<div v-else class="grid h-full w-full place-items-center bg-white/8">
|
||||
<Heart class="size-10 text-emerald-300" />
|
||||
<Heart class="size-10 text-yellow-300" />
|
||||
</div>
|
||||
<span class="absolute left-2 top-2 rounded bg-emerald-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
<span class="absolute left-2 top-2 rounded bg-yellow-400 px-2 py-1 text-xs font-black text-slate-950">
|
||||
Yêu thích
|
||||
</span>
|
||||
</div>
|
||||
@@ -71,19 +77,19 @@ useHead({
|
||||
|
||||
<div v-else-if="user"
|
||||
class="flex min-h-80 flex-col items-center justify-center rounded-lg border border-white/10 bg-white/6 p-6 text-center">
|
||||
<Heart class="size-12 text-emerald-300" />
|
||||
<Heart class="size-12 text-yellow-300" />
|
||||
<h2 class="mt-4 text-xl font-black">Chưa có phim yêu thích</h2>
|
||||
<p class="mt-2 max-w-md text-sm leading-6 text-slate-300">
|
||||
Bấm nút Yêu thích ở trang phim để lưu phim vào đây.
|
||||
</p>
|
||||
<NuxtLink to="/phim"
|
||||
class="mt-5 inline-flex h-11 items-center justify-center rounded-md bg-emerald-300 px-5 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
class="mt-5 inline-flex h-11 items-center justify-center rounded-md bg-yellow-300 px-5 text-sm font-black text-slate-950 transition hover:bg-white">
|
||||
Duyệt phim
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded-lg border border-white/10 bg-white/6 p-6">
|
||||
<UserRound class="size-10 text-emerald-300" />
|
||||
<UserRound class="size-10 text-yellow-300" />
|
||||
<h2 class="mt-4 text-xl font-black">Bạn chưa đăng nhập</h2>
|
||||
<p class="mt-2 text-sm leading-6 text-slate-300">
|
||||
Đăng nhập để đồng bộ danh sách yêu thích, hoặc vẫn có thể lưu tạm trên thiết bị này.
|
||||
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
services:
|
||||
kr-phim:
|
||||
cinek:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: kr-phim
|
||||
container_name: cinek
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
|
||||
@@ -4,6 +4,27 @@ import tailwindcss from '@tailwindcss/vite'
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
app: {
|
||||
head: {
|
||||
titleTemplate: (titleChunk) => titleChunk ? `${titleChunk}` : 'CineK - Xem phim Hàn Quốc online',
|
||||
meta: [
|
||||
{ name: 'description', content: 'CineK là website xem phim Hàn Quốc online với phụ đề Vietsub, thuyết minh, lồng tiếng, hình ảnh sắc nét và kho phim được cập nhật thường xuyên.' },
|
||||
{ property: 'og:site_name', content: 'CineK' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:title', content: 'CineK - Xem phim Hàn Quốc online' },
|
||||
{ property: 'og:description', content: 'Xem phim Hàn Quốc online, phim bộ, phim lẻ, show và nội dung mới cập nhật với trải nghiệm xem mượt trên mọi thiết bị.' },
|
||||
{ property: 'og:image', content: '/icon.png' },
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:title', content: 'CineK - Xem phim Hàn Quốc online' },
|
||||
{ name: 'twitter:description', content: 'Kho phim Hàn Quốc online được cập nhật thường xuyên, hỗ trợ Vietsub, thuyết minh và lồng tiếng.' },
|
||||
{ name: 'theme-color', content: '#020617' },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/png', href: '/icon.png' },
|
||||
{ rel: 'apple-touch-icon', href: '/icon.png' },
|
||||
],
|
||||
},
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
supabaseUrl: process.env.SUPABASE_URL,
|
||||
|
||||
Generated
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "kr-phim",
|
||||
"name": "cinek",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kr-phim",
|
||||
"name": "cinek",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.106.0",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "kr-phim",
|
||||
"name": "cinek",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -3,16 +3,12 @@ import { getMovieDetailGroup, parseSourceRefs } from '../../utils/movies'
|
||||
export default defineEventHandler(async (event) => {
|
||||
const slug = getRouterParam(event, 'slug')
|
||||
const query = getQuery(event)
|
||||
const source = query.source
|
||||
|
||||
if (!slug) {
|
||||
throw createError({ statusCode: 400, statusMessage: 'Thiếu slug phim' })
|
||||
}
|
||||
|
||||
const requestedSource = typeof source === 'string' && ['ophim', 'nguonc', 'kkphim'].includes(source)
|
||||
? source as 'ophim' | 'nguonc' | 'kkphim'
|
||||
: 'ophim'
|
||||
const refs = parseSourceRefs(query.sources || query.srcs, requestedSource, slug)
|
||||
const refs = parseSourceRefs(query.sources || query.srcs, query.source, slug)
|
||||
|
||||
return await getMovieDetailGroup(refs)
|
||||
})
|
||||
|
||||
+18
-5
@@ -63,7 +63,8 @@ const OPHIM_IMAGE = 'https://img.ophim.live/uploads/movies/'
|
||||
const NGUONC_BASE = 'https://phim.nguonc.com'
|
||||
const KKPHIM_BASE = 'https://phimapi.com'
|
||||
const KKPHIM_IMAGE = 'https://phimimg.com/'
|
||||
const SOURCE_NAMES: Source[] = ['ophim', 'nguonc', 'kkphim']
|
||||
const SOURCE_NAMES: Source[] = ['nguonc', 'ophim', 'kkphim']
|
||||
const DEFAULT_SOURCE: Source = 'nguonc'
|
||||
|
||||
function toArray<T>(value: T[] | T | undefined | null): T[] {
|
||||
if (!value) return []
|
||||
@@ -353,7 +354,7 @@ async function requestJson<T>(url: string): Promise<T> {
|
||||
return await $fetch<T>(url, {
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'user-agent': 'KR-Phim/1.0',
|
||||
'user-agent': 'CineK/1.0',
|
||||
},
|
||||
retry: 0,
|
||||
timeout: 12000,
|
||||
@@ -542,7 +543,7 @@ export async function getKkphimDetail(slug: string): Promise<MovieDetail> {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSourceRefs(value: unknown, fallbackSource: Source, fallbackSlug: string): MovieSourceRef[] {
|
||||
export function parseSourceRefs(value: unknown, fallbackSource: unknown, fallbackSlug: string): MovieSourceRef[] {
|
||||
const refs = typeof value === 'string'
|
||||
? value.split(',').map((item) => {
|
||||
const [source, ...slugParts] = item.split(':')
|
||||
@@ -551,8 +552,20 @@ export function parseSourceRefs(value: unknown, fallbackSource: Source, fallback
|
||||
}).filter(Boolean) as MovieSourceRef[]
|
||||
: []
|
||||
|
||||
if (refs.length) return refs
|
||||
return [{ source: fallbackSource, slug: fallbackSlug }]
|
||||
const uniqueRefs = refs.filter((ref, index, allRefs) =>
|
||||
index === allRefs.findIndex((item) => item.source === ref.source && item.slug === ref.slug),
|
||||
)
|
||||
|
||||
if (uniqueRefs.length) {
|
||||
const preferredSource = isSource(fallbackSource) ? fallbackSource : DEFAULT_SOURCE
|
||||
return [
|
||||
...uniqueRefs.filter((ref) => ref.source === preferredSource),
|
||||
...uniqueRefs.filter((ref) => ref.source !== preferredSource),
|
||||
]
|
||||
}
|
||||
|
||||
const source = isSource(fallbackSource) ? fallbackSource : DEFAULT_SOURCE
|
||||
return [{ source, slug: fallbackSlug }]
|
||||
}
|
||||
|
||||
export async function getMovieDetailGroup(refs: MovieSourceRef[]): Promise<MovieDetail> {
|
||||
|
||||
Reference in New Issue
Block a user