init
This commit is contained in:
7
app/frontend/src/app/login.js
Normal file
7
app/frontend/src/app/login.js
Normal 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");
|
||||
})
|
||||
8
app/frontend/src/app/torrent.js
Normal file
8
app/frontend/src/app/torrent.js
Normal 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");
|
||||
})
|
||||
49
app/frontend/src/components/auth/App.vue
Normal file
49
app/frontend/src/components/auth/App.vue
Normal 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>
|
||||
13
app/frontend/src/components/auth/Base.vue
Normal file
13
app/frontend/src/components/auth/Base.vue
Normal 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>
|
||||
66
app/frontend/src/components/auth/Friend.vue
Normal file
66
app/frontend/src/components/auth/Friend.vue
Normal 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>
|
||||
132
app/frontend/src/components/auth/FriendForm.vue
Normal file
132
app/frontend/src/components/auth/FriendForm.vue
Normal 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>
|
||||
32
app/frontend/src/components/auth/FriendManager.vue
Normal file
32
app/frontend/src/components/auth/FriendManager.vue
Normal 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>
|
||||
175
app/frontend/src/components/torrent/App.vue
Normal file
175
app/frontend/src/components/torrent/App.vue
Normal 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>
|
||||
54
app/frontend/src/components/torrent/FileItem.vue
Normal file
54
app/frontend/src/components/torrent/FileItem.vue
Normal 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>
|
||||
48
app/frontend/src/components/torrent/FileList.vue
Normal file
48
app/frontend/src/components/torrent/FileList.vue
Normal 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>
|
||||
126
app/frontend/src/components/torrent/TorrentItem.vue
Normal file
126
app/frontend/src/components/torrent/TorrentItem.vue
Normal 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>
|
||||
45
app/frontend/src/components/torrent/TorrentList.vue
Normal file
45
app/frontend/src/components/torrent/TorrentList.vue
Normal 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>
|
||||
110
app/frontend/src/components/torrent/TorrentShare.vue
Normal file
110
app/frontend/src/components/torrent/TorrentShare.vue
Normal 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>
|
||||
105
app/frontend/src/components/torrent/UploadForm.vue
Normal file
105
app/frontend/src/components/torrent/UploadForm.vue
Normal 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>
|
||||
41
app/frontend/src/components/utils/VideoPlayer.vue
Normal file
41
app/frontend/src/components/utils/VideoPlayer.vue
Normal 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>
|
||||
17
app/frontend/src/plugins/utils.js
Normal file
17
app/frontend/src/plugins/utils.js
Normal 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}
|
||||
7
app/frontend/src/plugins/vue_loader.js
Normal file
7
app/frontend/src/plugins/vue_loader.js
Normal 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)
|
||||
}
|
||||
30
app/frontend/src/plugins/vuetify.js
Normal file
30
app/frontend/src/plugins/vuetify.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
78
app/frontend/src/plugins/ws.js
Normal file
78
app/frontend/src/plugins/ws.js
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user