diff --git a/app/pages/phim/[slug].vue b/app/pages/phim/[slug].vue
index c7389b4..ebd2ade 100644
--- a/app/pages/phim/[slug].vue
+++ b/app/pages/phim/[slug].vue
@@ -3,6 +3,7 @@ import { ChevronDown, Heart, Play, Plus, Share2, Star } from 'lucide-vue-next'
const route = useRoute()
const requestedSource = computed(() => String(route.query.source || 'ophim'))
+const requestedSources = computed(() => typeof route.query.srcs === 'string' ? route.query.srcs : '')
const selectedServer = ref(0)
const movieInfoOpen = ref(false)
const activeTab = ref<'episodes' | 'actors'>('episodes')
@@ -16,29 +17,37 @@ const {
removeFavorite,
saveWatchLater,
} = useMovieLibrary()
-const sourceOptions = [
- { label: 'OPhim', value: 'ophim' },
- { label: 'NguonC', value: 'nguonc' },
- { label: 'KKPhim', value: 'kkphim' },
-]
const { data: movie, pending, error } = await useFetch(() => `/api/movies/${route.params.slug}`, {
query: computed(() => ({
source: requestedSource.value,
+ srcs: requestedSources.value || undefined,
})),
- watch: [requestedSource],
+ watch: [requestedSource, requestedSources],
})
const servers = computed(() => movie.value?.servers ?? [])
const activeServer = computed(() => servers.value[selectedServer.value])
-const activeSource = computed(() => String(movie.value?.source || route.query.source || 'ophim'))
+const activeSource = computed(() => String(activeServer.value?.source || movie.value?.source || route.query.source || 'ophim'))
+const activeSourceSlug = computed(() => String(activeServer.value?.sourceSlug || movie.value?.slug || route.params.slug))
+const activeSourceServerIndex = computed(() => Number(activeServer.value?.sourceServerIndex ?? 0))
+const sourceOptions = computed(() =>
+ (movie.value?.sources || [{ source: activeSource.value, slug: activeSourceSlug.value }])
+ .map((source: any) => ({
+ label: String(source.source || '').toUpperCase(),
+ value: source.source,
+ slug: source.slug,
+ }))
+ .filter((source: any) => source.value && source.slug),
+)
const actors = computed(() => movie.value?.actors ?? [])
const actorSummary = computed(() => actors.value.map((actor: any) => actor.name).filter(Boolean).slice(0, 6).join(', '))
const firstWatchLink = computed(() => ({
- path: `/xem/${route.params.slug}`,
+ path: `/xem/${activeSourceSlug.value}`,
query: {
source: activeSource.value,
- server: selectedServer.value,
+ srcs: requestedSources.value || undefined,
+ server: activeSourceServerIndex.value,
ep: 1,
},
}))
@@ -48,7 +57,7 @@ const libraryItem = computed(() => {
return {
source: activeSource.value,
- slug: String(route.params.slug),
+ slug: activeSourceSlug.value,
name: movie.value.name,
originName: movie.value.originName,
thumb: movie.value.thumb,
@@ -59,22 +68,32 @@ const libraryItem = computed(() => {
function episodeLink(index: number) {
return {
- path: `/xem/${route.params.slug}`,
+ path: `/xem/${activeSourceSlug.value}`,
query: {
source: activeSource.value,
- server: selectedServer.value,
+ srcs: requestedSources.value || undefined,
+ server: activeSourceServerIndex.value,
ep: index + 1,
},
}
}
function sourceLink(source: string) {
+ const ref = sourceOptions.value.find((item: any) => item.value === source)
return {
- path: `/phim/${route.params.slug}`,
- query: { source },
+ path: `/phim/${ref?.slug || route.params.slug}`,
+ query: {
+ source,
+ srcs: requestedSources.value || undefined,
+ },
}
}
+function serverLabel(server: any, index: number) {
+ const label = String(server?.name || '').replace(/^(ophim|nguonc|kkphim)\s*-\s*/i, '').trim()
+ return label || `Server ${index + 1}`
+}
+
function formatEpisodeName(name: string, index: number) {
const label = String(name || index + 1).trim()
return /^\d+$/.test(label) ? `Tập ${label}` : label
@@ -153,6 +172,10 @@ watch(requestedSource, () => {
selectedServer.value = 0
})
+watch(requestedSources, () => {
+ selectedServer.value = 0
+})
+
watch([libraryItem, user], () => {
refreshFavoriteState()
})
@@ -278,7 +301,7 @@ useHead(() => ({
-
+
Nguồn API
({
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'"
@click="selectedServer = index">
- {{ server.name }}
+ {{ serverLabel(server, index) }}
diff --git a/app/pages/phim/index.vue b/app/pages/phim/index.vue
index 7588707..0780cbe 100644
--- a/app/pages/phim/index.vue
+++ b/app/pages/phim/index.vue
@@ -8,7 +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 source = ref(typeof route.query.source === 'string' ? route.query.source : 'all')
const sourceOptions = [
{ label: 'Tất cả API', value: 'all' },
{ label: 'OPhim', value: 'ophim' },
@@ -30,17 +29,12 @@ watch(type, () => {
page.value = 1
})
-watch(source, () => {
- page.value = 1
-})
-
-watch([page, debouncedKeyword, type, source], () => {
+watch([page, debouncedKeyword, type], () => {
router.replace({
query: {
page: page.value > 1 ? page.value : undefined,
q: debouncedKeyword.value || undefined,
type: type.value !== 'all' ? type.value : undefined,
- source: source.value !== 'all' ? source.value : undefined,
},
})
})
@@ -49,7 +43,6 @@ const { data, pending, error } = await useFetch('/api/movies', {
query: computed(() => ({
page: page.value,
keyword: debouncedKeyword.value || undefined,
- source: source.value !== 'all' ? source.value : undefined,
})),
})
@@ -68,6 +61,23 @@ const filteredMovies = computed(() => {
const totalPages = computed(() => Number(data.value?.pagination?.totalPages || data.value?.pagination?.total_pages || 0))
const sourceStatus = computed(() => data.value?.sources ?? [])
+function movieSourcesQuery(movie: any) {
+ return (movie.sources || [{ source: movie.source, slug: movie.slug }])
+ .filter((source: any) => source.source && source.slug)
+ .map((source: any) => `${source.source}:${source.slug}`)
+ .join(',')
+}
+
+function movieLink(movie: any) {
+ return {
+ path: `/phim/${movie.slug}`,
+ query: {
+ source: movie.source,
+ srcs: movieSourcesQuery(movie),
+ },
+ }
+}
+
function nextPage() {
page.value += 1
window.scrollTo({ top: 0, behavior: 'smooth' })
@@ -132,16 +142,7 @@ useHead({
-
-
-
-
-
+
{{ source.name }}: {{ source.ok ? 'sẵn sàng' : 'đang bị chặn' }}
@@ -158,7 +159,7 @@ useHead({
+ :to="movieLink(movie)" class="group">
![]()
{{ movie.name }}
{{ movie.episode || movie.year || movie.quality }}
-
- {{ movie.source }}
+
+ {{ movie.sources.length }} server
diff --git a/app/pages/xem/[slug].vue b/app/pages/xem/[slug].vue
index 3fbd9d3..287154c 100644
--- a/app/pages/xem/[slug].vue
+++ b/app/pages/xem/[slug].vue
@@ -4,6 +4,7 @@ import {
FastForward,
Heart,
Loader2,
+ Menu,
Maximize,
Pause,
PictureInPicture2,
@@ -22,6 +23,7 @@ const route = useRoute()
const selectedServer = ref(Math.max(Number(route.query.server || 0), 0))
const selectedEpisode = ref(Math.max(Number(route.query.ep || 1) - 1, 0))
const hasStarted = ref(false)
+const hasManualPlayerMode = ref(false)
const isFavoriteMovie = ref(false)
const actionMessage = ref('')
const actionBusy = ref(false)
@@ -41,11 +43,24 @@ const qualityLevels = ref<{ label: string, level: number }[]>([])
const isSettingsOpen = ref(false)
const hlsErrorMessage = ref('')
const controlsVisible = ref(true)
+const isHlsFullscreen = ref(false)
+const edgeProgressVisible = ref(false)
+const gestureHint = ref('')
+const isHoldSpeedActive = ref(false)
const pendingResumeSeconds = ref(0)
let controlsHideTimer: ReturnType | undefined
+let edgeProgressTimer: ReturnType | undefined
+let singleTapTimer: ReturnType | undefined
+let holdSpeedTimer: ReturnType | undefined
+let hlsFallbackTimer: ReturnType | undefined
let touchStartX = 0
let touchStartY = 0
let touchStartAt = 0
+let lastTapAt = 0
+let lastTapX = 0
+let holdRestoreRate = 1
+let ignoreNextTap = false
+let hlsFatalRetryCount = 0
let hlsPlayer: Hls | undefined
let progressTimer: ReturnType | undefined
let lastSavedProgress = 0
@@ -61,10 +76,12 @@ const {
saveWatchLater,
} = useMovieLibrary()
-const { data: movie, pending, error } = await useFetch(`/api/movies/${route.params.slug}`, {
- query: {
+const { data: movie, pending, error } = await useFetch(() => `/api/movies/${route.params.slug}`, {
+ query: computed(() => ({
source: route.query.source,
- },
+ srcs: route.query.srcs,
+ })),
+ watch: [() => route.query.source, () => route.query.srcs],
})
const servers = computed(() => movie.value?.servers ?? [])
@@ -82,6 +99,7 @@ const progressPercent = computed(() => {
if (!durationSeconds.value || !progressSeconds.value) return 0
return Math.min(Math.max((progressSeconds.value / durationSeconds.value) * 100, 0), 100)
})
+const shouldShowHlsControls = computed(() => (controlsVisible.value || !isVideoPlaying.value) && !(isHlsFullscreen.value && edgeProgressVisible.value && isVideoPlaying.value))
const skipIntroSeconds = 85
const skipOutroSeconds = computed(() => Math.max((durationSeconds.value || 0) - 85, 0))
const libraryItem = computed(() => {
@@ -98,23 +116,65 @@ const libraryItem = computed(() => {
}
})
+function formatEpisodeName(name?: string, index = 0) {
+ const value = String(name || index + 1).trim()
+ if (!value) return `Tập ${index + 1}`
+ if (/^(tập|tap|episode|ep)\b/i.test(value)) return value
+ return `Tập ${value}`
+}
+
+function serverLabel(server: any, index: number) {
+ const source = String(server?.source || '').toUpperCase()
+ const name = String(server?.name || '').replace(/^(ophim|nguonc|kkphim)\s*-\s*/i, '').trim()
+ const sameSourceServers = servers.value.filter((item: any) => item.source === server?.source)
+ const sourceServerNumber = sameSourceServers.findIndex((item: any) => item === server) + 1
+
+ if (source && sameSourceServers.length > 1) return `${source} #${sourceServerNumber || index + 1}`
+ if (source) return source
+ return name || `Server ${index + 1}`
+}
+
function episodeLink(index: number) {
return {
path: `/xem/${route.params.slug}`,
query: {
source: route.query.source,
+ srcs: route.query.srcs,
server: selectedServer.value,
ep: index + 1,
},
}
}
-function selectServer(index: number) {
+function episodeIndexForServer(serverIndex: number, episodeIndex = selectedEpisode.value) {
+ const episodeCount = servers.value[serverIndex]?.episodes?.length || 0
+ if (!episodeCount) return 0
+ return Math.min(Math.max(episodeIndex, 0), episodeCount - 1)
+}
+
+async function selectServer(index: number) {
+ if (index === selectedServer.value) return
+ const shouldAutoplay = hasStarted.value
+ const nextEpisodeIndex = episodeIndexForServer(index)
selectedServer.value = index
- selectedEpisode.value = 0
+ selectedEpisode.value = nextEpisodeIndex
hasStarted.value = false
+ hasManualPlayerMode.value = false
resetProgressTimer()
destroyHlsPlayer()
+ choosePreferredPlayerMode(true)
+ await navigateTo({
+ path: `/xem/${route.params.slug}`,
+ query: {
+ ...route.query,
+ server: index,
+ ep: nextEpisodeIndex + 1,
+ },
+ }, { replace: true })
+
+ if (shouldAutoplay) {
+ nextTick(() => startPlayer())
+ }
}
async function startPlayer() {
@@ -131,6 +191,7 @@ async function startPlayer() {
function selectPlayerMode(mode: 'embed' | 'hls') {
if (mode === playerMode.value) return
+ hasManualPlayerMode.value = true
playerMode.value = mode
isSettingsOpen.value = false
hasStarted.value = false
@@ -138,6 +199,11 @@ function selectPlayerMode(mode: 'embed' | 'hls') {
destroyHlsPlayer()
}
+function choosePreferredPlayerMode(force = false) {
+ if (!force && hasManualPlayerMode.value) return
+ playerMode.value = canUseHls.value ? 'hls' : 'embed'
+}
+
function parseDurationSeconds(value: string) {
const normalizedValue = value.toLowerCase()
const hourMatch = normalizedValue.match(/(\d+)\s*(?:h|giờ|gio)/)
@@ -158,6 +224,7 @@ function resetProgressTimer() {
hlsErrorMessage.value = ''
pendingResumeSeconds.value = 0
lastSavedProgress = 0
+ clearHlsFallbackTimer()
stopProgressTimer()
}
@@ -233,6 +300,54 @@ function showControlsTemporarily() {
}, 2600)
}
+function showFullscreenProgressOnly() {
+ if (!isHlsFullscreen.value) return
+ edgeProgressVisible.value = true
+ controlsVisible.value = false
+ if (edgeProgressTimer) clearTimeout(edgeProgressTimer)
+ edgeProgressTimer = setTimeout(() => {
+ edgeProgressVisible.value = false
+ }, 1400)
+}
+
+function clearHlsFallbackTimer() {
+ if (!hlsFallbackTimer) return
+ clearTimeout(hlsFallbackTimer)
+ hlsFallbackTimer = undefined
+}
+
+function scheduleHlsFallback(message = 'HLS phản hồi chậm, đang chuyển sang chế độ Nhúng...') {
+ if (!canUseEmbed.value) return
+ clearHlsFallbackTimer()
+ hlsFallbackTimer = setTimeout(() => {
+ fallbackToEmbed(message)
+ }, 12000)
+}
+
+function fallbackToEmbed(message = 'HLS đang lỗi, đã chuyển sang chế độ Nhúng.') {
+ clearHlsFallbackTimer()
+ if (!canUseEmbed.value) {
+ hlsErrorMessage.value = 'HLS đang lỗi và phim này chưa có link Nhúng ở server này.'
+ isVideoBuffering.value = false
+ return
+ }
+
+ hlsErrorMessage.value = ''
+ isVideoBuffering.value = false
+ isVideoPlaying.value = false
+ isSettingsOpen.value = false
+ playerMode.value = 'embed'
+ destroyHlsPlayer()
+ flashActionMessage(message)
+}
+
+function flashGestureHint(message: string) {
+ gestureHint.value = message
+ window.setTimeout(() => {
+ if (gestureHint.value === message) gestureHint.value = ''
+ }, 650)
+}
+
function formatPlayerTime(seconds = 0) {
const totalSeconds = Math.max(Math.floor(seconds), 0)
const hours = Math.floor(totalSeconds / 3600)
@@ -250,6 +365,7 @@ async function setupHlsPlayer(autoplay = false) {
if (!import.meta.client || !videoRef.value || !hlsPlayerUrl.value) return
destroyHlsPlayer()
+ hlsFatalRetryCount = 0
const video = videoRef.value
isVideoBuffering.value = true
@@ -260,9 +376,15 @@ async function setupHlsPlayer(autoplay = false) {
if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = hlsPlayerUrl.value
+ scheduleHlsFallback()
} else {
const { default: HlsPlayer } = await import('hls.js')
+ if (!HlsPlayer.isSupported()) {
+ fallbackToEmbed('Trình duyệt chưa hỗ trợ HLS, đã chuyển sang chế độ Nhúng.')
+ return
+ }
+
if (!HlsPlayer.isSupported()) {
hlsErrorMessage.value = 'Trình duyệt chưa hỗ trợ HLS, bạn dùng chế độ Nhúng nhé.'
return
@@ -273,6 +395,7 @@ async function setupHlsPlayer(autoplay = false) {
lowLatencyMode: true,
})
hlsPlayer.on(HlsPlayer.Events.MANIFEST_PARSED, () => {
+ clearHlsFallbackTimer()
qualityLevels.value = hlsPlayer?.levels
.map((level, index) => ({
label: level.height ? `${level.height}p` : `${Math.round((level.bitrate || 0) / 1000)}kbps`,
@@ -285,6 +408,12 @@ async function setupHlsPlayer(autoplay = false) {
hlsPlayer.on(HlsPlayer.Events.ERROR, (_event, data) => {
if (!data.fatal) return
+ hlsFatalRetryCount += 1
+ if (hlsFatalRetryCount >= 2) {
+ fallbackToEmbed('HLS tải không ổn định, đã chuyển sang chế độ Nhúng.')
+ return
+ }
+
if (data.type === HlsPlayer.ErrorTypes.NETWORK_ERROR) {
hlsErrorMessage.value = 'Mạng đang chập chờn, đang thử tải lại...'
hlsPlayer?.startLoad()
@@ -297,6 +426,7 @@ async function setupHlsPlayer(autoplay = false) {
})
hlsPlayer.loadSource(hlsPlayerUrl.value)
hlsPlayer.attachMedia(video)
+ scheduleHlsFallback()
}
if (autoplay) {
@@ -304,11 +434,14 @@ async function setupHlsPlayer(autoplay = false) {
await video.play()
} catch {
isVideoPlaying.value = false
+ fallbackToEmbed('HLS chưa phát được, đã chuyển sang chế độ Nhúng.')
}
}
}
function destroyHlsPlayer() {
+ clearHlsFallbackTimer()
+
if (hlsPlayer) {
hlsPlayer.destroy()
hlsPlayer = undefined
@@ -334,11 +467,31 @@ function updateVideoTime() {
}
function updateVideoMetadata() {
+ clearHlsFallbackTimer()
updateVideoTime()
applyResumeProgress()
isVideoBuffering.value = false
}
+function handleVideoCanPlay() {
+ clearHlsFallbackTimer()
+ isVideoBuffering.value = false
+}
+
+function handleVideoWaiting() {
+ isVideoBuffering.value = true
+ scheduleHlsFallback('HLS bị đứng quá lâu, đã chuyển sang chế độ Nhúng.')
+}
+
+function handleVideoPlaying() {
+ clearHlsFallbackTimer()
+ isVideoBuffering.value = false
+}
+
+function handleVideoError() {
+ fallbackToEmbed('HLS phát lỗi, đã chuyển sang chế độ Nhúng.')
+}
+
async function loadResumeProgress() {
if (!import.meta.client || !movie.value) return
@@ -380,7 +533,11 @@ function seekBy(seconds: number) {
const nextTime = Math.min(Math.max((videoRef.value.currentTime || 0) + seconds, 0), durationSeconds.value || Number.MAX_SAFE_INTEGER)
videoRef.value.currentTime = nextTime
updateVideoTime()
- showControlsTemporarily()
+ if (isHlsFullscreen.value) {
+ showFullscreenProgressOnly()
+ } else {
+ showControlsTemporarily()
+ }
saveWatchHistory()
}
@@ -389,6 +546,7 @@ function seekHlsPlayer(event: Event) {
const value = Number((event.target as HTMLInputElement).value)
videoRef.value.currentTime = value
updateVideoTime()
+ if (isHlsFullscreen.value) showFullscreenProgressOnly()
saveWatchHistory()
}
@@ -421,14 +579,52 @@ function changeQuality(event: Event) {
showControlsTemporarily()
}
-function toggleFullscreen() {
- const target = videoRef.value?.parentElement
- if (!target) return
- if (document.fullscreenElement) {
- document.exitFullscreen()
- } else {
- target.requestFullscreen()
+async function toggleFullscreen() {
+ if (!import.meta.client || !videoRef.value) return
+
+ const video = videoRef.value as HTMLVideoElement & {
+ webkitEnterFullscreen?: () => void
+ webkitRequestFullscreen?: () => Promise | void
}
+ const target = (playerShellRef.value || video.parentElement || video) as HTMLElement & {
+ webkitRequestFullscreen?: () => Promise | void
+ }
+ const documentWithWebkit = document as Document & {
+ webkitFullscreenElement?: Element | null
+ webkitExitFullscreen?: () => Promise | void
+ }
+
+ try {
+ if (document.fullscreenElement || documentWithWebkit.webkitFullscreenElement) {
+ if (document.exitFullscreen) await document.exitFullscreen()
+ else await documentWithWebkit.webkitExitFullscreen?.()
+ return
+ }
+
+ if (target.requestFullscreen) {
+ await target.requestFullscreen()
+ return
+ }
+
+ if (target.webkitRequestFullscreen) {
+ await target.webkitRequestFullscreen()
+ return
+ }
+
+ video.webkitEnterFullscreen?.()
+ } catch {
+ video.webkitEnterFullscreen?.()
+ }
+}
+
+function updateFullscreenState() {
+ if (!import.meta.client) return
+ const documentWithWebkit = document as Document & {
+ webkitFullscreenElement?: Element | null
+ }
+ const fullscreenElement = document.fullscreenElement || documentWithWebkit.webkitFullscreenElement
+ isHlsFullscreen.value = Boolean(fullscreenElement && playerShellRef.value?.contains(fullscreenElement))
+ if (!isHlsFullscreen.value) edgeProgressVisible.value = false
}
async function togglePictureInPicture() {
@@ -472,6 +668,71 @@ function handleVideoEnded() {
playNextEpisode(true)
}
+function seekByTapPosition(clientX: number) {
+ const rect = playerShellRef.value?.getBoundingClientRect()
+ const centerX = rect ? rect.left + rect.width / 2 : window.innerWidth / 2
+ const seconds = clientX < centerX ? -10 : 10
+ seekBy(seconds)
+ flashGestureHint(seconds > 0 ? '+10s' : '-10s')
+}
+
+function handlePlayerPointerDown() {
+ if (!videoRef.value) return
+ if (holdSpeedTimer) clearTimeout(holdSpeedTimer)
+ holdSpeedTimer = setTimeout(() => {
+ if (!videoRef.value) return
+ holdRestoreRate = videoRef.value.playbackRate || playbackRate.value || 1
+ videoRef.value.playbackRate = 2
+ isHoldSpeedActive.value = true
+ ignoreNextTap = true
+ flashGestureHint('2x')
+ if (isHlsFullscreen.value) showFullscreenProgressOnly()
+ }, 420)
+}
+
+function stopHoldSpeed() {
+ if (holdSpeedTimer) {
+ clearTimeout(holdSpeedTimer)
+ holdSpeedTimer = undefined
+ }
+
+ if (!isHoldSpeedActive.value) return
+ if (videoRef.value) videoRef.value.playbackRate = playbackRate.value || holdRestoreRate || 1
+ isHoldSpeedActive.value = false
+ window.setTimeout(() => {
+ ignoreNextTap = false
+ }, 80)
+}
+
+function handlePlayerTap(event: MouseEvent) {
+ if (ignoreNextTap) {
+ ignoreNextTap = false
+ return
+ }
+
+ const now = Date.now()
+ const tapX = event.clientX
+ const isDoubleTap = now - lastTapAt < 280 && Math.abs(tapX - lastTapX) < 90
+
+ if (isDoubleTap) {
+ if (singleTapTimer) {
+ clearTimeout(singleTapTimer)
+ singleTapTimer = undefined
+ }
+ lastTapAt = 0
+ seekByTapPosition(tapX)
+ return
+ }
+
+ lastTapAt = now
+ lastTapX = tapX
+ if (singleTapTimer) clearTimeout(singleTapTimer)
+ singleTapTimer = setTimeout(() => {
+ toggleHlsPlayback()
+ singleTapTimer = undefined
+ }, 260)
+}
+
function handleTouchStart(event: TouchEvent) {
const touch = event.touches[0]
touchStartX = touch.clientX
@@ -488,6 +749,7 @@ function handleTouchEnd(event: TouchEvent) {
if (Math.abs(deltaX) > 48 && Math.abs(deltaX) > Math.abs(deltaY) && elapsed < 700) {
seekBy(deltaX > 0 ? 10 : -10)
+ flashGestureHint(deltaX > 0 ? '+10s' : '-10s')
return
}
@@ -583,32 +845,43 @@ onMounted(async () => {
await refreshFavoriteState()
document.addEventListener('visibilitychange', handleVisibilityChange)
document.addEventListener('keydown', handleKeyboardShortcut)
+ document.addEventListener('fullscreenchange', updateFullscreenState)
+ document.addEventListener('webkitfullscreenchange', updateFullscreenState)
})
onBeforeUnmount(() => {
if (hasStarted.value) saveWatchHistory()
stopProgressTimer()
if (controlsHideTimer) clearTimeout(controlsHideTimer)
+ if (edgeProgressTimer) clearTimeout(edgeProgressTimer)
+ if (singleTapTimer) clearTimeout(singleTapTimer)
+ if (holdSpeedTimer) clearTimeout(holdSpeedTimer)
+ clearHlsFallbackTimer()
destroyHlsPlayer()
if (import.meta.client) document.removeEventListener('visibilitychange', handleVisibilityChange)
if (import.meta.client) document.removeEventListener('keydown', handleKeyboardShortcut)
+ if (import.meta.client) document.removeEventListener('fullscreenchange', updateFullscreenState)
+ if (import.meta.client) document.removeEventListener('webkitfullscreenchange', updateFullscreenState)
})
watch(() => route.query, () => {
selectedServer.value = Math.max(Number(route.query.server || 0), 0)
selectedEpisode.value = Math.max(Number(route.query.ep || 1) - 1, 0)
hasStarted.value = false
+ hasManualPlayerMode.value = false
resetProgressTimer()
destroyHlsPlayer()
+ choosePreferredPlayerMode(true)
})
watch([movie, activeEpisode], () => {
if (hasStarted.value) saveWatchHistory()
+ hasManualPlayerMode.value = false
+ choosePreferredPlayerMode(true)
})
watch([canUseEmbed, canUseHls], () => {
- if (!canUseHls.value && playerMode.value === 'hls') playerMode.value = 'embed'
- if (!canUseEmbed.value && canUseHls.value) playerMode.value = 'hls'
+ choosePreferredPlayerMode()
}, {
immediate: true,
})
@@ -645,13 +918,14 @@ useHead(() => ({
-
‹ Chi tiết phim
-
+
+
+
+
+
+
+
@@ -681,6 +965,18 @@ useHead(() => ({
+
+
+
+
+
+