mirror of
https://github.com/chenasraf/nextcloud-jukebox.git
synced 2026-05-18 01:39:00 +00:00
feat: update video error/warning message designs
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<div v-if="video" class="video-container">
|
||||
<div v-if="showError" class="error-message">
|
||||
<NcNoteCard v-if="showError" type="error">
|
||||
<h3>Video format not supported in {{ browserName }}</h3>
|
||||
<p>
|
||||
This {{ videoFileExtension.toUpperCase() }} file contains codecs that {{ browserName }} cannot play.
|
||||
@@ -13,21 +13,29 @@
|
||||
</p>
|
||||
<p>You can download the video to play it in a media player like VLC or MPV.</p>
|
||||
<div class="error-actions">
|
||||
<a :href="streamUrl" :download="video.path.split('/').pop()" class="download-button">
|
||||
<NcButton :href="streamUrl" :download="video.path.split('/').pop()" variant="primary" size="large">
|
||||
Download Video
|
||||
</a>
|
||||
<a v-if="isFirefox" :href="currentUrl" class="download-button secondary" target="_blank" rel="noopener">
|
||||
</NcButton>
|
||||
<NcButton v-if="isFirefox" :href="currentUrl" target="_blank" variant="secondary" size="large">
|
||||
Try in Chrome
|
||||
</a>
|
||||
</NcButton>
|
||||
</div>
|
||||
</div>
|
||||
</NcNoteCard>
|
||||
|
||||
<div v-else class="video-wrapper">
|
||||
<video
|
||||
ref="videoElement"
|
||||
class="video-js vjs-default-skin vjs-big-play-centered"
|
||||
:poster="video.thumbnail ?? undefined">
|
||||
</video>
|
||||
<div v-else>
|
||||
<NcNoteCard v-if="videoFileExtension === 'mkv'" type="warning">
|
||||
<p>
|
||||
<strong>Note:</strong> MKV files may have audio compatibility issues in web browsers.
|
||||
If you don't hear sound, the file likely uses an unsupported audio codec (e.g., AC3, DTS).
|
||||
Try downloading the video to play in VLC or MPV for full audio support.
|
||||
</p>
|
||||
</NcNoteCard>
|
||||
|
||||
<div class="video-wrapper">
|
||||
<video ref="videoElement" class="video-js vjs-default-skin vjs-big-play-centered"
|
||||
:poster="video.thumbnail ?? undefined">
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="video-info">
|
||||
@@ -46,337 +54,300 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { axios } from '@/axios'
|
||||
import Page from '@/components/Page.vue'
|
||||
import playback from '@/composables/usePlayback'
|
||||
import type { Video } from '@/models/media'
|
||||
import videojs from 'video.js'
|
||||
import type Player from 'video.js/dist/types/player'
|
||||
import 'video.js/dist/video-js.css'
|
||||
import { defineComponent, ref, onMounted, computed, watch, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { axios } from '@/axios'
|
||||
import Page from '@/components/Page.vue'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import playback from '@/composables/usePlayback'
|
||||
import type { Video } from '@/models/media'
|
||||
import videojs from 'video.js'
|
||||
import type Player from 'video.js/dist/types/player'
|
||||
import 'video.js/dist/video-js.css'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoView',
|
||||
components: {
|
||||
Page,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const video = ref<Video | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
const player = ref<Player | null>(null)
|
||||
const showError = ref(false)
|
||||
const { overwriteQueue, isPlaying, currentTime, setSeek, currentMedia } = playback
|
||||
export default defineComponent({
|
||||
name: 'VideoView',
|
||||
components: {
|
||||
Page,
|
||||
NcNoteCard,
|
||||
NcButton,
|
||||
},
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const video = ref<Video | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
const player = ref<Player | null>(null)
|
||||
const showError = ref(false)
|
||||
const { overwriteQueue, isPlaying, currentTime, setSeek, currentMedia } = playback
|
||||
|
||||
const streamUrl = computed(() => {
|
||||
if (!video.value) return ''
|
||||
return `${axios.defaults.baseURL}/video/${video.value.id}/stream`
|
||||
})
|
||||
const streamUrl = computed(() => {
|
||||
if (!video.value) return ''
|
||||
return `${axios.defaults.baseURL}/video/${video.value.id}/stream`
|
||||
})
|
||||
|
||||
const videoMimeType = computed(() => {
|
||||
if (!video.value?.path) return 'video/mp4'
|
||||
const ext = video.value.path.split('.').pop()?.toLowerCase()
|
||||
const mimeTypes: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
ogg: 'video/ogg',
|
||||
ogv: 'video/ogg',
|
||||
mkv: 'video/webm', // Try webm MIME type as webm is a subset of MKV
|
||||
avi: 'video/x-msvideo',
|
||||
mov: 'video/quicktime',
|
||||
}
|
||||
return mimeTypes[ext || ''] || 'video/mp4'
|
||||
})
|
||||
const videoMimeType = computed(() => {
|
||||
if (!video.value?.path) return 'video/mp4'
|
||||
const ext = video.value.path.split('.').pop()?.toLowerCase()
|
||||
const mimeTypes: Record<string, string> = {
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
ogg: 'video/ogg',
|
||||
ogv: 'video/ogg',
|
||||
mkv: 'video/webm', // Try webm MIME type as webm is a subset of MKV
|
||||
avi: 'video/x-msvideo',
|
||||
mov: 'video/quicktime',
|
||||
}
|
||||
return mimeTypes[ext || ''] || 'video/mp4'
|
||||
})
|
||||
|
||||
const videoFileExtension = computed(() => {
|
||||
if (!video.value?.path) return ''
|
||||
return video.value.path.split('.').pop()?.toLowerCase() || ''
|
||||
})
|
||||
const videoFileExtension = computed(() => {
|
||||
if (!video.value?.path) return ''
|
||||
return video.value.path.split('.').pop()?.toLowerCase() || ''
|
||||
})
|
||||
|
||||
const isFirefox = /Firefox/i.test(navigator.userAgent)
|
||||
const browserName = computed(() => {
|
||||
const ua = navigator.userAgent
|
||||
if (/Firefox/i.test(ua)) return 'Firefox'
|
||||
if (/Chrome/i.test(ua)) return 'Chrome'
|
||||
if (/Safari/i.test(ua)) return 'Safari'
|
||||
if (/Edge/i.test(ua)) return 'Edge'
|
||||
return 'your browser'
|
||||
})
|
||||
const isFirefox = /Firefox/i.test(navigator.userAgent)
|
||||
const browserName = computed(() => {
|
||||
const ua = navigator.userAgent
|
||||
if (/Firefox/i.test(ua)) return 'Firefox'
|
||||
if (/Chrome/i.test(ua)) return 'Chrome'
|
||||
if (/Safari/i.test(ua)) return 'Safari'
|
||||
if (/Edge/i.test(ua)) return 'Edge'
|
||||
return 'your browser'
|
||||
})
|
||||
|
||||
const currentUrl = computed(() => window.location.href)
|
||||
const currentUrl = computed(() => window.location.href)
|
||||
|
||||
let isSyncing = false
|
||||
let isSyncing = false
|
||||
|
||||
onMounted(async () => {
|
||||
const id = decodeURIComponent(route.params.id as string)
|
||||
onMounted(async () => {
|
||||
const id = decodeURIComponent(route.params.id as string)
|
||||
|
||||
try {
|
||||
const res = await axios.get(`/video/${id}`)
|
||||
video.value = res.data
|
||||
isLoading.value = false
|
||||
try {
|
||||
const res = await axios.get(`/video/${id}`)
|
||||
video.value = res.data
|
||||
isLoading.value = false
|
||||
|
||||
// Add video to queue
|
||||
if (video.value) {
|
||||
overwriteQueue([{ type: 'video', ...video.value }], 0)
|
||||
// Add video to queue
|
||||
if (video.value) {
|
||||
overwriteQueue([{ type: 'video', ...video.value }], 0)
|
||||
|
||||
// Wait for DOM to update before initializing video.js
|
||||
await nextTick()
|
||||
// Wait for DOM to update before initializing video.js
|
||||
await nextTick()
|
||||
|
||||
// Initialize video.js player
|
||||
if (videoElement.value) {
|
||||
console.log('Initializing video.js player')
|
||||
// Initialize video.js player
|
||||
if (videoElement.value) {
|
||||
console.log('Initializing video.js player')
|
||||
|
||||
player.value = videojs(videoElement.value, {
|
||||
controls: true,
|
||||
responsive: true,
|
||||
aspectRatio: '16:9',
|
||||
preload: 'auto',
|
||||
html5: {
|
||||
vhs: {
|
||||
overrideNative: true,
|
||||
},
|
||||
nativeAudioTracks: false,
|
||||
nativeVideoTracks: false,
|
||||
player.value = videojs(videoElement.value, {
|
||||
controls: true,
|
||||
responsive: true,
|
||||
aspectRatio: '16:9',
|
||||
preload: 'auto',
|
||||
html5: {
|
||||
vhs: {
|
||||
overrideNative: true,
|
||||
},
|
||||
})
|
||||
nativeAudioTracks: false,
|
||||
nativeVideoTracks: false,
|
||||
},
|
||||
})
|
||||
|
||||
console.log('Video.js player initialized:', player.value)
|
||||
console.log('Video.js player initialized:', player.value)
|
||||
|
||||
// Set the source
|
||||
player.value.src({
|
||||
src: streamUrl.value,
|
||||
type: videoMimeType.value,
|
||||
})
|
||||
// Set the source
|
||||
player.value.src({
|
||||
src: streamUrl.value,
|
||||
type: videoMimeType.value,
|
||||
})
|
||||
|
||||
console.log('Video source set:', streamUrl.value, videoMimeType.value)
|
||||
console.log('Video source set:', streamUrl.value, videoMimeType.value)
|
||||
|
||||
// Event listeners
|
||||
player.value.on('play', () => {
|
||||
console.log('Video play event')
|
||||
if (!isPlaying.value && !isSyncing) {
|
||||
isSyncing = true
|
||||
playback.togglePlay()
|
||||
setTimeout(() => { isSyncing = false }, 100)
|
||||
// Event listeners
|
||||
player.value.on('play', () => {
|
||||
console.log('Video play event')
|
||||
if (!isPlaying.value && !isSyncing) {
|
||||
isSyncing = true
|
||||
playback.togglePlay()
|
||||
setTimeout(() => { isSyncing = false }, 100)
|
||||
}
|
||||
})
|
||||
|
||||
player.value.on('pause', () => {
|
||||
console.log('Video pause event')
|
||||
if (isPlaying.value && !isSyncing) {
|
||||
isSyncing = true
|
||||
playback.togglePlay()
|
||||
setTimeout(() => { isSyncing = false }, 100)
|
||||
}
|
||||
})
|
||||
|
||||
player.value.on('ended', () => {
|
||||
console.log('Video ended event')
|
||||
playback.next()
|
||||
})
|
||||
|
||||
player.value.on('timeupdate', () => {
|
||||
if (!isSyncing && player.value) {
|
||||
const time = player.value.currentTime()
|
||||
if (time !== undefined && Math.abs(currentTime.value - time) > 0.5) {
|
||||
setSeek(time)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
player.value.on('pause', () => {
|
||||
console.log('Video pause event')
|
||||
if (isPlaying.value && !isSyncing) {
|
||||
isSyncing = true
|
||||
playback.togglePlay()
|
||||
setTimeout(() => { isSyncing = false }, 100)
|
||||
}
|
||||
})
|
||||
player.value.on('error', () => {
|
||||
const error = player.value?.error()
|
||||
console.error('Video.js error:', error)
|
||||
|
||||
player.value.on('ended', () => {
|
||||
console.log('Video ended event')
|
||||
playback.next()
|
||||
})
|
||||
// Check for unsupported media errors
|
||||
if (error && (error.code === 4 || error.code === 3)) {
|
||||
// MEDIA_ERR_SRC_NOT_SUPPORTED (4) or MEDIA_ERR_DECODE (3)
|
||||
console.log('Media format not supported, showing error message')
|
||||
showError.value = true
|
||||
}
|
||||
})
|
||||
|
||||
player.value.on('timeupdate', () => {
|
||||
if (!isSyncing && player.value) {
|
||||
const time = player.value.currentTime()
|
||||
if (time !== undefined && Math.abs(currentTime.value - time) > 0.5) {
|
||||
setSeek(time)
|
||||
}
|
||||
}
|
||||
})
|
||||
player.value.on('loadedmetadata', () => {
|
||||
console.log('Video metadata loaded')
|
||||
})
|
||||
|
||||
player.value.on('error', () => {
|
||||
const error = player.value?.error()
|
||||
console.error('Video.js error:', error)
|
||||
|
||||
// Check for unsupported media errors
|
||||
if (error && (error.code === 4 || error.code === 3)) {
|
||||
// MEDIA_ERR_SRC_NOT_SUPPORTED (4) or MEDIA_ERR_DECODE (3)
|
||||
console.log('Media format not supported, showing error message')
|
||||
showError.value = true
|
||||
}
|
||||
})
|
||||
|
||||
player.value.on('loadedmetadata', () => {
|
||||
console.log('Video metadata loaded')
|
||||
})
|
||||
|
||||
// Auto-play
|
||||
player.value.ready(() => {
|
||||
console.log('Video.js ready, attempting auto-play')
|
||||
player.value?.play().catch((err) => console.error('Auto-play failed:', err))
|
||||
})
|
||||
} else {
|
||||
console.error('Video element not found')
|
||||
}
|
||||
// Auto-play
|
||||
player.value.ready(() => {
|
||||
console.log('Video.js ready, attempting auto-play')
|
||||
if (player.value) {
|
||||
player.value.play().catch((err) => console.error('Auto-play failed:', err))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('Video element not found')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load video:', err)
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// Sync video.js player with playback state
|
||||
watch(isPlaying, (playing) => {
|
||||
if (!player.value || isSyncing) return
|
||||
if (playing && player.value.paused()) {
|
||||
player.value.play().catch((err) => console.error('Video play failed:', err))
|
||||
} else if (!playing && !player.value.paused()) {
|
||||
player.value.pause()
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
// Sync video currentTime when seeking from external controls
|
||||
let lastExternalSeek = 0
|
||||
watch(currentTime, (time) => {
|
||||
if (!player.value || isSyncing) return
|
||||
const currentPlayerTime = player.value.currentTime()
|
||||
if (currentPlayerTime === undefined) return
|
||||
const diff = Math.abs(currentPlayerTime - time)
|
||||
// Only sync if difference is significant (more than 1 second) to avoid feedback loops
|
||||
if (diff > 1 && Date.now() - lastExternalSeek > 500) {
|
||||
player.value.currentTime(time)
|
||||
lastExternalSeek = Date.now()
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
// When current media changes away from this video, pause it
|
||||
watch(currentMedia, (media) => {
|
||||
if (!player.value) return
|
||||
if (!media || media.type !== 'video' || media.id !== video.value?.id) {
|
||||
player.value.pause()
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
} catch (err) {
|
||||
console.error('Failed to load video:', err)
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Dispose video.js player
|
||||
if (player.value) {
|
||||
player.value.dispose()
|
||||
player.value = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
video,
|
||||
isLoading,
|
||||
streamUrl,
|
||||
videoMimeType,
|
||||
videoFileExtension,
|
||||
videoElement,
|
||||
showError,
|
||||
isFirefox,
|
||||
browserName,
|
||||
currentUrl,
|
||||
formatDuration,
|
||||
// Sync video.js player with playback state
|
||||
watch(isPlaying, (playing) => {
|
||||
if (!player.value || isSyncing) return
|
||||
const p = player.value
|
||||
if (playing && p.paused()) {
|
||||
p.play().catch((err) => console.error('Video play failed:', err))
|
||||
} else if (!playing && !p.paused()) {
|
||||
p.pause()
|
||||
}
|
||||
},
|
||||
})
|
||||
}, { flush: 'post' })
|
||||
|
||||
// Sync video currentTime when seeking from external controls
|
||||
let lastExternalSeek = 0
|
||||
watch(currentTime, (time) => {
|
||||
if (!player.value || isSyncing) return
|
||||
const currentPlayerTime = player.value.currentTime()
|
||||
if (currentPlayerTime === undefined) return
|
||||
const diff = Math.abs(currentPlayerTime - time)
|
||||
// Only sync if difference is significant (more than 1 second) to avoid feedback loops
|
||||
if (diff > 1 && Date.now() - lastExternalSeek > 500) {
|
||||
player.value.currentTime(time)
|
||||
lastExternalSeek = Date.now()
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
// When current media changes away from this video, pause it
|
||||
watch(currentMedia, (media) => {
|
||||
if (!player.value) return
|
||||
if (!media || media.type !== 'video' || media.id !== video.value?.id) {
|
||||
player.value.pause()
|
||||
}
|
||||
}, { flush: 'post' })
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
const secs = seconds % 60
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
// Dispose video.js player
|
||||
if (player.value) {
|
||||
player.value.dispose()
|
||||
player.value = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
video,
|
||||
isLoading,
|
||||
streamUrl,
|
||||
videoMimeType,
|
||||
videoFileExtension,
|
||||
videoElement,
|
||||
showError,
|
||||
isFirefox,
|
||||
browserName,
|
||||
currentUrl,
|
||||
formatDuration,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.video-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
.video-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
|
||||
.video-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
.video-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
background-color: #000;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
/* justify-content: center; */
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 1.5rem;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
padding: 2rem;
|
||||
background-color: var(--color-background-dark);
|
||||
border: 2px solid var(--color-error);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-maxcontrast);
|
||||
|
||||
h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--color-error);
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.download-button {
|
||||
display: inline-block;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-element-light);
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background-color: var(--color-background-dark);
|
||||
border: 2px solid var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-info {
|
||||
margin-top: 1.5rem;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-maxcontrast);
|
||||
|
||||
span:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin-left: 1rem;
|
||||
}
|
||||
span:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// Global video.js styles (not scoped)
|
||||
.video-js {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
// Global video.js styles (not scoped)
|
||||
.video-js {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@ export default createAppConfig(
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('node_modules')) {
|
||||
if (id.includes('vue-material-design-icons')) return 'icons'
|
||||
if (id.includes('@nextcloud/dialogs')) return 'nextcloud-dialogs'
|
||||
if (id.includes('@nextcloud/vue')) return 'nextcloud-vue'
|
||||
if (id.includes('vue')) return 'vue'
|
||||
|
||||
Reference in New Issue
Block a user