@ -1,27 +1,8 @@ | |||
<template> | |||
<img alt="Vue logo" src="./assets/logo.png" /> | |||
<HelloWorld msg="Hello Vue 3 + TypeScript + Vite" /> | |||
<router-view></router-view> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent } from 'vue' | |||
import HelloWorld from './components/HelloWorld.vue' | |||
export default defineComponent({ | |||
name: 'App', | |||
components: { | |||
HelloWorld | |||
} | |||
}) | |||
// import { defineComponent } from 'vue' | |||
// export default defineComponent({}); | |||
</script> | |||
<style> | |||
#app { | |||
font-family: Avenir, Helvetica, Arial, sans-serif; | |||
-webkit-font-smoothing: antialiased; | |||
-moz-osx-font-smoothing: grayscale; | |||
text-align: center; | |||
color: #2c3e50; | |||
margin-top: 60px; | |||
} | |||
</style> |
@ -1,65 +0,0 @@ | |||
<template> | |||
<h1>{{ msg }}</h1> | |||
<p> | |||
Recommended IDE setup: | |||
<a href="https://code.visualstudio.com/" target="_blank">VSCode</a> | |||
+ | |||
<a | |||
href="https://marketplace.visualstudio.com/items?itemName=octref.vetur" | |||
target="_blank" | |||
>Vetur</a> | |||
or | |||
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a> | |||
(if using | |||
<code><script setup></code>) | |||
</p> | |||
<p>See <code>README.md</code> for more information.</p> | |||
<p> | |||
<a href="https://vitejs.dev/guide/features.html" target="_blank">Vite Docs</a> | | |||
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a> | |||
</p> | |||
<button @click="count++">count is: {{ count }}</button> | |||
<p> | |||
Edit | |||
<code>components/HelloWorld.vue</code> to test hot module replacement. | |||
</p> | |||
</template> | |||
<script lang="ts"> | |||
import { ref, defineComponent } from 'vue' | |||
export default defineComponent({ | |||
name: 'HelloWorld', | |||
props: { | |||
msg: { | |||
type: String, | |||
required: true | |||
} | |||
}, | |||
setup: () => { | |||
const count = ref(0) | |||
return { count } | |||
} | |||
}) | |||
</script> | |||
<style scoped> | |||
a { | |||
color: #42b983; | |||
} | |||
label { | |||
margin: 0 0.5em; | |||
font-weight: bold; | |||
} | |||
code { | |||
background-color: #eee; | |||
padding: 2px 4px; | |||
border-radius: 4px; | |||
color: #304455; | |||
} | |||
</style> |
@ -0,0 +1,3 @@ | |||
<template> | |||
<div></div> | |||
</template> |
@ -0,0 +1,89 @@ | |||
<template> | |||
<div class="box-border relative text-white radial-progress"> | |||
<svg class="transform -rotate-90" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" style="height: 100%; width: 100%"> | |||
<circle | |||
class="radial-progress-ring__circle" | |||
:stroke-width="ringWidth" | |||
:r="50 - ringWidth/2" | |||
:cx="50" | |||
:cy="50" | |||
ref="ring" | |||
/> | |||
</svg> | |||
<div class="antialiased flex absolute inset-0 items-center justify-center text-xs font-light rounded-full text-white">{{ progressFormatted }}%</div> | |||
<div class="flex absolute inset-0 items-center justify-center text-sm font-thin rounded-full bg-modal" :class="{ 'show-state': isPaused }" @click="$emit('toggleState')"> | |||
<i class="fas fa-play" v-if="!isPaused"></i> | |||
<i class="fas fa-pause" v-else></i> | |||
</div> | |||
</div> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent } from "vue"; | |||
export enum RadialProgressState { | |||
Downloading, | |||
Paused, | |||
Halted, | |||
Error | |||
} | |||
export default defineComponent({ | |||
computed: { | |||
circumference(): number { | |||
return 2*Math.PI*(50 - this.ringWidth/2); | |||
}, | |||
progressFormatted(): number { | |||
return Math.floor(100*(<number>this.progress)); | |||
} | |||
}, | |||
methods: { | |||
updateProgressBar() { | |||
(<HTMLElement>this.$refs.ring).style.strokeDasharray = `${this.circumference} ${this.circumference}`; | |||
(<HTMLElement>this.$refs.ring).style.strokeDashoffset = `${this.circumference - this.circumference*this.progress}`; | |||
} | |||
}, | |||
mounted() { | |||
this.updateProgressBar(); | |||
}, | |||
props: { | |||
progress: { | |||
type: Number, | |||
required: true | |||
}, | |||
isPaused: { | |||
type: Boolean, | |||
default: false | |||
}, | |||
ringWidth: { | |||
type: Number, | |||
default: 1 | |||
} | |||
}, | |||
watch: { | |||
progress() { | |||
this.updateProgressBar(); | |||
} | |||
} | |||
}); | |||
</script> | |||
<style lang="css"> | |||
.radial-progress-ring__circle { | |||
fill: #1F2937; | |||
stroke: #34D399; | |||
transition: stroke-dashoffset 0.25s; | |||
} | |||
.bg-modal { | |||
background: rgba(0, 0, 0, 0.35); | |||
opacity: 0.0; | |||
transition: opacity 0.25s; | |||
} | |||
.show-state { | |||
opacity: 1.0; | |||
} | |||
.radial-progress:hover .bg-modal { | |||
opacity: 1.0; | |||
} | |||
</style> |
@ -0,0 +1,133 @@ | |||
<template> | |||
<div class="bg-gray-800 rounded-md m-4 md:m-8 text-gray-300"> | |||
<div class="p-5"> | |||
<input type="text" placeholder="Search..." class="p-2 bg-gray-600 rounded w-full"> | |||
</div> | |||
<div class="grid grid-cols-torrent-list"> | |||
<!-- Header --> | |||
<div class="header contents col-span-5"> | |||
<div class="p-5 py-2 bg-gray-700"></div> | |||
<div class="p-5 py-2 bg-gray-700 col-span-2 md:col-span-1">Name</div> | |||
<div class="p-5 py-2 bg-gray-700 hidden md:block">Size</div> | |||
<div class="p-5 py-2 bg-gray-700 hidden md:block">ETA</div> | |||
<div class="p-5 py-2 bg-gray-700 hidden md:block">Peers</div> | |||
<div class="p-5 py-2 bg-gray-700 hidden md:block">Actions</div> | |||
</div> | |||
<!-- Items --> | |||
<torrent-list-item v-for="infoHash of infoHashes" :torrent="torrents[infoHash]" @delete="removeTorrent"></torrent-list-item> | |||
</div> | |||
<!-- <div v-for="i of order">{{ JSON.stringify(torrents[infoHashes[i]]) }}</div> --> | |||
</div> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent, reactive } from "vue"; | |||
import TorrentListItem from "./TorrentListItem.vue"; | |||
import PaginationWidget from "./PaginationWidget.vue"; | |||
interface ITorrentMap { | |||
[infoHash: string]: ITorrent | |||
} | |||
interface ITorrent { | |||
name: string, | |||
progress: number, | |||
infoHash: string, | |||
isPaused: boolean | |||
} | |||
export default defineComponent({ | |||
components: { | |||
TorrentListItem, | |||
PaginationWidget | |||
}, | |||
data() { | |||
return { | |||
infoHashes: <string[]>[], | |||
torrents: <ITorrentMap>{} | |||
} | |||
}, | |||
methods: { | |||
updateTorrentsList(torrents: ITorrent[]) { | |||
let toDelete = new Set(this.infoHashes); | |||
torrents.forEach((torrent, index) => { | |||
let infoHash = torrent.infoHash; | |||
if (infoHash in this.torrents) { | |||
toDelete.delete(infoHash); | |||
this.torrents[infoHash].progress = torrent.progress; | |||
this.torrents[infoHash].isPaused = torrent.isPaused; | |||
} else { | |||
this.infoHashes.splice(index, 0, infoHash); | |||
this.torrents[infoHash] = reactive(torrent); | |||
} | |||
}); | |||
toDelete.forEach((infoHash) => { | |||
delete this.torrents[infoHash]; | |||
}); | |||
}, | |||
removeTorrent(infoHash: string) { | |||
delete this.torrents[infoHash]; | |||
this.infoHashes.splice(this.infoHashes.indexOf(infoHash), 1); | |||
} | |||
}, | |||
mounted() { | |||
this.updateTorrentsList([{ | |||
name: "Big Buck Bunny", | |||
progress: 0.5, | |||
infoHash: "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c", | |||
isPaused: false | |||
}, | |||
// { | |||
// name: "Cosmos Laundromat", | |||
// progress: 0.5, | |||
// infoHash: "c9e15763f722f23e98a29decdfae341b98d53056", | |||
// isPaused: false | |||
// }, { | |||
// name: "Sintel", | |||
// progress: 0.5, | |||
// infoHash: "08ada5a7a6183aae1e09d831df6748d566095a10", | |||
// isPaused: false | |||
// }, { | |||
// name: "Tears of Steel", | |||
// progress: 0.5, | |||
// infoHash: "209c8226b299b308beaf2b9cd3fb49212dbd13ec", | |||
// isPaused: false | |||
// }, { | |||
// name: "The WIRED CD - Rip. Sample. Mash. Share", | |||
// progress: 0.5, | |||
// infoHash: "a88fda5954e89178c372716a6a78b8180ed4dad3", | |||
// isPaused: false | |||
// } | |||
]); | |||
setInterval(() => { | |||
for (let infoHash of this.infoHashes) { | |||
this.torrents[infoHash].progress = Math.random(); | |||
} | |||
}, 3000); | |||
} | |||
}); | |||
</script> | |||
<style lang="css"> | |||
.grid-cols-torrent-list { | |||
grid-template-columns: min-content 1fr min-content; | |||
} | |||
@media (min-width: 768px) { | |||
.grid-cols-torrent-list { | |||
grid-template-columns: min-content 1fr repeat(3, minmax(0, 0.20fr)) min-content; | |||
} | |||
} | |||
.row > div { | |||
align-items: center; | |||
@apply border-b border-gray-700 cursor-pointer; | |||
} | |||
.row:last-of-type > div { | |||
@apply border-b-0; | |||
} | |||
.row:hover > div { | |||
@apply bg-gray-600; | |||
} | |||
</style> |
@ -0,0 +1,59 @@ | |||
<template> | |||
<div class="row contents"> | |||
<div class="pl-5 py-3 w-16 h-16 flex justify-center"> | |||
<progress-ring :ring-width="8" class="h-10" :is-paused="torrent.isPaused" @toggle-state="toggleState" :progress="torrent.progress"/> | |||
</div> | |||
<div class="pl-5 py-3 h-16 flex overflow-ellipsis overflow-hidden whitespace-nowrap">{{ torrent.name }}</div> | |||
<div class="pl-5 py-3 h-16 hidden md:flex">1.21GB</div> | |||
<div class="pl-5 py-3 h-16 hidden md:flex">5 Minutes</div> | |||
<div class="pl-5 py-3 h-16 hidden md:flex">29</div> | |||
<div class="p-5 py-3 h-16 flex"> | |||
<div class="dropdown" :class="{'active': showMenu}"> | |||
<button id="actions-menu" aria-expanded="true" aria-haspopup="true">...</button> | |||
<div class="dropdown-menu origin-top-right absolute right: 0 mt-8 w-56" role="menu" aria-orientation="vertical" aria-labelledby="actions-menu"> | |||
<div class="py-1" role="none"> | |||
<a href="#" class="block px-4 py-2 text-sm text-gray-300">Thing</a> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent } from "vue"; | |||
import ProgressRing from "./ProgressRing.vue"; | |||
export default defineComponent({ | |||
components: { | |||
ProgressRing | |||
}, | |||
data() { | |||
return { | |||
showMenu: false | |||
} | |||
}, | |||
methods: { | |||
toggleState() { | |||
this.torrent.isPaused = !this.torrent.isPaused; | |||
}, | |||
}, | |||
mounted() { | |||
}, | |||
props: { | |||
torrent: { | |||
type: Object, | |||
required: true | |||
} | |||
} | |||
}); | |||
</script> | |||
<style class="css"> | |||
.dropdown .dropdown-menu { | |||
display: none; | |||
} | |||
.dropdown.active .dropdown-menu { | |||
display: initial; | |||
} | |||
</style> |
@ -0,0 +1,82 @@ | |||
<template> | |||
<tr class=""> | |||
<td class="pl-5 py-3 w-1 h-16 handle cursor-move text-gray-500 hover:text-white"> | |||
<div class="text-center"> | |||
<i class="fas fa-bars transitions transition-colors"></i> | |||
</div> | |||
</td> | |||
<td class="pl-5 py-3 w-16 h-16"> | |||
<progress-ring :ring-width="8" class="w-10 h-10" :is-paused="isPaused" @toggle-state="toggleState" :progress="progress"/> | |||
</td> | |||
<td class="p-5 py-3 h-16 overflow-ellipsis overflow-hidden whitespace-nowrap"> | |||
{{ name }} | |||
</td> | |||
<td class="p-5 py-3 h-16 w-32 hidden md:table-cell">1.21GB</td> | |||
<td class="p-5 py-3 h-16 w-32 hidden md:table-cell">5 Minutes</td> | |||
<td class="p-5 py-3 h-16 w-16 hidden md:table-cell">29</td> | |||
<td class="p-5 py-3 h-16 w-1 hidden md:table-cell"> | |||
<div class="dropdown" :class="{'active': showMenu}"> | |||
<button class="w-9 h-9 rounded-full hover:bg-indigo-500 hover:text-white transitions transition-colors" id="actions-menu" aria-expanded="true" aria-haspopup="true"><i class="fas fa-ellipsis-v"></i></button> | |||
<div class="dropdown-menu origin-top-right absolute right: 0 mt-8 w-56" role="menu" aria-orientation="vertical" aria-labelledby="actions-menu"> | |||
<div class="py-1" role="none"> | |||
<a href="#" class="block px-4 py-2 text-sm text-gray-300">Thing</a> | |||
</div> | |||
</div> | |||
</div> | |||
</td> | |||
</tr> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent } from "vue"; | |||
import ProgressRing from "./ProgressRing.vue"; | |||
export default defineComponent({ | |||
components: { | |||
ProgressRing | |||
}, | |||
data() { | |||
return { | |||
showMenu: false | |||
} | |||
}, | |||
methods: { | |||
toggleState() { | |||
// this.isPaused = !this.isPaused; | |||
}, | |||
}, | |||
mounted() { | |||
}, | |||
props: { | |||
id: { | |||
type: Number, | |||
required: true | |||
}, | |||
name: { | |||
type: String, | |||
required: true | |||
}, | |||
infoHash: { | |||
type: String, | |||
required: true, | |||
}, | |||
progress: { | |||
type: Number, | |||
required: true | |||
}, | |||
isPaused: { | |||
type: Boolean, | |||
required: true | |||
} | |||
} | |||
}); | |||
</script> | |||
<style class="css"> | |||
.dropdown .dropdown-menu { | |||
display: none; | |||
} | |||
.dropdown.active .dropdown-menu { | |||
display: initial; | |||
} | |||
</style> |
@ -0,0 +1,161 @@ | |||
<template> | |||
<div class="bg-gray-800 rounded-md m-4 md:m-8 text-gray-300"> | |||
<div class="p-5"> | |||
<input type="text" placeholder="Search..." class="p-2 bg-gray-600 rounded w-full"> | |||
</div> | |||
<table class="w-full" cellspacing="0" cellpadding="0"> | |||
<thead class="w-full text-center"> | |||
<tr> | |||
<th class="p-5 py-2 bg-gray-700" colspan="2"></th> | |||
<th class="p-5 py-2 bg-gray-700 text-left">Name</th> | |||
<th class="p-5 py-2 bg-gray-700 hidden md:table-cell">Size</th> | |||
<th class="p-5 py-2 bg-gray-700 hidden md:table-cell">ETA</th> | |||
<th class="p-5 py-2 bg-gray-700 hidden md:table-cell">Peers</th> | |||
<th class="p-5 py-2 bg-gray-700 hidden md:table-cell">Actions</th> | |||
</tr> | |||
</thead> | |||
<!-- Items --> | |||
<draggable v-model="torrents" v-bind="dragOptions" item-key="id" tag="transition-group" :component-data="{tag: 'tbody'}" @start="isDragging=true" @end="isDragging=false" @sort="onSort"> | |||
<template v-slot:item="{element}"> | |||
<torrent-list-item-new v-bind="element"></torrent-list-item-new> | |||
</template> | |||
</draggable> | |||
</table> | |||
</div> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent, reactive } from "vue"; | |||
import Draggable from "vuedraggable"; | |||
import TorrentListItemNew from "./TorrentListItemNew.vue"; | |||
import PaginationWidget from "./PaginationWidget.vue"; | |||
interface ITorrentMap { | |||
[infoHash: string]: ITorrent | |||
} | |||
interface ITorrent { | |||
id: number, | |||
name: string, | |||
progress: number, | |||
infoHash: string, | |||
isPaused: boolean | |||
} | |||
export default defineComponent({ | |||
components: { | |||
Draggable, | |||
TorrentListItemNew, | |||
PaginationWidget | |||
}, | |||
data() { | |||
return { | |||
torrents: <ITorrent[]>[{ | |||
id: 0, | |||
name: "Big Buck Bunny", | |||
progress: 0.5, | |||
infoHash: "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c", | |||
isPaused: false | |||
}, | |||
{ | |||
id: 1, | |||
name: "Cosmos Laundromat", | |||
progress: 0.5, | |||
infoHash: "c9e15763f722f23e98a29decdfae341b98d53056", | |||
isPaused: false | |||
}, { | |||
id: 2, | |||
name: "Sintel", | |||
progress: 0.5, | |||
infoHash: "08ada5a7a6183aae1e09d831df6748d566095a10", | |||
isPaused: false | |||
}, { | |||
id: 3, | |||
name: "Tears of Steel", | |||
progress: 0.5, | |||
infoHash: "209c8226b299b308beaf2b9cd3fb49212dbd13ec", | |||
isPaused: false | |||
}, { | |||
id: 4, | |||
name: "The WIRED CD - Rip. Sample. Mash. Share", | |||
progress: 0.5, | |||
infoHash: "a88fda5954e89178c372716a6a78b8180ed4dad3", | |||
isPaused: false | |||
}], | |||
isDragging: false, | |||
dragOptions: { | |||
animation: 200, | |||
ghostClass: "ghost", | |||
handle: ".handle" | |||
} | |||
} | |||
}, | |||
methods: { | |||
updateTorrentsList(torrents: ITorrent[]) { | |||
this.torrents = reactive(torrents); | |||
}, | |||
onSort() { | |||
console.log("Sorted"); | |||
} | |||
}, | |||
mounted() { | |||
// this.updateTorrentsList([{ | |||
// name: "Big Buck Bunny", | |||
// progress: 0.5, | |||
// infoHash: "dd8255ecdc7ca55fb0bbf81323d87062db1f6d1c", | |||
// isPaused: false | |||
// }, | |||
// { | |||
// name: "Cosmos Laundromat", | |||
// progress: 0.5, | |||
// infoHash: "c9e15763f722f23e98a29decdfae341b98d53056", | |||
// isPaused: false | |||
// }, { | |||
// name: "Sintel", | |||
// progress: 0.5, | |||
// infoHash: "08ada5a7a6183aae1e09d831df6748d566095a10", | |||
// isPaused: false | |||
// }, { | |||
// name: "Tears of Steel", | |||
// progress: 0.5, | |||
// infoHash: "209c8226b299b308beaf2b9cd3fb49212dbd13ec", | |||
// isPaused: false | |||
// }, { | |||
// name: "The WIRED CD - Rip. Sample. Mash. Share", | |||
// progress: 0.5, | |||
// infoHash: "a88fda5954e89178c372716a6a78b8180ed4dad3", | |||
// isPaused: false | |||
// } | |||
// ]); | |||
setInterval(() => { | |||
for (let torrent of this.torrents) { | |||
torrent.progress = Math.random(); | |||
} | |||
}, 3000); | |||
} | |||
}); | |||
</script> | |||
<style lang="css"> | |||
.grid-cols-torrent-list { | |||
grid-template-columns: min-content 1fr min-content; | |||
} | |||
@media (min-width: 768px) { | |||
.grid-cols-torrent-list { | |||
grid-template-columns: min-content 1fr repeat(3, minmax(0, 0.20fr)) min-content; | |||
} | |||
} | |||
tr:last-of-type > div { | |||
@apply border-b-0; | |||
} | |||
tr:hover { | |||
@apply bg-gray-600; | |||
} | |||
.ghost { | |||
opacity: 0.0; | |||
} | |||
</style> |
@ -1,5 +1,17 @@ | |||
import { createApp } from 'vue' | |||
import router from "./routes"; | |||
import App from './App.vue' | |||
import "./styles/index.css"; | |||
createApp(App).mount('#app'); | |||
let app = createApp(App); | |||
app.use(router); | |||
app.mount("#app"); | |||
/** | |||
* Dropdown menus | |||
*/ | |||
document.addEventListener("click", (event) => { | |||
let dropdownMenus = document.querySelectorAll(".dropdown.active").forEach((elem) => { | |||
elem.classList.remove("active"); | |||
}); | |||
}); |
@ -0,0 +1,21 @@ | |||
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router"; | |||
const routes: RouteRecordRaw[] = [ | |||
{ | |||
path: "/", | |||
name: "Home", | |||
component: () => import("../views/Home.vue") | |||
}, | |||
{ | |||
path: "/login", | |||
name: "Login", | |||
component: () => import("../views/Login.vue") | |||
} | |||
]; | |||
const router = createRouter({ | |||
history: createWebHistory(), | |||
routes | |||
}); | |||
export default router; |
@ -1,3 +1,4 @@ | |||
@import '@fortawesome/fontawesome-free/css/all.css'; | |||
@tailwind base; | |||
@tailwind components; | |||
@tailwind utilities; |
@ -0,0 +1,15 @@ | |||
<template> | |||
<torrent-list-new></torrent-list-new> | |||
<!-- <sortable-list></sortable-list> --> | |||
</template> | |||
<script lang="ts"> | |||
import { defineComponent } from "vue"; | |||
import TorrentListNew from "../components/TorrentListNew.vue"; | |||
export default defineComponent({ | |||
components: { | |||
TorrentListNew | |||
} | |||
}); | |||
</script> |
@ -0,0 +1,3 @@ | |||
<template> | |||
<div>Login</div> | |||
</template> |