mirror of
https://github.com/ngthanhvu/kr-phim.git
synced 2026-08-10 15:27:46 +00:00
Refactor components for improved type safety and code consistency
- Updated AppHeader.vue to use TypeScript interfaces for member menu items and removed unnecessary type assertions. - Modified AuthModal.vue to enhance styling and improve accessibility with consistent class names and structure. - Enhanced CommentReplies.vue to support deleting replies with a new prop and improved event emission. - Adjusted GifPicker.vue to specify types for fetched data, ensuring better type safety. - Refined HomeMovieCard.vue to handle mouse and focus events more robustly, improving user interaction. - Corrected layout in chinh-sach-bao-mat.vue for better responsiveness and readability. - Updated index.vue to streamline gradient overlays and button positioning for a cleaner UI. - Improved [slug].vue to handle server channels and subtitles more dynamically, enhancing playback experience.
This commit is contained in:
@@ -2,47 +2,53 @@
|
||||
<main class="min-h-screen bg-[#0f111a] text-white">
|
||||
<AppHeader />
|
||||
|
||||
<section class="mx-auto max-w-[800px] px-4 pt-24 pb-16 sm:px-6 lg:px-8 xl:px-10">
|
||||
<section class="mx-auto max-w-200 px-4 pt-24 pb-16 sm:px-6 lg:px-8 xl:px-10">
|
||||
<h1 class="text-3xl font-black text-white mb-6">Chính sách bảo mật</h1>
|
||||
|
||||
<div class="space-y-6 text-sm text-slate-300 leading-relaxed">
|
||||
<div class="rounded-xl border border-white/10 bg-[#191b24] p-6">
|
||||
<h2 class="text-lg font-bold text-white mb-3">1. Thu thập thông tin</h2>
|
||||
<p>
|
||||
CineK chỉ thu thập các thông tin cần thiết để cung cấp dịch vụ: email khi đăng ký tài khoản, lịch sử xem để lưu tiến trình, và danh sách phim yêu thích. Chúng tôi không thu thập thông tin cá nhân nhạy cảm.
|
||||
CineK chỉ thu thập các thông tin cần thiết để cung cấp dịch vụ: email khi đăng ký tài khoản, lịch sử xem để
|
||||
lưu tiến trình, và danh sách phim yêu thích. Chúng tôi không thu thập thông tin cá nhân nhạy cảm.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-[#191b24] p-6">
|
||||
<h2 class="text-lg font-bold text-white mb-3">2. Sử dụng thông tin</h2>
|
||||
<p>
|
||||
Thông tin thu thập được sử dụng để: cung cấp và cải thiện dịch vụ, lưu tiến trình xem, đồng bộ danh sách yêu thích giữa các thiết bị, và gửi thông báo về phim mới (nếu bạn đăng ký).
|
||||
Thông tin thu thập được sử dụng để: cung cấp và cải thiện dịch vụ, lưu tiến trình xem, đồng bộ danh sách yêu
|
||||
thích giữa các thiết bị, và gửi thông báo về phim mới (nếu bạn đăng ký).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-[#191b24] p-6">
|
||||
<h2 class="text-lg font-bold text-white mb-3">3. Chia sẻ thông tin</h2>
|
||||
<p>
|
||||
CineK không bán, chia sẻ hoặc tiết lộ thông tin cá nhân của bạn cho bên thứ ba, trừ khi có yêu cầu từ cơ quan pháp luật có thẩm quyền.
|
||||
CineK không bán, chia sẻ hoặc tiết lộ thông tin cá nhân của bạn cho bên thứ ba, trừ khi có yêu cầu từ cơ
|
||||
quan pháp luật có thẩm quyền.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-[#191b24] p-6">
|
||||
<h2 class="text-lg font-bold text-white mb-3">4. Bảo mật dữ liệu</h2>
|
||||
<p>
|
||||
Chúng tôi sử dụng các biện pháp bảo mật tiêu chuẩn để bảo vệ thông tin người dùng, bao gồm mã hóa mật khẩu và kết nối HTTPS.
|
||||
Chúng tôi sử dụng các biện pháp bảo mật tiêu chuẩn để bảo vệ thông tin người dùng, bao gồm mã hóa mật khẩu
|
||||
và kết nối HTTPS.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-[#191b24] p-6">
|
||||
<h2 class="text-lg font-bold text-white mb-3">5. Cookie</h2>
|
||||
<p>
|
||||
CineK sử dụng cookie để lưu phiên đăng nhập và tùy chọn người dùng. Bạn có thể tắt cookie trong trình duyệt nhưng một số tính năng có thể không hoạt động đúng.
|
||||
CineK sử dụng cookie để lưu phiên đăng nhập và tùy chọn người dùng. Bạn có thể tắt cookie trong trình duyệt
|
||||
nhưng một số tính năng có thể không hoạt động đúng.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-slate-500">
|
||||
Cập nhật lần cuối: Tháng 7, 2026. Nếu có thắc mắc, vui lòng <NuxtLink to="/lien-he" class="text-yellow-300 hover:underline">liên hệ chúng tôi</NuxtLink>.
|
||||
Cập nhật lần cuối: Tháng 7, 2026. Nếu có thắc mắc, vui lòng <NuxtLink to="/lien-he"
|
||||
class="text-yellow-300 hover:underline">liên hệ chúng tôi</NuxtLink>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+5
-6
@@ -313,8 +313,7 @@ useHead({
|
||||
<AppHeader />
|
||||
|
||||
<div v-if="hero" class="relative bg-black mt-16 md:mt-0">
|
||||
<section
|
||||
class="relative w-full aspect-video md:h-screen overflow-hidden bg-black after:content-[''] after:absolute after:bottom-0 after:left-0 after:right-0 after:h-1.5 after:bg-black after:z-10">
|
||||
<section class="relative w-full aspect-video md:h-screen overflow-hidden bg-black">
|
||||
<ClientOnly>
|
||||
<HeroSlider v-if="heroSlides.length" ref="heroSliderRef" :slides="heroSlides"
|
||||
@update:modelValue="heroIndex = $event" />
|
||||
@@ -322,12 +321,12 @@ useHead({
|
||||
|
||||
<!-- Mobile gradient overlay (bottom only) -->
|
||||
<div class="absolute inset-0 z-10 pointer-events-none md:hidden"
|
||||
style="background:linear-gradient(to top, #000000 0%, rgba(0,0,0,0.92) 8%, rgba(0,0,0,0.75) 22%, rgba(0,0,0,0.35) 50%, transparent 78%)">
|
||||
style="background:linear-gradient(to top, #0E111A 0%, rgba(14,17,26,0.95) 8%, rgba(14,17,26,0.78) 22%, rgba(14,17,26,0.38) 50%, transparent 78%)">
|
||||
</div>
|
||||
|
||||
<!-- Desktop gradient overlay (bottom only) -->
|
||||
<div class="absolute inset-0 z-10 pointer-events-none hidden md:block"
|
||||
style="background:linear-gradient(to top, rgba(0,0,0,0.95) 0%, rgba(0,0,0,0.75) 20%, rgba(0,0,0,0.35) 45%, transparent 70%)">
|
||||
style="background:linear-gradient(to top, #0E111A 0%, rgba(14,17,26,0.78) 20%, rgba(14,17,26,0.38) 45%, transparent 70%)">
|
||||
</div>
|
||||
|
||||
<!-- Content overlay -->
|
||||
@@ -675,13 +674,13 @@ useHead({
|
||||
</div>
|
||||
|
||||
<!-- Carousel navigation -->
|
||||
<button type="button" class="absolute -left-[4.25rem] top-1/2 z-20 hidden size-14 -translate-y-1/2 place-items-center
|
||||
<button type="button" class="absolute -left-17 top-1/2 z-20 hidden size-14 -translate-y-1/2 place-items-center
|
||||
rounded-full border border-white/15 bg-[#11141d] text-white shadow-lg
|
||||
transition hover:bg-[#181c27] lg:grid" @click="scrollComments('left')">
|
||||
<AppIcon name="chevron-left" class="size-6" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="absolute -right-[4.25rem] top-1/2 z-20 hidden size-14 -translate-y-1/2 place-items-center
|
||||
<button type="button" class="absolute -right-17 top-1/2 z-20 hidden size-14 -translate-y-1/2 place-items-center
|
||||
rounded-full border border-white/15 bg-[#11141d] text-white shadow-lg
|
||||
transition hover:bg-[#181c27] lg:grid" @click="scrollComments('right')">
|
||||
<AppIcon name="chevron-right" class="size-6" />
|
||||
|
||||
+61
-17
@@ -57,8 +57,51 @@ const libraryItem = computed(() => movie.value ? {
|
||||
|
||||
const servers = computed(() => movie.value?.servers ?? [])
|
||||
const activeServer = computed(() => servers.value[selectedServer.value] ?? servers.value[0])
|
||||
const activeSubtitle = computed(() => activeServer.value?.channels?.[selectedSubtitle.value] ?? activeServer.value)
|
||||
const activeEpisode = computed(() => activeSubtitle.value?.episodes?.[selectedEpisode.value] ?? activeSubtitle.value?.episodes?.[0])
|
||||
// Server may have channels property at runtime
|
||||
const activeSubtitle = computed(() => {
|
||||
const server = (servers.value[selectedServer.value] as any)
|
||||
if (Array.isArray(server?.channels) && server.channels.length > 0) {
|
||||
return server.channels[selectedSubtitle.value] ?? server
|
||||
}
|
||||
return server
|
||||
})
|
||||
const activeEpisode = computed(() => {
|
||||
const sub = activeSubtitle.value as any
|
||||
if (!sub) return undefined
|
||||
const arr = sub?.episodes || []
|
||||
return arr[selectedEpisode.value] || arr[0]
|
||||
})
|
||||
|
||||
// Player/computed properties needed for template
|
||||
const durationSeconds = computed(() => videoRef.value ? Math.floor(videoRef.value.duration || 0) : 0)
|
||||
const hlsPlayerUrl = computed(() => {
|
||||
const ep = activeEpisode.value
|
||||
return ep?.linkM3u8 || ep?.link_embed || ''
|
||||
})
|
||||
const hasNextEpisode = computed(() => {
|
||||
const totalEps = activeSubtitle.value?.episodes?.length || 0
|
||||
return selectedEpisode.value < totalEps - 1
|
||||
})
|
||||
const shouldShowHlsControls = computed(() => controlsVisible.value || isSettingsOpen.value)
|
||||
const episodeProgress = computed(() => {
|
||||
if (!movie.value?.episode || !movie.value?.episodeTotal) return null
|
||||
const match = String(movie.value.episode).match(/(\d+)\s*\/\s*(\d+)/)
|
||||
if (match) return { available: Number(match[1]), total: Number(match[2]) }
|
||||
return null
|
||||
})
|
||||
const progressPercent = computed(() => {
|
||||
if (!durationSeconds.value) return 0
|
||||
return Math.round((progressSeconds.value / durationSeconds.value) * 100)
|
||||
})
|
||||
|
||||
function getProxyUrl(url: string): string {
|
||||
if (!url) return ''
|
||||
return '/api/proxy-m3u8?url=' + encodeURIComponent(url)
|
||||
}
|
||||
|
||||
// Skip intro/outro seconds (set from admin config when available)
|
||||
const skipIntroSeconds = 0
|
||||
const skipOutroSeconds = ref(0)
|
||||
|
||||
|
||||
// Related movies
|
||||
@@ -98,7 +141,8 @@ function episodeLink(index: number) {
|
||||
}
|
||||
|
||||
function episodeIndexForServer(serverIndex: number, subIndex = selectedSubtitle.value, episodeIndex = selectedEpisode.value) {
|
||||
const sub = servers.value[serverIndex]?.channels?.[subIndex] ?? servers.value[serverIndex]
|
||||
const server = (servers.value[serverIndex] as any)
|
||||
const sub = server?.channels?.[subIndex] ?? server
|
||||
const count = sub?.episodes?.length || 0
|
||||
return count ? Math.min(Math.max(episodeIndex, 0), count - 1) : 0
|
||||
}
|
||||
@@ -576,7 +620,7 @@ useHead(() => ({
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-semibold text-white">{{ formatEpisodeName(activeEpisode?.name, selectedEpisode)
|
||||
}}</span>
|
||||
}}</span>
|
||||
<span class="mx-2 text-white/30">•</span>
|
||||
<span class="text-white/60">{{ serverLabel(activeServer, selectedServer) }}</span>
|
||||
</div>
|
||||
@@ -698,9 +742,9 @@ useHead(() => ({
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button v-for="(server, index) in servers" :key="server.name" type="button"
|
||||
class="flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-[13px] font-medium transition-colors"
|
||||
:class="selectedServer === index ? 'border-white/30 bg-white/5 text-white' : 'border-transparent text-white/60 hover:text-white'"
|
||||
@click="selectServer(index)">
|
||||
{{ serverLabel(server, index) }}
|
||||
:class="selectedServer === Number(index) ? 'border-white/30 bg-white/5 text-white' : 'border-transparent text-white/60 hover:text-white'"
|
||||
@click="selectServer(Number(index))">
|
||||
{{ serverLabel(server, Number(index)) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -708,18 +752,18 @@ useHead(() => ({
|
||||
</div>
|
||||
|
||||
<!-- Subtitle/Source Buttons -->
|
||||
<div v-if="activeServer?.channels?.length" class="mb-6 flex flex-wrap gap-2">
|
||||
<button v-for="(sub, index) in activeServer.channels" :key="sub.name || index" type="button"
|
||||
<div v-if="(activeServer as any)?.channels?.length" class="mb-6 flex flex-wrap gap-2">
|
||||
<button v-for="(sub, index) in (activeServer as any)?.channels" :key="sub.name || index" type="button"
|
||||
class="group flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium transition-colors"
|
||||
:class="selectedSubtitle === index ? 'border-white/30 bg-white/5 text-white' : 'border-transparent text-white/60 hover:text-white'"
|
||||
@click="selectSubtitle(index)">
|
||||
<AppIcon v-if="index === 0" name="captions" class="size-3.5" />
|
||||
:class="selectedSubtitle === Number(index) ? 'border-white/30 bg-white/5 text-white' : 'border-transparent text-white/60 hover:text-white'"
|
||||
@click="selectSubtitle(Number(index))">
|
||||
<AppIcon v-if="Number(index) === 0" name="captions" class="size-3.5" />
|
||||
<AppIcon v-else name="languages" class="size-3.5" />
|
||||
|
||||
<span>{{ subtitleLabel(sub, index) }}</span>
|
||||
<span>{{ subtitleLabel(sub, Number(index)) }}</span>
|
||||
|
||||
<span class="rounded px-1.5 text-[10px] font-bold"
|
||||
:class="selectedSubtitle === index ? 'bg-white text-black' : 'bg-white/10 text-white/50'">
|
||||
:class="selectedSubtitle === Number(index) ? 'bg-white text-black' : 'bg-white/10 text-white/50'">
|
||||
{{ sub.episodes?.length || 0 }}
|
||||
</span>
|
||||
</button>
|
||||
@@ -729,11 +773,11 @@ useHead(() => ({
|
||||
<div v-if="activeSubtitle?.episodes?.length"
|
||||
class="grid grid-cols-3 gap-x-2.5 gap-y-2.5 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8">
|
||||
<NuxtLink v-for="(episode, index) in activeSubtitle.episodes" :key="`${episode.name}-${index}`"
|
||||
:to="episodeLink(index)"
|
||||
:to="episodeLink(Number(index))"
|
||||
class="group flex items-center justify-center gap-1.5 rounded-lg bg-[#191b24] px-2 py-2.5 text-[13px] text-white/90 shadow-sm transition-all hover:bg-[#1f2130] hover:text-[#FFD166]"
|
||||
:class="selectedEpisode === index ? 'border border-[#F5C518]/40 bg-[#F5C518]/20 text-[#FFD166]' : ''">
|
||||
:class="selectedEpisode === Number(index) ? 'border border-[#F5C518]/40 bg-[#F5C518]/20 text-[#FFD166]' : ''">
|
||||
<AppIcon name="play" class="size-3 fill-current" />
|
||||
{{ formatEpisodeName(episode.name, index) }}
|
||||
{{ formatEpisodeName(episode.name, Number(index)) }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user