This commit is contained in:
2025-03-13 22:08:06 +01:00
commit bab5571428
93 changed files with 4323 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import 'vite/modulepreload-polyfill'
import App from '@/components/auth/App.vue';
import("@/plugins/vue_loader.js").then(utils => {
utils.createVue(App, "#app");
})

View File

@@ -0,0 +1,8 @@
// add the beginning of your app entry
import 'vite/modulepreload-polyfill'
import App from '../components/torrent/App.vue';
import("@/plugins/vue_loader.js").then(utils => {
utils.createVue(App, "#app");
})

View File

@@ -0,0 +1,49 @@
<template>
<Base>
<v-card class="elevation-12">
<v-toolbar color="primary" dark :flat="true">
<v-toolbar-title>Login form</v-toolbar-title>
<v-spacer/>
</v-toolbar>
<v-card-text class="red--text">
{{error_message}}
</v-card-text>
<v-card-text>
<!-- <v-form @submit.prevent="checkForm" id="check-login-form">-->
<v-form id="check-login-form" method="post" action="/user/login/">
<input type="hidden" :value="Cookies.get('csrftoken')" name="csrfmiddlewaretoken">
<v-text-field id="username" v-model="username" label="Username" name="username" prepend-icon="mdi-account" type="text"/>
<v-text-field id="password" v-model="password" label="Password" name="password" prepend-icon="mdi-lock" type="password"/>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn color="warning" href="/password_reset/" target="_blank" variant="plain">Password lost</v-btn>
<v-spacer/>
<v-btn type="submit" color="primary" form="check-login-form" variant="elevated" class="text-overline">Login</v-btn>
</v-card-actions>
</v-card>
</Base>
</template>
<script setup>
import Cookies from "js-cookie";
import Base from "./Base.vue";
</script>
<script>
export default {
data(){
return {
error_message: "",
username: "",
password: ""
}
},
mounted() {
if(form_error){
this.error_message = "Bad login/password";
console.log(JSON.stringify(form_error));
}
}
}
</script>

View File

@@ -0,0 +1,13 @@
<template>
<v-app id="inspire">
<v-main>
<v-container class="fill-height" :fluid="true">
<v-row align="center" justify="center">
<v-col cols="12" sm="8" md="4">
<slot></slot>
</v-col>
</v-row>
</v-container>
</v-main>
</v-app>
</template>

View File

@@ -0,0 +1,66 @@
<template>
<v-list>
<v-list-item>
<v-list-item-action class="justify-center">
<FriendManager :friends="friends" @friends-updated="fetchFriends"/>
</v-list-item-action>
</v-list-item>
<v-divider/>
<v-list-item
:active="active_user === default_user_id"
@click="$emit('userSelected')"
>
<template v-slot:title>
My torrents
</template>
</v-list-item>
<v-list-item
v-for="(friend, i) in friends"
:key="i"
:active="active_user === friend.id"
@click="$emit('userSelected', friend)"
>
<template v-slot:title>
{{friend.username}}
</template>
<template v-slot:subtitle>
{{friend.count_torrent}} torrent{{friend.count_torrent !== 1 ? 's':''}}
</template>
</v-list-item>
</v-list>
</template>
<script setup>
import FriendManager from "./FriendManager.vue";
</script>
<script>
export default {
emits: ["userSelected"],
props: {
active_user: {
type: Number,
required: false
}
},
data(){
return {
friends: [],
default_user_id: current_user.id
}
},
async mounted(){
await this.fetchFriends();
},
methods: {
async fetchFriends(){
let filters = {
"only_friends": true
}
let response = await fetch(`/api/users/?${new URLSearchParams(filters)}`);
this.friends = await response.json();
},
}
}
</script>

View File

