- Caricamento Video
This commit is contained in:
@@ -74,6 +74,8 @@ export default defineConfig((ctx) => {
|
||||
'@paths': path.resolve(__dirname, 'src/store/Api/ApiRoutes.ts'),
|
||||
'@images': path.resolve(__dirname, 'src/assets/images'),
|
||||
'@icons': path.resolve(__dirname, 'src/public/myicons'),
|
||||
'@types': path.resolve(__dirname, 'src/types'),
|
||||
'@services': path.resolve(__dirname, 'src/services'),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ const msg_website_it = {
|
||||
Ammetti: 'Ammetti',
|
||||
AbilitaCircuito: 'Abilita Circuito',
|
||||
installaApp: 'Installa App',
|
||||
VideoPage: 'Video',
|
||||
fundraising: 'Sostieni il Progetto',
|
||||
notifs: 'Configura le Notifiche',
|
||||
unsubscribe: 'Disiscriviti',
|
||||
@@ -88,6 +89,7 @@ const msg_website_it = {
|
||||
eventodef: 'Evento:',
|
||||
prova: 'prova',
|
||||
dbop: 'Operazioni',
|
||||
VideoPage: 'Video',
|
||||
dbopmacro: 'Operazioni Macro',
|
||||
projall: 'Comunitari',
|
||||
groups: 'Lista Gruppi',
|
||||
|
||||
150
src/components/video/VideoGallery.scss
Normal file
150
src/components/video/VideoGallery.scss
Normal file
@@ -0,0 +1,150 @@
|
||||
.video-gallery {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
|
||||
.breadcrumb-section {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Folder Cards
|
||||
.folder-card {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// Video Cards
|
||||
.video-card {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
padding-top: 56.25%; // 16:9
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-preview {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.play-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
transition: background 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
// List View
|
||||
.video-list {
|
||||
.video-list-item {
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video-thumbnail {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbnail-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
// Dialogs
|
||||
.video-player-dialog {
|
||||
background: #000;
|
||||
|
||||
.video-player-container {
|
||||
height: calc(100vh - 50px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-move {
|
||||
min-width: 350px;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark Mode
|
||||
.body--dark {
|
||||
.video-gallery {
|
||||
.breadcrumb-section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.video-list-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
src/components/video/VideoGallery.ts
Normal file
291
src/components/video/VideoGallery.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { videoService } from '@/services/videoService';
|
||||
import type { IVideo, IFolder, IFolderOption } from '@/types/video.types';
|
||||
|
||||
interface IBreadcrumb {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoGallery',
|
||||
|
||||
props: {
|
||||
refreshTrigger: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const $q = useQuasar();
|
||||
|
||||
// State
|
||||
const loading = ref(false);
|
||||
const currentPath = ref('');
|
||||
const videos = ref<IVideo[]>([]);
|
||||
const subfolders = ref<IFolder[]>([]);
|
||||
const allFolders = ref<IFolderOption[]>([]);
|
||||
const viewMode = ref<'grid' | 'list'>('grid');
|
||||
|
||||
// Video player
|
||||
const showVideoDialog = ref(false);
|
||||
const currentVideo = ref<IVideo | null>(null);
|
||||
const videoPlayer = ref<HTMLVideoElement | null>(null);
|
||||
|
||||
// Move dialog
|
||||
const showMoveDialog = ref(false);
|
||||
const moveDestination = ref('');
|
||||
const videoToMove = ref<IVideo | null>(null);
|
||||
const moving = ref(false);
|
||||
|
||||
// Computed
|
||||
const breadcrumbs = computed<IBreadcrumb[]>(() => {
|
||||
if (!currentPath.value) return [];
|
||||
|
||||
const parts = currentPath.value.split('/');
|
||||
return parts.map((part, index) => ({
|
||||
name: part,
|
||||
path: parts.slice(0, index + 1).join('/')
|
||||
}));
|
||||
});
|
||||
|
||||
// Methods
|
||||
const loadContent = async (): Promise<void> => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await videoService.getVideos(currentPath.value);
|
||||
videos.value = response.data?.videos || [];
|
||||
subfolders.value = response.data?.folders || [];
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nel caricamento dei contenuti'
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadAllFolders = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await videoService.getFolders();
|
||||
const folders = response.data?.folders || [];
|
||||
allFolders.value = [
|
||||
{ label: 'Root', value: '' },
|
||||
...folders.map(f => ({
|
||||
label: f.path,
|
||||
value: f.path
|
||||
}))
|
||||
];
|
||||
} catch (error) {
|
||||
console.error('Error loading folders:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateTo = (path: string): void => {
|
||||
currentPath.value = path;
|
||||
loadContent();
|
||||
};
|
||||
|
||||
const toggleViewMode = (): void => {
|
||||
viewMode.value = viewMode.value === 'grid' ? 'list' : 'grid';
|
||||
};
|
||||
|
||||
const openVideo = (video: IVideo): void => {
|
||||
currentVideo.value = video;
|
||||
showVideoDialog.value = true;
|
||||
};
|
||||
|
||||
const getVideoUrl = (path: string): string => {
|
||||
return videoService.getVideoUrl(path);
|
||||
};
|
||||
|
||||
const downloadVideo = (video: IVideo): void => {
|
||||
const link = document.createElement('a');
|
||||
link.href = getVideoUrl(video.path);
|
||||
link.download = video.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
const renameVideo = (video: IVideo): void => {
|
||||
$q.dialog({
|
||||
title: 'Rinomina Video',
|
||||
message: 'Inserisci il nuovo nome del file:',
|
||||
prompt: {
|
||||
model: video.filename,
|
||||
type: 'text'
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async (newName: string) => {
|
||||
if (!newName.trim() || newName === video.filename) return;
|
||||
|
||||
try {
|
||||
await videoService.renameVideo(video.folder || currentPath.value, video.filename, newName);
|
||||
$q.notify({ type: 'positive', message: 'Video rinominato!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante la rinomina'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const moveVideo = async (video: IVideo): Promise<void> => {
|
||||
await loadAllFolders();
|
||||
videoToMove.value = video;
|
||||
moveDestination.value = '';
|
||||
showMoveDialog.value = true;
|
||||
};
|
||||
|
||||
const confirmMoveVideo = async (): Promise<void> => {
|
||||
if (!videoToMove.value) return;
|
||||
|
||||
moving.value = true;
|
||||
try {
|
||||
await videoService.moveVideo(
|
||||
videoToMove.value.folder || currentPath.value,
|
||||
videoToMove.value.filename,
|
||||
moveDestination.value
|
||||
);
|
||||
$q.notify({ type: 'positive', message: 'Video spostato!' });
|
||||
showMoveDialog.value = false;
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante lo spostamento'
|
||||
});
|
||||
} finally {
|
||||
moving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteVideo = (video: IVideo): void => {
|
||||
$q.dialog({
|
||||
title: 'Conferma Eliminazione',
|
||||
message: `Sei sicuro di voler eliminare "${video.filename}"?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
color: 'negative'
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await videoService.deleteVideo(video.folder || currentPath.value, video.filename);
|
||||
$q.notify({ type: 'positive', message: 'Video eliminato!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renameFolder = (folder: IFolder): void => {
|
||||
$q.dialog({
|
||||
title: 'Rinomina Cartella',
|
||||
message: 'Inserisci il nuovo nome:',
|
||||
prompt: {
|
||||
model: folder.name,
|
||||
type: 'text'
|
||||
},
|
||||
cancel: true,
|
||||
persistent: true
|
||||
}).onOk(async (newName: string) => {
|
||||
if (!newName.trim() || newName === folder.name) return;
|
||||
|
||||
try {
|
||||
await videoService.renameFolder(folder.path, newName);
|
||||
$q.notify({ type: 'positive', message: 'Cartella rinominata!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante la rinomina'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDeleteFolder = (folder: IFolder): void => {
|
||||
$q.dialog({
|
||||
title: 'Conferma Eliminazione',
|
||||
message: `Sei sicuro di voler eliminare la cartella "${folder.name}" e tutto il suo contenuto?`,
|
||||
cancel: true,
|
||||
persistent: true,
|
||||
color: 'negative'
|
||||
}).onOk(async () => {
|
||||
try {
|
||||
await videoService.deleteFolder(folder.path);
|
||||
$q.notify({ type: 'positive', message: 'Cartella eliminata!' });
|
||||
loadContent();
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore durante l\'eliminazione'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
return videoService.formatFileSize(bytes);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
return videoService.formatDate(dateString);
|
||||
};
|
||||
|
||||
// Watchers
|
||||
watch(() => props.refreshTrigger, () => {
|
||||
loadContent();
|
||||
});
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadContent();
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
loading,
|
||||
currentPath,
|
||||
videos,
|
||||
subfolders,
|
||||
allFolders,
|
||||
viewMode,
|
||||
showVideoDialog,
|
||||
currentVideo,
|
||||
videoPlayer,
|
||||
showMoveDialog,
|
||||
moveDestination,
|
||||
moving,
|
||||
|
||||
// Computed
|
||||
breadcrumbs,
|
||||
|
||||
// Methods
|
||||
loadContent,
|
||||
navigateTo,
|
||||
toggleViewMode,
|
||||
openVideo,
|
||||
getVideoUrl,
|
||||
downloadVideo,
|
||||
renameVideo,
|
||||
moveVideo,
|
||||
confirmMoveVideo,
|
||||
confirmDeleteVideo,
|
||||
renameFolder,
|
||||
confirmDeleteFolder,
|
||||
formatFileSize,
|
||||
formatDate
|
||||
};
|
||||
}
|
||||
});
|
||||
271
src/components/video/VideoGallery.vue
Normal file
271
src/components/video/VideoGallery.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<q-card class="video-gallery">
|
||||
<q-card-section>
|
||||
<div class="row items-center justify-between">
|
||||
<div class="text-h6">
|
||||
<q-icon name="video_library" class="q-mr-sm" />
|
||||
Galleria Video
|
||||
</div>
|
||||
<div class="row q-gutter-sm">
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
:icon="viewMode === 'grid' ? 'view_list' : 'grid_view'"
|
||||
@click="toggleViewMode"
|
||||
>
|
||||
<q-tooltip>Cambia vista</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
icon="refresh"
|
||||
:loading="loading"
|
||||
@click="loadContent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<!-- Breadcrumb -->
|
||||
<q-card-section class="q-py-sm breadcrumb-section">
|
||||
<q-breadcrumbs>
|
||||
<q-breadcrumbs-el
|
||||
icon="home"
|
||||
label="Root"
|
||||
class="cursor-pointer"
|
||||
@click="navigateTo('')"
|
||||
/>
|
||||
<q-breadcrumbs-el
|
||||
v-for="(crumb, index) in breadcrumbs"
|
||||
:key="index"
|
||||
:label="crumb.name"
|
||||
class="cursor-pointer"
|
||||
@click="navigateTo(crumb.path)"
|
||||
/>
|
||||
</q-breadcrumbs>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="text-center q-pa-lg">
|
||||
<q-spinner-orbit size="50px" color="primary" />
|
||||
<div class="q-mt-md">Caricamento...</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div v-else>
|
||||
<!-- Folders -->
|
||||
<div v-if="subfolders.length > 0" class="q-mb-lg">
|
||||
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
|
||||
<q-icon name="folder" class="q-mr-xs" />
|
||||
Cartelle
|
||||
</div>
|
||||
<div class="row q-gutter-md">
|
||||
<div
|
||||
v-for="folder in subfolders"
|
||||
:key="folder.path"
|
||||
class="col-6 col-sm-4 col-md-3 col-lg-2"
|
||||
>
|
||||
<q-card
|
||||
class="folder-card cursor-pointer"
|
||||
flat
|
||||
bordered
|
||||
@click="navigateTo(folder.path)"
|
||||
>
|
||||
<q-card-section class="text-center">
|
||||
<q-icon name="folder" size="48px" color="amber" />
|
||||
<div class="text-subtitle2 q-mt-sm ellipsis">
|
||||
{{ folder.name }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
<q-menu touch-position context-menu>
|
||||
<q-list dense>
|
||||
<q-item v-close-popup clickable @click="renameFolder(folder)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="edit" color="primary" />
|
||||
</q-item-section>
|
||||
<q-item-section>Rinomina</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-close-popup clickable @click="confirmDeleteFolder(folder)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete" color="negative" />
|
||||
</q-item-section>
|
||||
<q-item-section>Elimina</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Videos -->
|
||||
<div v-if="videos.length > 0">
|
||||
<div class="text-subtitle1 text-grey-7 q-mb-sm section-title">
|
||||
<q-icon name="movie" class="q-mr-xs" />
|
||||
Video ({{ videos.length }})
|
||||
</div>
|
||||
|
||||
<!-- Grid View -->
|
||||
<div v-if="viewMode === 'grid'" class="row q-gutter-md">
|
||||
<div
|
||||
v-for="video in videos"
|
||||
:key="video.id"
|
||||
class="col-12 col-sm-6 col-md-4 col-lg-3"
|
||||
>
|
||||
<q-card class="video-card">
|
||||
<div class="video-container" @click="openVideo(video)">
|
||||
<video
|
||||
:src="getVideoUrl(video.path)"
|
||||
class="video-preview"
|
||||
preload="metadata"
|
||||
/>
|
||||
<div class="play-overlay">
|
||||
<q-icon name="play_circle" size="64px" color="white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-card-section class="q-py-sm">
|
||||
<div class="text-subtitle2 ellipsis">{{ video.filename }}</div>
|
||||
<div class="text-caption text-grey">
|
||||
{{ formatFileSize(video.size) }} •
|
||||
{{ formatDate(video.createdAt) }}
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions>
|
||||
<q-btn flat round icon="play_arrow" color="primary" @click="openVideo(video)">
|
||||
<q-tooltip>Riproduci</q-tooltip>
|
||||
</q-btn>
|
||||
<q-btn flat round icon="download" color="secondary" @click="downloadVideo(video)">
|
||||
<q-tooltip>Scarica</q-tooltip>
|
||||
</q-btn>
|
||||
<q-space />
|
||||
<q-btn flat round icon="more_vert">
|
||||
<q-menu>
|
||||
<q-list dense>
|
||||
<q-item v-close-popup clickable @click="renameVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="edit" />
|
||||
</q-item-section>
|
||||
<q-item-section>Rinomina</q-item-section>
|
||||
</q-item>
|
||||
<q-item v-close-popup clickable @click="moveVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="drive_file_move" />
|
||||
</q-item-section>
|
||||
<q-item-section>Sposta</q-item-section>
|
||||
</q-item>
|
||||
<q-separator />
|
||||
<q-item v-close-popup clickable @click="confirmDeleteVideo(video)">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="delete" color="negative" />
|
||||
</q-item-section>
|
||||
<q-item-section class="text-negative">Elimina</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-menu>
|
||||
</q-btn>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<q-list v-else separator class="video-list">
|
||||
<q-item v-for="video in videos" :key="video.id" class="video-list-item">
|
||||
<q-item-section avatar>
|
||||
<q-avatar square size="60px" class="video-thumbnail">
|
||||
<video :src="getVideoUrl(video.path)" preload="metadata" />
|
||||
<div class="thumbnail-overlay">
|
||||
<q-icon name="play_arrow" color="white" />
|
||||
</div>
|
||||
</q-avatar>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ video.filename }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ formatFileSize(video.size) }} • {{ formatDate(video.createdAt) }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<div class="row q-gutter-xs">
|
||||
<q-btn flat round dense icon="play_arrow" color="primary" @click="openVideo(video)" />
|
||||
<q-btn flat round dense icon="download" color="secondary" @click="downloadVideo(video)" />
|
||||
<q-btn flat round dense icon="delete" color="negative" @click="confirmDeleteVideo(video)" />
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && videos.length === 0 && subfolders.length === 0" class="empty-state">
|
||||
<q-icon name="folder_off" size="80px" color="grey-5" />
|
||||
<div class="text-h6 text-grey-6 q-mt-md">Nessun contenuto</div>
|
||||
<div class="text-grey-5">
|
||||
Questa cartella è vuota. Carica dei video per iniziare.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<!-- Video Player Dialog -->
|
||||
<q-dialog v-model="showVideoDialog" maximized transition-show="fade" transition-hide="fade">
|
||||
<q-card class="video-player-dialog">
|
||||
<q-bar class="bg-grey-9">
|
||||
<div class="text-white ellipsis">{{ currentVideo?.filename }}</div>
|
||||
<q-space />
|
||||
<q-btn v-close-popup dense flat icon="close" color="white" />
|
||||
</q-bar>
|
||||
|
||||
<q-card-section class="video-player-container">
|
||||
<video
|
||||
v-if="currentVideo"
|
||||
ref="videoPlayer"
|
||||
:src="getVideoUrl(currentVideo.path)"
|
||||
controls
|
||||
autoplay
|
||||
class="video-player"
|
||||
/>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!-- Move Video Dialog -->
|
||||
<q-dialog v-model="showMoveDialog" persistent>
|
||||
<q-card class="dialog-move">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Sposta Video</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-select
|
||||
v-model="moveDestination"
|
||||
:options="allFolders"
|
||||
label="Cartella di destinazione"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat label="Annulla" />
|
||||
<q-btn color="primary" label="Sposta" :loading="moving" @click="confirmMoveVideo" />
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideoGallery.ts" />
|
||||
<style lang="scss" src="./VideoGallery.scss" scoped />
|
||||
73
src/components/video/VideoUploader.scss
Normal file
73
src/components/video/VideoUploader.scss
Normal file
@@ -0,0 +1,73 @@
|
||||
.video-uploader {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
.btn-new-folder {
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
min-height: 120px;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
|
||||
&:deep(.q-field__control) {
|
||||
min-height: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-chip {
|
||||
max-width: 100%;
|
||||
|
||||
.ellipsis {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-queue {
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 8px;
|
||||
|
||||
.q-item {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-new-folder {
|
||||
min-width: 350px;
|
||||
|
||||
@media (max-width: 400px) {
|
||||
min-width: 90vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
.body--dark {
|
||||
.video-uploader {
|
||||
.dropzone {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--q-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-queue {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
|
||||
.q-item {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
199
src/components/video/VideoUploader.ts
Normal file
199
src/components/video/VideoUploader.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { defineComponent, ref, computed, onMounted } from 'vue';
|
||||
import { useQuasar } from 'quasar';
|
||||
import { videoService } from '@/services/videoService';
|
||||
import {
|
||||
IFolder,
|
||||
IFolderOption,
|
||||
IUploadQueueItem,
|
||||
UploadStatus,
|
||||
UPLOAD_STATUS_CONFIG,
|
||||
MAX_FILES
|
||||
} from '@/types/video.types';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideoUploader',
|
||||
|
||||
emits: ['upload-complete'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const $q = useQuasar();
|
||||
|
||||
// State
|
||||
const selectedFiles = ref<File[] | null>(null);
|
||||
const selectedFolder = ref<string>('');
|
||||
const folders = ref<IFolder[]>([]);
|
||||
const loadingFolders = ref(false);
|
||||
const uploadQueue = ref<IUploadQueueItem[]>([]);
|
||||
const isUploading = ref(false);
|
||||
|
||||
// Dialog state
|
||||
const showNewFolderDialog = ref(false);
|
||||
const newFolderName = ref('');
|
||||
const newFolderParent = ref('');
|
||||
const creatingFolder = ref(false);
|
||||
|
||||
// Constants
|
||||
const maxFiles = MAX_FILES;
|
||||
|
||||
// Computed
|
||||
const folderOptions = computed<IFolderOption[]>(() => [
|
||||
{ label: '📁 Root (principale)', value: '' },
|
||||
...folders.value.map(f => ({
|
||||
label: `${' '.repeat((f.level || 1) - 1)}📂 ${f.name}`,
|
||||
value: f.path
|
||||
}))
|
||||
]);
|
||||
|
||||
const parentFolderOptions = computed<IFolderOption[]>(() => [
|
||||
{ label: 'Root', value: '' },
|
||||
...folderOptions.value.slice(1)
|
||||
]);
|
||||
|
||||
// Methods
|
||||
const loadFolders = async (): Promise<void> => {
|
||||
loadingFolders.value = true;
|
||||
try {
|
||||
const response = await videoService.getFolders();
|
||||
folders.value = response.data?.folders || [];
|
||||
} catch (error) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: 'Errore nel caricamento delle cartelle'
|
||||
});
|
||||
} finally {
|
||||
loadingFolders.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onFilesSelected = (files: File[] | null): void => {
|
||||
if (!files) return;
|
||||
|
||||
const fileArray = Array.isArray(files) ? files : [files];
|
||||
uploadQueue.value = fileArray.map(file => ({
|
||||
file,
|
||||
progress: 0,
|
||||
status: 'pending' as UploadStatus
|
||||
}));
|
||||
};
|
||||
|
||||
const startUpload = async (): Promise<void> => {
|
||||
if (uploadQueue.value.length === 0) return;
|
||||
|
||||
isUploading.value = true;
|
||||
let completedCount = 0;
|
||||
|
||||
for (const item of uploadQueue.value) {
|
||||
if (item.status === 'complete') continue;
|
||||
|
||||
item.status = 'uploading';
|
||||
|
||||
try {
|
||||
await videoService.uploadVideo(
|
||||
item.file,
|
||||
selectedFolder.value || 'default',
|
||||
(progress: number) => {
|
||||
item.progress = progress;
|
||||
}
|
||||
);
|
||||
item.status = 'complete';
|
||||
item.progress = 100;
|
||||
completedCount++;
|
||||
} catch (error: any) {
|
||||
item.status = 'error';
|
||||
item.error = error.response?.data?.error || error.message;
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: `Errore upload ${item.file.name}: ${item.error}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isUploading.value = false;
|
||||
|
||||
if (completedCount > 0) {
|
||||
$q.notify({
|
||||
type: 'positive',
|
||||
message: `${completedCount} video caricati con successo!`
|
||||
});
|
||||
emit('upload-complete');
|
||||
}
|
||||
};
|
||||
|
||||
const createNewFolder = async (): Promise<void> => {
|
||||
if (!newFolderName.value.trim()) {
|
||||
$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Inserisci un nome per la cartella'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
creatingFolder.value = true;
|
||||
try {
|
||||
await videoService.createFolder(newFolderName.value, newFolderParent.value);
|
||||
$q.notify({ type: 'positive', message: 'Cartella creata!' });
|
||||
|
||||
await loadFolders();
|
||||
|
||||
selectedFolder.value = newFolderParent.value
|
||||
? `${newFolderParent.value}/${newFolderName.value}`
|
||||
: newFolderName.value;
|
||||
|
||||
showNewFolderDialog.value = false;
|
||||
newFolderName.value = '';
|
||||
newFolderParent.value = '';
|
||||
} catch (error: any) {
|
||||
$q.notify({
|
||||
type: 'negative',
|
||||
message: error.response?.data?.error || 'Errore nella creazione della cartella'
|
||||
});
|
||||
} finally {
|
||||
creatingFolder.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearQueue = (): void => {
|
||||
uploadQueue.value = [];
|
||||
selectedFiles.value = null;
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
return videoService.formatFileSize(bytes);
|
||||
};
|
||||
|
||||
const getStatusConfig = (status: UploadStatus) => {
|
||||
return UPLOAD_STATUS_CONFIG[status];
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
loadFolders();
|
||||
});
|
||||
|
||||
return {
|
||||
// State
|
||||
selectedFiles,
|
||||
selectedFolder,
|
||||
loadingFolders,
|
||||
uploadQueue,
|
||||
isUploading,
|
||||
showNewFolderDialog,
|
||||
newFolderName,
|
||||
newFolderParent,
|
||||
creatingFolder,
|
||||
maxFiles,
|
||||
|
||||
// Computed
|
||||
folderOptions,
|
||||
parentFolderOptions,
|
||||
|
||||
// Methods
|
||||
onFilesSelected,
|
||||
startUpload,
|
||||
createNewFolder,
|
||||
clearQueue,
|
||||
formatFileSize,
|
||||
getStatusConfig
|
||||
};
|
||||
}
|
||||
});
|
||||
181
src/components/video/VideoUploader.vue
Normal file
181
src/components/video/VideoUploader.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<q-card class="video-uploader">
|
||||
<q-card-section>
|
||||
<div class="text-h6">
|
||||
<q-icon name="cloud_upload" class="q-mr-sm" />
|
||||
Carica Video
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-section>
|
||||
<!-- Selezione Cartella -->
|
||||
<div class="row q-gutter-md q-mb-md">
|
||||
<div class="col-12 col-md-8">
|
||||
<q-select
|
||||
v-model="selectedFolder"
|
||||
:options="folderOptions"
|
||||
label="Seleziona Cartella di Destinazione"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
:loading="loadingFolders"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="folder" />
|
||||
</template>
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps">
|
||||
<q-item-section avatar>
|
||||
<q-icon name="folder" color="amber" />
|
||||
</q-item-section>
|
||||
<q-item-section>
|
||||
<q-item-label>{{ scope.opt.label }}</q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</template>
|
||||
</q-select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-4">
|
||||
<q-btn
|
||||
color="secondary"
|
||||
icon="create_new_folder"
|
||||
label="Nuova Cartella"
|
||||
class="full-width btn-new-folder"
|
||||
@click="showNewFolderDialog = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropzone -->
|
||||
<q-file
|
||||
v-model="selectedFiles"
|
||||
label="Trascina i video qui o clicca per selezionare"
|
||||
outlined
|
||||
multiple
|
||||
counter
|
||||
accept="video/*"
|
||||
:max-files="maxFiles"
|
||||
class="dropzone"
|
||||
@update:model-value="onFilesSelected"
|
||||
>
|
||||
<template #prepend>
|
||||
<q-icon name="videocam" />
|
||||
</template>
|
||||
<template #file="{ file }">
|
||||
<q-chip class="full-width q-my-xs file-chip" square>
|
||||
<q-avatar>
|
||||
<q-icon name="movie" />
|
||||
</q-avatar>
|
||||
<div class="ellipsis relative-position">
|
||||
{{ file.name }}
|
||||
<q-tooltip>{{ file.name }}</q-tooltip>
|
||||
</div>
|
||||
<q-chip dense class="q-ml-sm" color="primary" text-color="white">
|
||||
{{ formatFileSize(file.size) }}
|
||||
</q-chip>
|
||||
</q-chip>
|
||||
</template>
|
||||
</q-file>
|
||||
|
||||
<!-- Lista Upload Queue -->
|
||||
<q-list v-if="uploadQueue.length > 0" class="q-mt-md upload-queue">
|
||||
<q-item v-for="(item, index) in uploadQueue" :key="index">
|
||||
<q-item-section avatar>
|
||||
<q-icon
|
||||
:name="getStatusConfig(item.status).icon"
|
||||
:color="getStatusConfig(item.status).color"
|
||||
/>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>
|
||||
<q-item-label>{{ item.file.name }}</q-item-label>
|
||||
<q-item-label caption>
|
||||
{{ formatFileSize(item.file.size) }}
|
||||
</q-item-label>
|
||||
<q-linear-progress
|
||||
v-if="item.status === 'uploading'"
|
||||
:value="item.progress / 100"
|
||||
color="primary"
|
||||
class="q-mt-sm"
|
||||
/>
|
||||
<q-item-label v-if="item.error" caption class="text-negative">
|
||||
{{ item.error }}
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section side>
|
||||
<q-badge
|
||||
:color="getStatusConfig(item.status).color"
|
||||
:label="getStatusConfig(item.status).label"
|
||||
/>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
</q-card-section>
|
||||
|
||||
<q-separator />
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
flat
|
||||
label="Pulisci"
|
||||
color="grey"
|
||||
:disable="isUploading"
|
||||
@click="clearQueue"
|
||||
/>
|
||||
<q-btn
|
||||
color="primary"
|
||||
icon="cloud_upload"
|
||||
label="Carica Video"
|
||||
:loading="isUploading"
|
||||
:disable="uploadQueue.length === 0 || !selectedFolder"
|
||||
@click="startUpload"
|
||||
/>
|
||||
</q-card-actions>
|
||||
|
||||
<!-- Dialog Nuova Cartella -->
|
||||
<q-dialog v-model="showNewFolderDialog" persistent>
|
||||
<q-card class="dialog-new-folder">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Nuova Cartella</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-pt-none">
|
||||
<q-input
|
||||
v-model="newFolderName"
|
||||
label="Nome Cartella"
|
||||
autofocus
|
||||
outlined
|
||||
@keyup.enter="createNewFolder"
|
||||
/>
|
||||
|
||||
<q-select
|
||||
v-model="newFolderParent"
|
||||
:options="parentFolderOptions"
|
||||
label="Cartella Padre (opzionale)"
|
||||
outlined
|
||||
emit-value
|
||||
map-options
|
||||
class="q-mt-md"
|
||||
/>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn v-close-popup flat label="Annulla" />
|
||||
<q-btn
|
||||
color="primary"
|
||||
label="Crea"
|
||||
:loading="creatingFolder"
|
||||
@click="createNewFolder"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideoUploader.ts" />
|
||||
<style lang="scss" src="./VideoUploader.scss" scoped />
|
||||
@@ -85,6 +85,7 @@ const msg_website_it = {
|
||||
eventodef: 'Evento:',
|
||||
prova: 'prova',
|
||||
dbop: 'Operazioni',
|
||||
VideoPage: 'Video',
|
||||
projall: 'Comunitari',
|
||||
groups: 'Lista Gruppi',
|
||||
projectsShared: 'Condivisi da me',
|
||||
|
||||
@@ -250,6 +250,7 @@ export interface IUserFields {
|
||||
made_gift?: boolean
|
||||
tokens?: IToken[]
|
||||
date_reg?: Date
|
||||
date_deleted?: Date
|
||||
lasttimeonline?: Date
|
||||
profile: IUserProfile
|
||||
qualified?: boolean
|
||||
|
||||
16
src/pages/VideosPage.scss
Normal file
16
src/pages/VideosPage.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
.videos-page {
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.body--dark {
|
||||
.videos-page {
|
||||
background: #1d1d1d;
|
||||
}
|
||||
}
|
||||
25
src/pages/VideosPage.ts
Normal file
25
src/pages/VideosPage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import VideoUploader from '@/components/video/VideoUploader.vue';
|
||||
import VideoGallery from '@/components/video/VideoGallery.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'VideosPage',
|
||||
|
||||
components: {
|
||||
VideoUploader,
|
||||
VideoGallery
|
||||
},
|
||||
|
||||
setup() {
|
||||
const refreshTrigger = ref(0);
|
||||
|
||||
const onUploadComplete = (): void => {
|
||||
refreshTrigger.value++;
|
||||
};
|
||||
|
||||
return {
|
||||
refreshTrigger,
|
||||
onUploadComplete
|
||||
};
|
||||
}
|
||||
});
|
||||
11
src/pages/VideosPage.vue
Normal file
11
src/pages/VideosPage.vue
Normal file
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<q-page class="videos-page">
|
||||
<div class="container">
|
||||
<VideoUploader @upload-complete="onUploadComplete" />
|
||||
<VideoGallery :refresh-trigger="refreshTrigger" class="q-mt-lg" />
|
||||
</div>
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./VideosPage.ts" />
|
||||
<style lang="scss" src="./VideosPage.scss" scoped />
|
||||
@@ -196,6 +196,15 @@
|
||||
mykey="Email"
|
||||
:myvalue="myuser.email"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
mykey="Registrata il"
|
||||
:mydate="myuser.date_reg"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
v-if="myuser.date_deleted"
|
||||
mykey="Cancellato il"
|
||||
:mydate="myuser.date_deleted"
|
||||
/>
|
||||
<CKeyAndValue
|
||||
mykey="Email Verificata"
|
||||
:myvalue="myuser.verified_email"
|
||||
|
||||
@@ -43,6 +43,19 @@ function getRoutesAd(site: ISites) {
|
||||
submenu: true,
|
||||
onlyAdmin: true
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 125,
|
||||
path: '/admin/videos',
|
||||
materialIcon: 'fas fa-video',
|
||||
name: 'pages.VideoPage',
|
||||
component: () => import('@/pages/VideosPage.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
inmenu: false,
|
||||
onlyManager: true,
|
||||
onlyAdmin: true,
|
||||
infooter: false,
|
||||
},
|
||||
{
|
||||
active: true,
|
||||
order: 1020,
|
||||
|
||||
167
src/services/videoService.js
Normal file
167
src/services/videoService.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import { Api } from '@/store/Api';
|
||||
|
||||
const BASE_URL = process.env.VITE_MONGODB_HOST;
|
||||
|
||||
export const videoService = {
|
||||
// ============ FOLDER METHODS ============
|
||||
|
||||
async getFolders() {
|
||||
const response = await Api.SendReq('/api/video/folders', 'GET');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async createFolder(folderName, parentPath = '') {
|
||||
const response = await Api.SendReq('/api/video/folders', 'POST', {
|
||||
folderName,
|
||||
parentPath,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async renameFolder(folderPath, newName) {
|
||||
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'PUT', {
|
||||
newName,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteFolder(folderPath) {
|
||||
const response = await Api.SendReq(`/api/video/folders/${folderPath}`, 'DELETE');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ VIDEO METHODS ============
|
||||
|
||||
async getVideos(folder = '') {
|
||||
const response = await Api.SendReq('/api/video/videos', 'GET', { folder });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadVideo(file, folder, onProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('video', file);
|
||||
|
||||
// ✅ Folder come query parameter nell'URL
|
||||
const targetFolder = encodeURIComponent(folder || 'default');
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/upload?folder=${targetFolder}`,
|
||||
'POSTFORMDATA',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
5000,
|
||||
formData,
|
||||
null,
|
||||
{
|
||||
timeout: 600000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total && onProgress) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
onProgress(percentCompleted);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadVideos(files, folder, onProgress) {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => formData.append('videos', file));
|
||||
|
||||
// ✅ Folder come query parameter
|
||||
const targetFolder = encodeURIComponent(folder || 'default');
|
||||
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/upload-multiple?folder=${targetFolder}`,
|
||||
'POSTFORMDATA',
|
||||
null,
|
||||
false,
|
||||
false,
|
||||
1,
|
||||
5000,
|
||||
formData,
|
||||
null,
|
||||
{
|
||||
timeout: 600000,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total && onProgress) {
|
||||
const percentCompleted = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total
|
||||
);
|
||||
onProgress(percentCompleted);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async renameVideo(folder, filename, newFilename) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}/rename`,
|
||||
'PUT',
|
||||
{ newFilename }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async moveVideo(folder, filename, destinationFolder) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}/move`,
|
||||
'PUT',
|
||||
{ destinationFolder }
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async deleteVideo(folder, filename) {
|
||||
const response = await Api.SendReq(
|
||||
`/api/video/videos/${folder}/${filename}`,
|
||||
'DELETE'
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// ============ UTILITY METHODS ============
|
||||
|
||||
getVideoUrl(videoPath) {
|
||||
return `${BASE_URL}${videoPath}`;
|
||||
},
|
||||
|
||||
getStreamUrl(folder, filename) {
|
||||
return `${BASE_URL}/api/video/stream/${folder}/${filename}`;
|
||||
},
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
|
||||
try {
|
||||
return new Date(dateString).toLocaleDateString('it-IT', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return 'N/A';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default videoService;
|
||||
@@ -1,23 +1,36 @@
|
||||
import axios, {
|
||||
AxiosInstance, AxiosPromise, AxiosResponse, AxiosInterceptorManager,
|
||||
} from 'axios'
|
||||
import { Api } from '@api'
|
||||
import type * as Types from '@/store/Api/ApiTypes'
|
||||
|
||||
AxiosInstance,
|
||||
AxiosPromise,
|
||||
AxiosResponse,
|
||||
AxiosInterceptorManager,
|
||||
} from 'axios';
|
||||
import { Api } from '@api';
|
||||
import type * as Types from '@/store/Api/ApiTypes';
|
||||
|
||||
// Funzione che smista la richiesta in base al metodo
|
||||
async function sendRequest(url, method, mydata, myformdata = null, responsedata = null, options = null) {
|
||||
async function sendRequest(
|
||||
url: any,
|
||||
method: string,
|
||||
mydata: any,
|
||||
myformdata: any = null,
|
||||
responsedata: any = null,
|
||||
options: any = null
|
||||
) {
|
||||
const actions = {
|
||||
get: () => Api.get(url, mydata, responsedata),
|
||||
post: () => Api.post(url, mydata, responsedata, options),
|
||||
postformdata: () => Api.postFormData(url, myformdata, responsedata),
|
||||
postformdata: () => Api.postFormData(url, myformdata, responsedata, options), // ✅ Aggiunto options
|
||||
delete: () => Api.Delete(url, mydata, responsedata),
|
||||
put: () => Api.put(url, mydata, responsedata),
|
||||
patch: () => Api.patch(url, mydata, responsedata),
|
||||
};
|
||||
|
||||
const key = method.toLowerCase();
|
||||
if (actions[key]) return await actions[key]();
|
||||
|
||||
if (actions[key]) {
|
||||
return await actions[key]();
|
||||
}
|
||||
|
||||
throw new Error(`Metodo non supportato: ${method}`);
|
||||
}
|
||||
|
||||
export default sendRequest
|
||||
export default sendRequest;
|
||||
|
||||
@@ -145,7 +145,17 @@ async function Request(
|
||||
|
||||
// ✅ AGGIUNGI IL TIMEOUT DALLE OPTIONS
|
||||
if (options?.timeout) {
|
||||
config.timeout = options.timeout; // in millisecondi (es. 300000 = 5 minuti)
|
||||
config.timeout = options.timeout;
|
||||
}
|
||||
|
||||
// ✅ AGGIUNGI SUPPORTO PER onUploadProgress
|
||||
if (options?.onUploadProgress) {
|
||||
config.onUploadProgress = options.onUploadProgress;
|
||||
}
|
||||
|
||||
// ✅ AGGIUNGI SUPPORTO PER onDownloadProgress (opzionale)
|
||||
if (options?.onDownloadProgress) {
|
||||
config.onDownloadProgress = options.onDownloadProgress;
|
||||
}
|
||||
|
||||
if (options?.stream) config.responseType = 'stream';
|
||||
@@ -210,7 +220,6 @@ async function Request(
|
||||
},
|
||||
...responsedata,
|
||||
});*/
|
||||
|
||||
} else if (type === 'postFormData') {
|
||||
response = await axiosInstance.post(path, payload, config);
|
||||
} else {
|
||||
@@ -221,11 +230,9 @@ async function Request(
|
||||
// Gestione aggiornamento token se necessario
|
||||
//const setAuthToken = path === '/updatepwd' || path === '/users/login';
|
||||
const setAuthToken = !!x_auth_token;
|
||||
if (
|
||||
response && setAuthToken
|
||||
) {
|
||||
if (response && setAuthToken) {
|
||||
const refreshToken = String(response.headers['x-refrtok'] || '');
|
||||
const browser_random = userStore.getBrowserRandom()
|
||||
const browser_random = userStore.getBrowserRandom();
|
||||
if (!x_auth_token) {
|
||||
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
|
||||
}
|
||||
@@ -237,7 +244,7 @@ async function Request(
|
||||
userStore.setAuth(x_auth_token, refreshToken, browser_random);
|
||||
localStorage.setItem(toolsext.localStorage.token, x_auth_token);
|
||||
localStorage.setItem(toolsext.localStorage.refreshToken, refreshToken);
|
||||
localStorage.setItem(toolsext.localStorage. browser_random, browser_random);
|
||||
localStorage.setItem(toolsext.localStorage.browser_random, browser_random);
|
||||
}
|
||||
|
||||
globalStore.setStateConnection('online');
|
||||
@@ -245,7 +252,7 @@ async function Request(
|
||||
return new Types.AxiosSuccess(response.data, response.status);
|
||||
} catch (error) {
|
||||
// Aggiornamento asincrono dello stato di connessione (setTimeout per dare tempo a eventuali animazioni)
|
||||
console.error('Errore funzione Request', error)
|
||||
console.error('Errore funzione Request', error);
|
||||
setTimeout(() => {
|
||||
if (['get'].includes(type.toLowerCase())) {
|
||||
globalStore.connData.downloading_server =
|
||||
|
||||
@@ -49,11 +49,11 @@ export const Api = {
|
||||
return await Request('post', path, payload, responsedata, options);
|
||||
},
|
||||
|
||||
async postFormData(path: string, payload?: any, responsedata?: any) {
|
||||
const globalStore = useGlobalStore();
|
||||
globalStore.connData.uploading_server = 1;
|
||||
globalStore.connData.downloading_server = 1;
|
||||
return await Request('postFormData', path, payload, responsedata);
|
||||
async postFormData(path: string, payload?: any, responsedata?: any, options?: any) {
|
||||
const globalStore = useGlobalStore();
|
||||
globalStore.connData.downloading_server = 1;
|
||||
globalStore.connData.uploading_server = 1;
|
||||
return await Request('postFormData', path, payload, responsedata, options);
|
||||
},
|
||||
|
||||
async get(path: string, payload?: any, responsedata?: any) {
|
||||
@@ -241,7 +241,7 @@ export const Api = {
|
||||
|
||||
if (res.status === serv_constants.RIS_CODE__HTTP_INVALID_TOKEN) {
|
||||
userStore.setServerCode(toolsext.ERR_AUTHENTICATION);
|
||||
userStore.setAuth('', '');
|
||||
userStore.setAuth('', '', '');
|
||||
// throw { code: toolsext.ERR_AUTHENTICATION };
|
||||
throw { status: toolsext.ERR_RETRY_LOGIN };
|
||||
}
|
||||
|
||||
100
src/types/video.types.ts
Normal file
100
src/types/video.types.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// ============ INTERFACES ============
|
||||
|
||||
export interface IFolder {
|
||||
name: string;
|
||||
path: string;
|
||||
level?: number;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface IVideo {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName?: string;
|
||||
folder: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mimetype?: string;
|
||||
createdAt: string;
|
||||
modifiedAt?: string;
|
||||
uploadedAt?: string;
|
||||
}
|
||||
|
||||
export interface IFolderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export interface IUploadQueueItem {
|
||||
file: File;
|
||||
progress: number;
|
||||
status: UploadStatus;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IVideoResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
videos?: IVideo[];
|
||||
folders?: IFolder[];
|
||||
video?: IVideo;
|
||||
folder?: IFolder;
|
||||
currentPath?: string;
|
||||
totalVideos?: number;
|
||||
newPath?: string;
|
||||
};
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface IFolderResponse {
|
||||
success: boolean;
|
||||
data?: {
|
||||
folders?: IFolder[];
|
||||
folder?: IFolder;
|
||||
};
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// ============ TYPES ============
|
||||
|
||||
export type UploadStatus = 'pending' | 'uploading' | 'complete' | 'error';
|
||||
|
||||
// ============ CONSTANTS ============
|
||||
|
||||
export const UPLOAD_STATUS_CONFIG = {
|
||||
pending: {
|
||||
icon: 'schedule',
|
||||
color: 'grey',
|
||||
label: 'In attesa'
|
||||
},
|
||||
uploading: {
|
||||
icon: 'cloud_upload',
|
||||
color: 'primary',
|
||||
label: 'Caricamento...'
|
||||
},
|
||||
complete: {
|
||||
icon: 'check_circle',
|
||||
color: 'positive',
|
||||
label: 'Completato'
|
||||
},
|
||||
error: {
|
||||
icon: 'error',
|
||||
color: 'negative',
|
||||
label: 'Errore'
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const ALLOWED_VIDEO_TYPES = [
|
||||
'video/mp4',
|
||||
'video/webm',
|
||||
'video/ogg',
|
||||
'video/quicktime',
|
||||
'video/x-msvideo',
|
||||
'video/x-matroska'
|
||||
];
|
||||
|
||||
export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
|
||||
export const MAX_FILES = 10;
|
||||
@@ -6,7 +6,6 @@
|
||||
"target": "ESNext",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
|
||||
"baseUrl": "./",
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
@@ -88,6 +87,12 @@
|
||||
"@icons": [
|
||||
"src/public/myicons/*"
|
||||
],
|
||||
"@types/*": [
|
||||
"src/types/*"
|
||||
],
|
||||
"@services/*": [
|
||||
"src/services/*"
|
||||
],
|
||||
"@images": [
|
||||
"src/public/images/*"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user