@@ -0,0 +1,132 @@
<template>
<v-card>
<v-card-title class="text-center">Manage friends</v-card-title>
<v-card-text
v-if="add_status"
class="text-center justify-center"
:style="{
'background-color': add_status.success ? 'green': 'red',
'padding-top': '13px'
}"
v-text="add_status.message"
/>
<v-card-actions class="justify-center">
<v-text-field @keyup.enter="addFriendRequest" v-model="username_model" prepend-icon="mdi-account-plus"/>
</v-card-actions>
<v-card v-if="friend_requests.length">
<v-card-title>Friends requests</v-card-title>
<v-list>
<v-list-item
v-for="(fr, i) in friend_requests"
:key="i"
:title="fr.username"
>
<template v-slot:append>
<v-row>
<v-col>
<v-btn icon="mdi-check" color="green" @click="acceptFriendRequest(i)"/>
</v-col>
<v-col>
<v-btn icon="mdi-cancel" color="red" @click="removeFriendRequest(i)"/>
</v-col>
</v-row>
</template>
</v-list-item>
</v-list>
</v-card>
<v-divider/>
<v-card v-if="friends.length">
<v-card-title>Friends</v-card-title>
<v-list>
<v-list-item
v-for="(user, i) in friends"
:key="i"
prepend-icon="mdi-account"
:title="user.username"
>
<template v-slot:append>
<v-btn icon="mdi-minus" color="red" variant="plain" @click="removeFriend(i)"/>
</template>
</v-list-item>
</v-list>
</v-card>
</v-card>
</template>
<script>
import Cookies from "js-cookie";
let initial_add_status = {
message: "",
success: null
}
export default {
emits: ["friendsUpdated"],
props: {
friends: {
type: Array,
required: true
}
},
data(){
return {
enabled: false,
username_model: "",
friend_requests: [],
add_status: null
}
},
mounted() {
this.fetchFriendRequests();
},
methods: {
// friend request related
async fetchFriendRequests(){
let response = await fetch("/api/friend_requests/");
this.friend_requests = await response.json()
},
async addFriendRequest(username=null){
if(username === null || typeof username !== "string"){
username = this.username_model
}
let response = await fetch(`/api/users/${username}/add_friend_request/`);
this.add_status = await response.json();
setTimeout(() => {
this.add_status = null;
}, 3000)
this.username_model = "";
await this.fetchFriendRequests();
this.$emit("friendsUpdated");
},
async acceptFriendRequest(i){
let friend_request = this.friend_requests[i];
await this.addFriendRequest(friend_request.username);
this.friend_requests.splice(i, 1);
this.$emit("friendsUpdated");
},
async removeFriendRequest(i){
let friend_request = this.friend_requests[i];
let response = await fetch(`/api/friend_requests/${friend_request.id}/`,
{
method: "DELETE",
headers: {
"X-CSRFToken": Cookies.get('csrftoken')
},
}
);
if(response.ok) this.friend_requests.splice(i, 1);
// todo : check success
},
//friends related
async removeFriend(i){
let user = this.friends[i];
let response = await fetch(`/api/users/${user.id}/remove_friend/`);
let json = await response.json();
if(json.success) this.friends.splice(i, 1);
this.$emit("friendsUpdated");
},
}
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<v-dialog v-model="enabled" max-width="500">
<template v-slot:activator="{props}">
<v-btn color="blue" prepend-icon="mdi-account-group" text="Manage friends" v-bind="props"/>
</template>
<FriendForm :friends="friends" @friends-updated="$emit('friendsUpdated')"/>
</v-dialog>
</template>
<script setup>
import FriendForm from "./FriendForm.vue";
</script>
<script>
export default {
emits: ["friendsUpdated"],
props: {
friends: {
type: Array,
required: true
}
},
data(){
return {
enabled: false
}
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,175 @@
<template>
<v-app>
<v-navigation-drawer :permanent="true">
<v-list>
<v-list-item>
<v-list-item-action class="justify-center">
<v-row>
<v-col class="text-center">
<UploadForm @torrent_added="torrentAdded"/>
</v-col>
</v-row>
</v-list-item-action>
</v-list-item>
<v-divider/>
</v-list>
</v-navigation-drawer>
<v-navigation-drawer v-model="right_drawer" location="right" temporary>
<Friend v-if="right_drawer" :active_user="display_user.id" @userSelected="userSelectedUpdated"/>
</v-navigation-drawer>
<v-app-bar>
<v-app-bar-title><v-btn variant="plain" href="/">Oxpanel</v-btn></v-app-bar-title>
<v-spacer/>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props">Manage</v-btn>
</template>
<v-list>
<v-list-item href="/password_change/" title="Change password"/>
<v-list-item>
<v-list-item-action>
<form action="/logout/" method="post">
<input type="hidden" name="csrfmiddlewaretoken" :value="Cookies.get('csrftoken')">
<button>Disconnect</button>
</form>
</v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
<v-btn
@click="right_drawer = !right_drawer"
prepend-icon="mdi-account-group"
:color="display_user.id === user.id ? 'green':'orange'"
:text="display_user.username"
/>
</v-app-bar>
<v-main>
<TorrentList :loading="loading_torrents" :torrents="torrents" :delete_disabled="user.id !== display_user.id"/>
</v-main>
</v-app>
</template>
<script setup>
import Cookies from "js-cookie";
import TorrentList from "@/components/torrent/TorrentList.vue";
import UploadForm from "@/components/torrent/UploadForm.vue";
import Friend from "@/components/auth/Friend.vue";
</script>
<script>
export default {
data(){
return {
right_drawer: false,
user: current_user,
display_user: {
id: current_user.id,
username: "My torrents"
},
loading_torrents: false,
filters: {},
torrents: []
}
},
async mounted(){
this.$ws.connect("/ws/torrent_event/");
this.$ws.on("connect", () => {
this.$ws.send(JSON.stringify({"context": "change_follow_user", "user_id": this.display_user.id}));
});
this.$ws.on("json_message", (message) => {
switch(message.context){
case "transmission_data_updated":
this.updateTransmissionData(message.data);
break;
case "add_torrent":
this.fetchTorrent(message.torrent_id);
break;
case "remove_torrent":
this.removeTorrent(message.torrent_id);
break;
case "update_torrent":
this.updateTorrent(message.torrent_id, message.updated_fields)
}
})
await this.fetchTorrents();
},
methods: {
torrentAdded(torrent){
if(!(torrent.id in this.torrentsAsObject)){
this.torrents.unshift(torrent);
}
},
changeDisplayUser(user){
if(user){
this.display_user = {
"username": this.user.id === user.id ? "My torrents": `${user.username} torrents`,
"id": user.id
}
}else{
}
this.display_user = {
"username": this.user.id === user.id ? "My torrents": `${user.username} torrents`,
"id": user.id
}
this.$ws.send(JSON.stringify({"context": "change_follow_user", "user_id": this.display_user.id}));
},
updateTransmissionData(data){
if(data.hashString in this.torrentsAsObject){
this.torrentsAsObject[data.hashString].transmission_data = data
}
},
async fetchTorrents(){
this.loading_torrents = true;
let filters = {...this.filters, user: this.display_user.id},
url = `/api/torrents/?${new URLSearchParams(filters)}`;
let response = await fetch(url);
this.torrents = await response.json();
this.loading_torrents = false;
},
async fetchTorrent(torrent_id){
if(torrent_id in this.torrentsAsObject) return;
let url = `/api/torrents/${torrent_id}/`;
let response = await fetch(url);
if(response.ok){
let torrent = await response.json();
if(!(torrent.id in this.torrentsAsObject)) this.torrents.unshift(torrent);
}else{
setTimeout(() => this.fetchTorrent(torrent_id), 1000);
}
},
async updateTorrent(torrent_id, updated_fields){
if(torrent_id in this.torrentsAsObject){
for(let key in updated_fields){
this.torrentsAsObject[torrent_id][key] = updated_fields[key];
}
}
},
async removeTorrent(torrent_id){
if(!(torrent_id in this.torrentsAsObject)) return;
for(let i = 0; i < this.torrents.length; i++){
if(this.torrents[i].id === torrent_id){
this.torrents.splice(i, 1);
break;
}
}
},
async userSelectedUpdated(user){
await this.changeDisplayUser(user ? user: this.user);
await this.fetchTorrents();
this.right_drawer = false
},
},
computed: {
torrentsAsObject(){
let r = {};
this.torrents.forEach(torrent => {
r[torrent.id] = torrent;
});
return r;
}
}
}
</script>

View File

@@ -0,0 +1,54 @@
<template>
<v-list-item
style="background-color: #434343"
@click.stop="downloadClicked"
:title="file.rel_name"
:subtitle="fs_format(file.size)"
>
<template v-slot:prepend>
<v-btn @click.stop="downloadClicked" :disabled="!is_download_finished" icon="mdi-download" color="green" variant="text"/>
<v-dialog v-if="file.is_stream_video" v-model="video_modal" width="75%" height="100%">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-play" variant="text"/>
</template>
<v-card>
<VideoPlayer class="text-center"/>
</v-card>
</v-dialog>
</template>
</v-list-item>
</template>
<script setup>
import {fs_format} from "@/plugins/utils.js";
import VideoPlayer from "@/components/utils/VideoPlayer.vue";
</script>
<script>
export default {
props: {
file: {
required: true,
type: Object,
},
is_download_finished: {
required: true,
type: Boolean,
}
},
data(){
return {
video_modal: false,
}
},
methods: {
downloadClicked(){
if(!this.is_download_finished) return;
let a = document.createElement("a");
a.href = this.file.download_url;
a.setAttribute("download", "download");
a.click();
}
}
}
</script>

View File

@@ -0,0 +1,48 @@
<template>
<v-list density="compact">
<div v-if="loading">
<v-progress-circular indeterminate/>
Loading files ...
</div>
<FileItem v-for="file in files" :key="file.id" :file="file" :is_download_finished="is_download_finished"/>
</v-list>
</template>
<script setup>
import FileItem from "@/components/torrent/FileItem.vue";
</script>
<script>
export default {
props: {
torrent_id: {
required: true,
type: String,
},
is_download_finished: {
required: true,
type: Boolean,
}
},
data(){
return {
loading: true,
files: [],
filters: {
torrent: this.torrent_id,
}
}
},
async mounted(){
await this.fetchFiles();
},
methods: {
async fetchFiles(){
this.loading = true;
let response = await fetch(`/api/torrent/files?${new URLSearchParams(this.filters)}`);
this.files = await response.json();
this.loading = false;
}
}
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<v-card @click="show_files = !show_files">
<v-progress-linear height="20" :color="progressData.color" :model-value="progressData.value">
<!-- barre de progression -->
<v-progress-circular
v-if="!torrent.transmission_data.rateDownload && !torrent.transmission_data.rateUpload"
size="15"
indeterminate
:color="torrent.transmission_data.progress < 100 ? 'green':'red'"
/>
<v-icon
v-else
:color="torrent.transmission_data.rateDownload ? 'green':'red'"
:icon="torrent.transmission_data.rateDownload ? 'mdi-arrow-down-bold':'mdi-arrow-up-bold'"
/>
<strong style="padding-left: 5px">
{{progressData.value}}%
<strong
v-if="torrent.transmission_data.rateDownload || torrent.transmission_data.rateUpload"
>
({{torrent.transmission_data.rateDownload ? fs_speed_format(torrent.transmission_data.rateDownload):fs_speed_format(torrent.transmission_data.rateUpload)}}/s)
</strong>
</strong>
</v-progress-linear>
<v-row no-gutters>
<!-- ligne du haut -->
<v-col>
<v-card-text v-text="torrent.name"></v-card-text>
</v-col>
<v-col lg="1">
<v-card-text v-text="fs_format(torrent.size)"></v-card-text>
</v-col>
</v-row>
<v-row v-if="torrent.transmission_data.progress < 100" justify="end" no-gutters>
<!-- ligne du milieu -->
<v-col lg="2">
<v-card-text>remaining : {{fs_format(torrent.transmission_data.leftUntilDone)}}/{{progressData.eta}}</v-card-text>
</v-col>
</v-row>
<v-card-actions>
<v-btn :disabled="torrent.transmission_data.progress < 100" @click.stop="downloadClicked" color="green" :icon="torrent.count_files === 1 ? 'mdi-download':'mdi-folder-download'"/>
<v-dialog v-model="share_modal" max-width="600">
<template v-slot:activator="{props}">
<v-btn icon="mdi-share-variant" v-bind="props"/>
</template>
<TorrentShare :torrent="torrent"/>
</v-dialog>
<v-spacer/>
<v-btn @click.stop="deleteClicked" color="red" icon="mdi-delete-variant" :disabled="delete_disabled"/>
</v-card-actions>
<v-expand-transition>
<div v-if="show_files">
<v-divider />
<FileList v-if="show_files" :torrent_id="torrent.id" :is_download_finished="torrent.transmission_data.progress >= 100"/>
</div>
</v-expand-transition>
</v-card>
</template>
<script setup>
import {fs_format, fs_speed_format} from "@/plugins/utils.js";
import FileList from "@/components/torrent/FileList.vue";
import TorrentShare from "@/components/torrent/TorrentShare.vue";
</script>
<script>
import Cookies from "js-cookie";
import {toHHMMSS} from "@/plugins/utils.js";
export default {
emits: ["delete"],
props: {
torrent: {
required: true,
type: Object,
},
delete_disabled: {
required: true,
type: Boolean,
}
},
data(){
return {
show_files: false,
share_modal: false,
}
},
methods: {
async downloadClicked(){
if(this.torrent.transmission_data.progress < 100) return;
let a = document.createElement("a");
a.href = this.torrent.download_url;
a.setAttribute("download", "download");
a.click();
},
async deleteClicked(){
this.$emit("delete", this.torrent.id);
await fetch(`/api/torrents/${this.torrent.id}/`, {
method: "DELETE",
headers: {
"X-CSRFToken": Cookies.get("csrftoken"),
}
})
}
},
computed: {
progressData(){
let color = "red", value = 0, eta;
if(this.torrent.transmission_data.progress < 100){
color = "blue";
value = this.torrent.transmission_data.progress;
if(this.torrent.transmission_data.eta !== -1){
eta = toHHMMSS(this.torrent.transmission_data.eta);
}else{
eta = "-";
}
}else{
color = "green";
value = Number((this.torrent.transmission_data.uploadRatio / 5) * 100).toFixed(2)
if(value > 100) value = 100;
}
return {color, value, eta};
}
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<v-container fluid>
<TorrentItem
v-if="torrents.length"
v-for="(torrent, i) in torrents"
:key="torrent.id"
:torrent="torrent"
:delete_disabled="delete_disabled"
@delete="torrents.splice(i, 1)"
/>
<v-card v-else :color="loading ? 'orange':'red'">
<v-card-text v-if="loading" class="text-center">loading torrents...</v-card-text>
<v-card-text v-else class="text-center">There is no torrent to display</v-card-text>
</v-card>
</v-container>
</template>
<script setup>
import TorrentItem from "./TorrentItem.vue";
</script>
<script>
export default {
props: {
torrents: {
required: true,
type: Array
},
loading: {
required: true,
type: Boolean
},
delete_disabled: {
required: true,
type: Boolean,
}
},
methods: {
deleteClicked(i){
console.log("delete clicked !", i)
this.torrents.splice(i, 1)
}
}
}
</script>

View File

@@ -0,0 +1,110 @@
<template>
<v-container>
<v-card>
<v-card-title>Sharing of `{{ torrent.name }}`</v-card-title>
<v-divider/>
<v-row>
<v-col>
<v-card class="justify-center">
<v-card-title>Share with others users</v-card-title>
<v-card-text
v-if="message"
class="text-center justify-center"
style="background-color: green; padding-top: 13px"
v-text="message"
/>
<v-card-text>
<v-autocomplete
v-model="share_input"
:items="filteredUser"
item-title="username"
item-value="id"
/>
</v-card-text>
</v-card>
</v-col>
<v-divider :vertical="true"/>
<!-- <v-col>-->
<!-- <v-card-text>(public share W.I.P., only enabled to trusted User)</v-card-text>-->
<!-- </v-col>-->
</v-row>
</v-card>
</v-container>
</template>
<script>
import Cookies from "js-cookie";
export default {
props: {
torrent: {
required: true,
type: Object,
}
},
data(){
return {
users: [],
share_input: "",
message: ""
}
},
watch: {
share_input(newValue, oldValue){
if(newValue){
this.sharedFieldChange(newValue);
}
}
},
async mounted(){
await this.fetchUsers();
},
methods: {
async sharedFieldChange(user_id){
let username;
for(let user of this.users){
if(user_id === user.id){
username = user.username;
break;
}
}
let form = new FormData();
form.append("user_id", user_id);
let response = await fetch(`/api/torrents/${this.torrent.id}/share/`, {
method: "POST",
headers: {
"X-CSRFToken": Cookies.get('csrftoken')
},
body: form
})
let status = await response.json();
if(status.success){
this.share_message_status = `Shared with '${username}' successfully`
setTimeout(() => {
this.share_message_status = ""
}, 3000);
this.torrent.shared_users.push(user_id)
}
this.$nextTick(() => {
this.share_input = "";
})
},
async fetchUsers(){
let response = await fetch("/api/users/");
this.users = await response.json();
}
},
computed: {
filteredUser(){
let r = [];
this.users.forEach(user => {
if(this.torrent.user !== user.id && user.id !== current_user.id && !(this.torrent.shared_users.includes(user.id))){
r.push(user)
}
})
return r
}
}
}
</script>

View File

@@ -0,0 +1,105 @@
<template>
<v-dialog v-model="enabled" max-width="500">
<template v-slot:activator="{props}">
<v-btn color="blue" prepend-icon="mdi-file-upload" text=".torrent" v-bind="props"/>
</template>
<input type="file" multiple="multiple" ref="fileinput" @change="uploadFieldChange" hidden="hidden" accept=".torrent">
<v-card>
<v-card-title class="text-center">Send Torrent Files</v-card-title>
<v-card-actions class="justify-center">
<v-btn @click="uploadFieldTriggered" color="blue" prepend-icon="mdi-plus" variant="tonal" text="add .torrent" />
<v-btn :disabled="!attachments.length" color="green" variant="tonal" icon="mdi-check" @click="sendFiles"/>
<v-btn @click="attachments.splice(0, attachments.length)" :disabled="!attachments.length" color="red" variant="tonal" icon="mdi-cancel"/>
</v-card-actions>
<v-list >
<v-list-item
v-for="(attachment, i) in attachments"
:key="i"
@click="this.attachments.splice(i, 1)"
prepend-icon="mdi-file"
:title="attachment.file_object.name"
>
<v-list-item-subtitle :class="`text-${attachment.text_color}`" v-text="attachment.response ? attachment.response.message:'waiting'"/>
</v-list-item>
</v-list>
</v-card>
</v-dialog>
</template>
<script>
import Cookies from "js-cookie";
export default {
emits: ["torrent_added"],
data(){
return {
enabled: false,
attachments: []
}
},
watch: {
enabled(newValue, oldValue){
if(newValue){
this.$nextTick(() => {
this.uploadFieldTriggered();
})
}else{
this.attachments.splice(0, this.attachments.length)
}
},
// attachments(newValue, oldValue){
// this.status.pop()
// }
},
methods: {
async sendFiles(){
let form, response, data;
for (const attachment of this.attachments) {
if(!attachment.response){
form = new FormData();
form.append("file", attachment.file_object);
// form.append("csrfmiddlewaretoken", Cookies.get('csrftoken'))
response = await fetch("/api/torrents/", {
method: "POST",
headers: {
"X-CSRFToken": Cookies.get('csrftoken')
},
body: form
})
data = await response.json()
attachment.response = data
switch (data.status){
case "error":
attachment.text_color = "red";
break;
case "warn":
attachment.text_color = "yellow";
this.$emit("torrent_added", data.torrent);
break;
case "success":
attachment.text_color = "green"
this.$emit("torrent_added", data.torrent);
break;
}
}
}
},
uploadFieldChange(event){
let files = event.target.files || event.dataTransfer.files;
if(files.length){
Array.prototype.forEach.call(files, file => {
this.attachments.push({
file_object: file,
response: null,
text_color: "blue"
})
})
}
},
uploadFieldTriggered(){
this.$refs.fileinput.click();
}
}
}
</script>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<video ref="videoPlayer" id="my-video" controls preload="auto" class="video-js vjs-default-skin vjs-16-9"></video>
</div>
</template>
<script>
import "video.js/dist/video-js.min.css";
import videojs from "video.js";
export default {
name: "VideoPlayer",
props: {
options: {
type: Object,
default(){
return {};
}
}
},
data(){
return {
player: null,
}
},
mounted() {
this.player = videojs(this.$refs.videoPlayer, this.options, () => {
this.player.log("onPlayerReady", this);
})
},
beforeUnmount() {
if(this.player) {
this.player.dispose();
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,17 @@
import {filesize} from "filesize";
function fs_format(value){
return filesize(value, {round: 2, exponent: 3})
}
function fs_speed_format(value){
return filesize(value, {})
}
function toHHMMSS(time) {
let sec_num = parseInt(time), hours = Math.floor(sec_num / 3600), minutes = Math.floor(sec_num / 60) % 60, seconds = sec_num % 60;
return [hours, minutes, seconds]
.map(v => v < 10 ? "0" + v : v).filter((v, i) => v !== "00" || i > 0).join(":");
}
export {fs_format, fs_speed_format, toHHMMSS}

View File

@@ -0,0 +1,7 @@
import { createApp } from 'vue';
import Vuetify from "./vuetify"
import Ws from "./ws";
export function createVue(component, dom_id){
return createApp(component).use(Vuetify).use(Ws).mount(dom_id)
}

View File

@@ -0,0 +1,30 @@
import '@mdi/font/css/materialdesignicons.css' // Ensure you are using css-loader
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import colors from 'vuetify/lib/util/colors.mjs'
// import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
export default createVuetify({
// components,
// directives,
icons: {
defaultSet: 'mdi',
},
// ssr: false,
theme: {
defaultTheme: "dark",
themes: {
dark: {
colors: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
}
});

View File

@@ -0,0 +1,78 @@
import { EventEmitter } from 'events';
class WebSocketClient {
constructor() {
this.url = null;
this.socket = null;
this.reconnectInterval = 5000;
this.eventEmitter = new EventEmitter(); // Ajouter l'EventEmitter
}
connect(url) {
this.url = url;
this.socket = new window.WebSocket(this.url);
this.socket.onopen = () => {
console.log('WebSocket connected');
this.eventEmitter.emit('connect');
};
this.socket.onmessage = (message) => {
// console.log('Message received:', message.data);
// Émettre l'événement 'message' avec les données reçues
this.eventEmitter.emit('message', message.data);
this.handleMessage(message.data);
};
this.socket.onclose = () => {
console.log('WebSocket disconnected, retrying in 5 seconds...');
this.eventEmitter.emit('disconnect');
setTimeout(() => this.connect(this.url), this.reconnectInterval);
};
this.socket.onerror = (error) => {
console.error('WebSocket error:', error);
this.eventEmitter.emit('error', error);
this.socket.close();
};
}
handleMessage(rawData){
try {
const parsedData = JSON.parse(rawData);
this.eventEmitter.emit('json_message', parsedData);
if("context" in parsedData){
this.eventEmitter.emit(parsedData.context, parsedData);
}
return parsedData;
} catch (error) {
console.error('Failed to parse message as JSON:', error);
this.eventEmitter.emit('invalid_message', { raw: rawData, error });
return null;
}
}
// Méthodes pour gérer les événements
on(event, callback) {
this.eventEmitter.on(event, callback);
}
off(event, callback) {
this.eventEmitter.off(event, callback);
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
} else {
console.error('WebSocket is not open. Cannot send data.');
}
}
}
export default {
install: (app, options) => {
app.config.globalProperties.$ws = new WebSocketClient();
}
}