sticking with pure vue for now.

This commit is contained in:
ArabCoders 2024-12-14 16:43:27 +03:00
parent cf8f971785
commit ec6b8ec85f
37 changed files with 0 additions and 44187 deletions

17
.vscode/launch.json vendored
View file

@ -74,22 +74,5 @@
],
"justMyCode": true
},
{
"name": "Node: NuxtJS",
"request": "launch",
"runtimeArgs": [
"run",
"dev",
"--",
"--port",
"8082"
],
"runtimeExecutable": "yarn",
"type": "node",
"cwd": "${workspaceFolder}/ui",
"env": {
"VUE_APP_BASE_URL": "http://localhost:8081",
}
}
]
}

23
ui/.gitignore vendored
View file

@ -1,23 +0,0 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
exported
# Node dependencies
node_modules
# Logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

File diff suppressed because it is too large Load diff

View file

@ -1,966 +0,0 @@
.switch[type=checkbox] {
outline: 0;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: inline-block;
position: absolute;
opacity: 0
}
.switch[type=checkbox]:focus + label::after, .switch[type=checkbox]:focus + label::before, .switch[type=checkbox]:focus + label:after, .switch[type=checkbox]:focus + label:before {
outline: 1px dotted #b5b5b5
}
.switch[type=checkbox][disabled] {
cursor: not-allowed
}
.switch[type=checkbox][disabled] + label {
opacity: .5
}
.switch[type=checkbox][disabled] + label::before, .switch[type=checkbox][disabled] + label:before {
opacity: .5
}
.switch[type=checkbox][disabled] + label::after, .switch[type=checkbox][disabled] + label:after {
opacity: .5
}
.switch[type=checkbox][disabled] + label:hover {
cursor: not-allowed
}
.switch[type=checkbox] + label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-start;
font-size: 1rem;
height: 2.5em;
line-height: 1.5;
padding-left: 3.5rem;
padding-top: .2rem;
cursor: pointer
}
.switch[type=checkbox] + label::before, .switch[type=checkbox] + label:before {
position: absolute;
display: block;
top: calc(50% - 1.5rem * .5);
left: 0;
width: 3rem;
height: 1.5rem;
border: .1rem solid transparent;
border-radius: 4px;
background: #b5b5b5;
content: ""
}
.switch[type=checkbox] + label::after, .switch[type=checkbox] + label:after {
display: block;
position: absolute;
top: calc(50% - 1rem * .5);
left: .25rem;
width: 1rem;
height: 1rem;
transform: translate3d(0, 0, 0);
border-radius: 4px;
background: #fff;
transition: all .25s ease-out;
content: ""
}
.switch[type=checkbox] + label .switch-active, .switch[type=checkbox] + label .switch-inactive {
font-size: .9rem;
z-index: 1;
margin-top: -4px
}
.switch[type=checkbox] + label.has-text-inside .switch-inactive {
margin-left: -1.925rem
}
.switch[type=checkbox] + label.has-text-inside .switch-active {
margin-left: -3.25rem
}
.switch[type=checkbox].is-rtl + label {
padding-left: 0;
padding-right: 3.5rem
}
.switch[type=checkbox].is-rtl + label::before, .switch[type=checkbox].is-rtl + label:before {
left: auto;
right: 0
}
.switch[type=checkbox].is-rtl + label::after, .switch[type=checkbox].is-rtl + label:after {
left: auto;
right: 1.625rem
}
.switch[type=checkbox]:checked + label::before, .switch[type=checkbox]:checked + label:before {
background: #00d1b2
}
.switch[type=checkbox]:checked + label::after {
left: 1.625rem
}
.switch[type=checkbox]:checked.is-rtl + label::after, .switch[type=checkbox]:checked.is-rtl + label:after {
left: auto;
right: .25rem
}
.switch[type=checkbox].is-outlined + label::before, .switch[type=checkbox].is-outlined + label:before {
background-color: transparent;
border-color: #b5b5b5
}
.switch[type=checkbox].is-outlined + label::after, .switch[type=checkbox].is-outlined + label:after {
background: #b5b5b5
}
.switch[type=checkbox].is-outlined:checked + label::before, .switch[type=checkbox].is-outlined:checked + label:before {
background-color: transparent;
border-color: #00d1b2
}
.switch[type=checkbox].is-outlined:checked + label::after, .switch[type=checkbox].is-outlined:checked + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-thin + label::before, .switch[type=checkbox].is-thin + label:before {
top: .5454545456rem;
height: .375rem
}
.switch[type=checkbox].is-thin + label::after, .switch[type=checkbox].is-thin + label:after {
box-shadow: 0 0 3px #7a7a7a
}
.switch[type=checkbox].is-rounded + label::before, .switch[type=checkbox].is-rounded + label:before {
border-radius: 24px
}
.switch[type=checkbox].is-rounded + label::after, .switch[type=checkbox].is-rounded + label:after {
border-radius: 50%
}
.switch[type=checkbox].is-small + label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-start;
font-size: .75rem;
height: 2.5em;
line-height: 1.5;
padding-left: 2.75rem;
padding-top: .2rem;
cursor: pointer
}
.switch[type=checkbox].is-small + label::before, .switch[type=checkbox].is-small + label:before {
position: absolute;
display: block;
top: calc(50% - 1.125rem * .5);
left: 0;
width: 2.25rem;
height: 1.125rem;
border: .1rem solid transparent;
border-radius: 4px;
background: #b5b5b5;
content: ""
}
.switch[type=checkbox].is-small + label::after, .switch[type=checkbox].is-small + label:after {
display: block;
position: absolute;
top: calc(50% - .625rem * .5);
left: .25rem;
width: .625rem;
height: .625rem;
transform: translate3d(0, 0, 0);
border-radius: 4px;
background: #fff;
transition: all .25s ease-out;
content: ""
}
.switch[type=checkbox].is-small + label .switch-active, .switch[type=checkbox].is-small + label .switch-inactive {
font-size: .65rem;
z-index: 1;
margin-top: -4px
}
.switch[type=checkbox].is-small + label.has-text-inside .switch-inactive {
margin-left: -1.55rem
}
.switch[type=checkbox].is-small + label.has-text-inside .switch-active {
margin-left: -2.5rem
}
.switch[type=checkbox].is-small.is-rtl + label {
padding-left: 0;
padding-right: 2.75rem
}
.switch[type=checkbox].is-small.is-rtl + label::before, .switch[type=checkbox].is-small.is-rtl + label:before {
left: auto;
right: 0
}
.switch[type=checkbox].is-small.is-rtl + label::after, .switch[type=checkbox].is-small.is-rtl + label:after {
left: auto;
right: 1.25rem
}
.switch[type=checkbox].is-small:checked + label::before, .switch[type=checkbox].is-small:checked + label:before {
background: #00d1b2
}
.switch[type=checkbox].is-small:checked + label::after {
left: 1.25rem
}
.switch[type=checkbox].is-small:checked.is-rtl + label::after, .switch[type=checkbox].is-small:checked.is-rtl + label:after {
left: auto;
right: .25rem
}
.switch[type=checkbox].is-small.is-outlined + label::before, .switch[type=checkbox].is-small.is-outlined + label:before {
background-color: transparent;
border-color: #b5b5b5
}
.switch[type=checkbox].is-small.is-outlined + label::after, .switch[type=checkbox].is-small.is-outlined + label:after {
background: #b5b5b5
}
.switch[type=checkbox].is-small.is-outlined:checked + label::before, .switch[type=checkbox].is-small.is-outlined:checked + label:before {
background-color: transparent;
border-color: #00d1b2
}
.switch[type=checkbox].is-small.is-outlined:checked + label::after, .switch[type=checkbox].is-small.is-outlined:checked + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-small.is-thin + label::before, .switch[type=checkbox].is-small.is-thin + label:before {
top: .4090909093rem;
height: .28125rem
}
.switch[type=checkbox].is-small.is-thin + label::after, .switch[type=checkbox].is-small.is-thin + label:after {
box-shadow: 0 0 3px #7a7a7a
}
.switch[type=checkbox].is-small.is-rounded + label::before, .switch[type=checkbox].is-small.is-rounded + label:before {
border-radius: 24px
}
.switch[type=checkbox].is-small.is-rounded + label::after, .switch[type=checkbox].is-small.is-rounded + label:after {
border-radius: 50%
}
.switch[type=checkbox].is-medium + label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-start;
font-size: 1.25rem;
height: 2.5em;
line-height: 1.5;
padding-left: 4.25rem;
padding-top: .2rem;
cursor: pointer
}
.switch[type=checkbox].is-medium + label::before, .switch[type=checkbox].is-medium + label:before {
position: absolute;
display: block;
top: calc(50% - 1.875rem * .5);
left: 0;
width: 3.75rem;
height: 1.875rem;
border: .1rem solid transparent;
border-radius: 4px;
background: #b5b5b5;
content: ""
}
.switch[type=checkbox].is-medium + label::after, .switch[type=checkbox].is-medium + label:after {
display: block;
position: absolute;
top: calc(50% - 1.375rem * .5);
left: .25rem;
width: 1.375rem;
height: 1.375rem;
transform: translate3d(0, 0, 0);
border-radius: 4px;
background: #fff;
transition: all .25s ease-out;
content: ""
}
.switch[type=checkbox].is-medium + label .switch-active, .switch[type=checkbox].is-medium + label .switch-inactive {
font-size: 1.15rem;
z-index: 1;
margin-top: -4px
}
.switch[type=checkbox].is-medium + label.has-text-inside .switch-inactive {
margin-left: -2.3rem
}
.switch[type=checkbox].is-medium + label.has-text-inside .switch-active {
margin-left: -4rem
}
.switch[type=checkbox].is-medium.is-rtl + label {
padding-left: 0;
padding-right: 4.25rem
}
.switch[type=checkbox].is-medium.is-rtl + label::before, .switch[type=checkbox].is-medium.is-rtl + label:before {
left: auto;
right: 0
}
.switch[type=checkbox].is-medium.is-rtl + label::after, .switch[type=checkbox].is-medium.is-rtl + label:after {
left: auto;
right: 2rem
}
.switch[type=checkbox].is-medium:checked + label::before, .switch[type=checkbox].is-medium:checked + label:before {
background: #00d1b2
}
.switch[type=checkbox].is-medium:checked + label::after {
left: 2rem
}
.switch[type=checkbox].is-medium:checked.is-rtl + label::after, .switch[type=checkbox].is-medium:checked.is-rtl + label:after {
left: auto;
right: .25rem
}
.switch[type=checkbox].is-medium.is-outlined + label::before, .switch[type=checkbox].is-medium.is-outlined + label:before {
background-color: transparent;
border-color: #b5b5b5
}
.switch[type=checkbox].is-medium.is-outlined + label::after, .switch[type=checkbox].is-medium.is-outlined + label:after {
background: #b5b5b5
}
.switch[type=checkbox].is-medium.is-outlined:checked + label::before, .switch[type=checkbox].is-medium.is-outlined:checked + label:before {
background-color: transparent;
border-color: #00d1b2
}
.switch[type=checkbox].is-medium.is-outlined:checked + label::after, .switch[type=checkbox].is-medium.is-outlined:checked + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-medium.is-thin + label::before, .switch[type=checkbox].is-medium.is-thin + label:before {
top: .6818181819rem;
height: .46875rem
}
.switch[type=checkbox].is-medium.is-thin + label::after, .switch[type=checkbox].is-medium.is-thin + label:after {
box-shadow: 0 0 3px #7a7a7a
}
.switch[type=checkbox].is-medium.is-rounded + label::before, .switch[type=checkbox].is-medium.is-rounded + label:before {
border-radius: 24px
}
.switch[type=checkbox].is-medium.is-rounded + label::after, .switch[type=checkbox].is-medium.is-rounded + label:after {
border-radius: 50%
}
.switch[type=checkbox].is-large + label {
position: relative;
display: inline-flex;
align-items: center;
justify-content: flex-start;
font-size: 1.5rem;
height: 2.5em;
line-height: 1.5;
padding-left: 5rem;
padding-top: .2rem;
cursor: pointer
}
.switch[type=checkbox].is-large + label::before, .switch[type=checkbox].is-large + label:before {
position: absolute;
display: block;
top: calc(50% - 2.25rem * .5);
left: 0;
width: 4.5rem;
height: 2.25rem;
border: .1rem solid transparent;
border-radius: 4px;
background: #b5b5b5;
content: ""
}
.switch[type=checkbox].is-large + label::after, .switch[type=checkbox].is-large + label:after {
display: block;
position: absolute;
top: calc(50% - 1.75rem * .5);
left: .25rem;
width: 1.75rem;
height: 1.75rem;
transform: translate3d(0, 0, 0);
border-radius: 4px;
background: #fff;
transition: all .25s ease-out;
content: ""
}
.switch[type=checkbox].is-large + label .switch-active, .switch[type=checkbox].is-large + label .switch-inactive {
font-size: 1.4rem;
z-index: 1;
margin-top: -4px
}
.switch[type=checkbox].is-large + label.has-text-inside .switch-inactive {
margin-left: -2.675rem
}
.switch[type=checkbox].is-large + label.has-text-inside .switch-active {
margin-left: -4.75rem
}
.switch[type=checkbox].is-large.is-rtl + label {
padding-left: 0;
padding-right: 5rem
}
.switch[type=checkbox].is-large.is-rtl + label::before, .switch[type=checkbox].is-large.is-rtl + label:before {
left: auto;
right: 0
}
.switch[type=checkbox].is-large.is-rtl + label::after, .switch[type=checkbox].is-large.is-rtl + label:after {
left: auto;
right: 2.375rem
}
.switch[type=checkbox].is-large:checked + label::before, .switch[type=checkbox].is-large:checked + label:before {
background: #00d1b2
}
.switch[type=checkbox].is-large:checked + label::after {
left: 2.375rem
}
.switch[type=checkbox].is-large:checked.is-rtl + label::after, .switch[type=checkbox].is-large:checked.is-rtl + label:after {
left: auto;
right: .25rem
}
.switch[type=checkbox].is-large.is-outlined + label::before, .switch[type=checkbox].is-large.is-outlined + label:before {
background-color: transparent;
border-color: #b5b5b5
}
.switch[type=checkbox].is-large.is-outlined + label::after, .switch[type=checkbox].is-large.is-outlined + label:after {
background: #b5b5b5
}
.switch[type=checkbox].is-large.is-outlined:checked + label::before, .switch[type=checkbox].is-large.is-outlined:checked + label:before {
background-color: transparent;
border-color: #00d1b2
}
.switch[type=checkbox].is-large.is-outlined:checked + label::after, .switch[type=checkbox].is-large.is-outlined:checked + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-large.is-thin + label::before, .switch[type=checkbox].is-large.is-thin + label:before {
top: .8181818183rem;
height: .5625rem
}
.switch[type=checkbox].is-large.is-thin + label::after, .switch[type=checkbox].is-large.is-thin + label:after {
box-shadow: 0 0 3px #7a7a7a
}
.switch[type=checkbox].is-large.is-rounded + label::before, .switch[type=checkbox].is-large.is-rounded + label:before {
border-radius: 24px
}
.switch[type=checkbox].is-large.is-rounded + label::after, .switch[type=checkbox].is-large.is-rounded + label:after {
border-radius: 50%
}
.switch[type=checkbox].is-white + label .switch-active {
display: none
}
.switch[type=checkbox].is-white + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-white:checked + label::before, .switch[type=checkbox].is-white:checked + label:before {
background: #fff
}
.switch[type=checkbox].is-white:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-white:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-white.is-outlined:checked + label::before, .switch[type=checkbox].is-white.is-outlined:checked + label:before {
background-color: transparent;
border-color: #fff !important
}
.switch[type=checkbox].is-white.is-outlined:checked + label::after, .switch[type=checkbox].is-white.is-outlined:checked + label:after {
background: #fff
}
.switch[type=checkbox].is-white.is-thin.is-outlined + label::after, .switch[type=checkbox].is-white.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-white + label::before, .switch[type=checkbox].is-unchecked-white + label:before {
background: #fff
}
.switch[type=checkbox].is-unchecked-white.is-outlined + label::before, .switch[type=checkbox].is-unchecked-white.is-outlined + label:before {
background-color: transparent;
border-color: #fff !important
}
.switch[type=checkbox].is-unchecked-white.is-outlined + label::after, .switch[type=checkbox].is-unchecked-white.is-outlined + label:after {
background: #fff
}
.switch[type=checkbox].is-black + label .switch-active {
display: none
}
.switch[type=checkbox].is-black + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-black:checked + label::before, .switch[type=checkbox].is-black:checked + label:before {
background: #0a0a0a
}
.switch[type=checkbox].is-black:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-black:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-black.is-outlined:checked + label::before, .switch[type=checkbox].is-black.is-outlined:checked + label:before {
background-color: transparent;
border-color: #0a0a0a !important
}
.switch[type=checkbox].is-black.is-outlined:checked + label::after, .switch[type=checkbox].is-black.is-outlined:checked + label:after {
background: #0a0a0a
}
.switch[type=checkbox].is-black.is-thin.is-outlined + label::after, .switch[type=checkbox].is-black.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-black + label::before, .switch[type=checkbox].is-unchecked-black + label:before {
background: #0a0a0a
}
.switch[type=checkbox].is-unchecked-black.is-outlined + label::before, .switch[type=checkbox].is-unchecked-black.is-outlined + label:before {
background-color: transparent;
border-color: #0a0a0a !important
}
.switch[type=checkbox].is-unchecked-black.is-outlined + label::after, .switch[type=checkbox].is-unchecked-black.is-outlined + label:after {
background: #0a0a0a
}
.switch[type=checkbox].is-light + label .switch-active {
display: none
}
.switch[type=checkbox].is-light + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-light:checked + label::before, .switch[type=checkbox].is-light:checked + label:before {
background: #f5f5f5
}
.switch[type=checkbox].is-light:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-light:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-light.is-outlined:checked + label::before, .switch[type=checkbox].is-light.is-outlined:checked + label:before {
background-color: transparent;
border-color: #f5f5f5 !important
}
.switch[type=checkbox].is-light.is-outlined:checked + label::after, .switch[type=checkbox].is-light.is-outlined:checked + label:after {
background: #f5f5f5
}
.switch[type=checkbox].is-light.is-thin.is-outlined + label::after, .switch[type=checkbox].is-light.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-light + label::before, .switch[type=checkbox].is-unchecked-light + label:before {
background: #f5f5f5
}
.switch[type=checkbox].is-unchecked-light.is-outlined + label::before, .switch[type=checkbox].is-unchecked-light.is-outlined + label:before {
background-color: transparent;
border-color: #f5f5f5 !important
}
.switch[type=checkbox].is-unchecked-light.is-outlined + label::after, .switch[type=checkbox].is-unchecked-light.is-outlined + label:after {
background: #f5f5f5
}
.switch[type=checkbox].is-dark + label .switch-active {
display: none
}
.switch[type=checkbox].is-dark + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-dark:checked + label::before, .switch[type=checkbox].is-dark:checked + label:before {
background: #363636
}
.switch[type=checkbox].is-dark:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-dark:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-dark.is-outlined:checked + label::before, .switch[type=checkbox].is-dark.is-outlined:checked + label:before {
background-color: transparent;
border-color: #363636 !important
}
.switch[type=checkbox].is-dark.is-outlined:checked + label::after, .switch[type=checkbox].is-dark.is-outlined:checked + label:after {
background: #363636
}
.switch[type=checkbox].is-dark.is-thin.is-outlined + label::after, .switch[type=checkbox].is-dark.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-dark + label::before, .switch[type=checkbox].is-unchecked-dark + label:before {
background: #363636
}
.switch[type=checkbox].is-unchecked-dark.is-outlined + label::before, .switch[type=checkbox].is-unchecked-dark.is-outlined + label:before {
background-color: transparent;
border-color: #363636 !important
}
.switch[type=checkbox].is-unchecked-dark.is-outlined + label::after, .switch[type=checkbox].is-unchecked-dark.is-outlined + label:after {
background: #363636
}
.switch[type=checkbox].is-primary + label .switch-active {
display: none
}
.switch[type=checkbox].is-primary + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-primary:checked + label::before, .switch[type=checkbox].is-primary:checked + label:before {
background: #00d1b2
}
.switch[type=checkbox].is-primary:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-primary:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-primary.is-outlined:checked + label::before, .switch[type=checkbox].is-primary.is-outlined:checked + label:before {
background-color: transparent;
border-color: #00d1b2 !important
}
.switch[type=checkbox].is-primary.is-outlined:checked + label::after, .switch[type=checkbox].is-primary.is-outlined:checked + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-primary.is-thin.is-outlined + label::after, .switch[type=checkbox].is-primary.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-primary + label::before, .switch[type=checkbox].is-unchecked-primary + label:before {
background: #00d1b2
}
.switch[type=checkbox].is-unchecked-primary.is-outlined + label::before, .switch[type=checkbox].is-unchecked-primary.is-outlined + label:before {
background-color: transparent;
border-color: #00d1b2 !important
}
.switch[type=checkbox].is-unchecked-primary.is-outlined + label::after, .switch[type=checkbox].is-unchecked-primary.is-outlined + label:after {
background: #00d1b2
}
.switch[type=checkbox].is-link + label .switch-active {
display: none
}
.switch[type=checkbox].is-link + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-link:checked + label::before, .switch[type=checkbox].is-link:checked + label:before {
background: #485fc7
}
.switch[type=checkbox].is-link:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-link:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-link.is-outlined:checked + label::before, .switch[type=checkbox].is-link.is-outlined:checked + label:before {
background-color: transparent;
border-color: #485fc7 !important
}
.switch[type=checkbox].is-link.is-outlined:checked + label::after, .switch[type=checkbox].is-link.is-outlined:checked + label:after {
background: #485fc7
}
.switch[type=checkbox].is-link.is-thin.is-outlined + label::after, .switch[type=checkbox].is-link.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-link + label::before, .switch[type=checkbox].is-unchecked-link + label:before {
background: #485fc7
}
.switch[type=checkbox].is-unchecked-link.is-outlined + label::before, .switch[type=checkbox].is-unchecked-link.is-outlined + label:before {
background-color: transparent;
border-color: #485fc7 !important
}
.switch[type=checkbox].is-unchecked-link.is-outlined + label::after, .switch[type=checkbox].is-unchecked-link.is-outlined + label:after {
background: #485fc7
}
.switch[type=checkbox].is-info + label .switch-active {
display: none
}
.switch[type=checkbox].is-info + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-info:checked + label::before, .switch[type=checkbox].is-info:checked + label:before {
background: #3e8ed0
}
.switch[type=checkbox].is-info:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-info:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-info.is-outlined:checked + label::before, .switch[type=checkbox].is-info.is-outlined:checked + label:before {
background-color: transparent;
border-color: #3e8ed0 !important
}
.switch[type=checkbox].is-info.is-outlined:checked + label::after, .switch[type=checkbox].is-info.is-outlined:checked + label:after {
background: #3e8ed0
}
.switch[type=checkbox].is-info.is-thin.is-outlined + label::after, .switch[type=checkbox].is-info.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-info + label::before, .switch[type=checkbox].is-unchecked-info + label:before {
background: #3e8ed0
}
.switch[type=checkbox].is-unchecked-info.is-outlined + label::before, .switch[type=checkbox].is-unchecked-info.is-outlined + label:before {
background-color: transparent;
border-color: #3e8ed0 !important
}
.switch[type=checkbox].is-unchecked-info.is-outlined + label::after, .switch[type=checkbox].is-unchecked-info.is-outlined + label:after {
background: #3e8ed0
}
.switch[type=checkbox].is-success + label .switch-active {
display: none
}
.switch[type=checkbox].is-success + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-success:checked + label::before, .switch[type=checkbox].is-success:checked + label:before {
background: #48c78e
}
.switch[type=checkbox].is-success:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-success:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-success.is-outlined:checked + label::before, .switch[type=checkbox].is-success.is-outlined:checked + label:before {
background-color: transparent;
border-color: #48c78e !important
}
.switch[type=checkbox].is-success.is-outlined:checked + label::after, .switch[type=checkbox].is-success.is-outlined:checked + label:after {
background: #48c78e
}
.switch[type=checkbox].is-success.is-thin.is-outlined + label::after, .switch[type=checkbox].is-success.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-success + label::before, .switch[type=checkbox].is-unchecked-success + label:before {
background: #48c78e
}
.switch[type=checkbox].is-unchecked-success.is-outlined + label::before, .switch[type=checkbox].is-unchecked-success.is-outlined + label:before {
background-color: transparent;
border-color: #48c78e !important
}
.switch[type=checkbox].is-unchecked-success.is-outlined + label::after, .switch[type=checkbox].is-unchecked-success.is-outlined + label:after {
background: #48c78e
}
.switch[type=checkbox].is-warning + label .switch-active {
display: none
}
.switch[type=checkbox].is-warning + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-warning:checked + label::before, .switch[type=checkbox].is-warning:checked + label:before {
background: #ffe08a
}
.switch[type=checkbox].is-warning:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-warning:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-warning.is-outlined:checked + label::before, .switch[type=checkbox].is-warning.is-outlined:checked + label:before {
background-color: transparent;
border-color: #ffe08a !important
}
.switch[type=checkbox].is-warning.is-outlined:checked + label::after, .switch[type=checkbox].is-warning.is-outlined:checked + label:after {
background: #ffe08a
}
.switch[type=checkbox].is-warning.is-thin.is-outlined + label::after, .switch[type=checkbox].is-warning.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-warning + label::before, .switch[type=checkbox].is-unchecked-warning + label:before {
background: #ffe08a
}
.switch[type=checkbox].is-unchecked-warning.is-outlined + label::before, .switch[type=checkbox].is-unchecked-warning.is-outlined + label:before {
background-color: transparent;
border-color: #ffe08a !important
}
.switch[type=checkbox].is-unchecked-warning.is-outlined + label::after, .switch[type=checkbox].is-unchecked-warning.is-outlined + label:after {
background: #ffe08a
}
.switch[type=checkbox].is-danger + label .switch-active {
display: none
}
.switch[type=checkbox].is-danger + label .switch-inactive {
display: inline-block
}
.switch[type=checkbox].is-danger:checked + label::before, .switch[type=checkbox].is-danger:checked + label:before {
background: #f14668
}
.switch[type=checkbox].is-danger:checked + label .switch-active {
display: inline-block
}
.switch[type=checkbox].is-danger:checked + label .switch-inactive {
display: none
}
.switch[type=checkbox].is-danger.is-outlined:checked + label::before, .switch[type=checkbox].is-danger.is-outlined:checked + label:before {
background-color: transparent;
border-color: #f14668 !important
}
.switch[type=checkbox].is-danger.is-outlined:checked + label::after, .switch[type=checkbox].is-danger.is-outlined:checked + label:after {
background: #f14668
}
.switch[type=checkbox].is-danger.is-thin.is-outlined + label::after, .switch[type=checkbox].is-danger.is-thin.is-outlined + label:after {
box-shadow: none
}
.switch[type=checkbox].is-unchecked-danger + label::before, .switch[type=checkbox].is-unchecked-danger + label:before {
background: #f14668
}
.switch[type=checkbox].is-unchecked-danger.is-outlined + label::before, .switch[type=checkbox].is-unchecked-danger.is-outlined + label:before {
background-color: transparent;
border-color: #f14668 !important
}
.switch[type=checkbox].is-unchecked-danger.is-outlined + label::after, .switch[type=checkbox].is-unchecked-danger.is-outlined + label:after {
background: #f14668
}
.field-body .switch[type=checkbox] + label {
margin-top: .375em
}

24828
ui/assets/css/bulma.css vendored

File diff suppressed because it is too large Load diff

View file

@ -1,209 +0,0 @@
* {
unicode-bidi: plaintext;
}
.container {
padding: 1em;
margin-top: 1em;
}
.container,
.card,
.box,
.navbar {
border-radius: 15px;
}
.is-bordered-danger {
border: 1px solid red;
}
.is-bordered-info {
border: 1px solid #3e8ed0;
}
html,
body {
background-color: #eaeaea;
}
.container {
background-color: #ffffff;
}
hr {
background-color: #000;
}
.is-unselectable {
user-select: none;
}
.is-text-overflow {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (prefers-color-scheme: dark) {
* {
unicode-bidi: plaintext;
}
.container {
padding: 1em;
margin-top: 1em;
}
.container,
.card,
.box,
.navbar {
border-radius: 15px;
}
.is-bordered-danger {
border: 1px solid red;
}
.is-bordered-info {
border: 1px solid #3e8ed0;
}
hr {
background-color: #fff;
}
html,
body {
background-color: #000000;
}
.container {
background-color: #0f1010;
}
.is-unselectable {
user-select: none;
}
.is-text-overflow {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.is-borderless {
border: none;
}
.is-marginless {
margin: 0 !important;
}
.is-paddingless {
padding: 0 !important;
}
.section {
padding: 3rem 3rem;
}
.section.is-medium {
padding: 9rem 4.5rem;
}
.section.is-large {
padding: 18rem 6rem;
}
.footer {
background-color: #fafafa;
padding: 3rem 1.5rem 6rem;
}
.progress-bar {
border-radius: 15px;
position: relative;
width: 100%;
height: 30px;
background-color: #F5F5F5;
}
.progress,
.progress-percentage {
border-radius: 15px;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.progress-percentage {
text-align: center;
z-index: 2;
line-height: 30px;
}
.progress {
z-index: 1;
background-color: #00d1b2;
}
@media (prefers-color-scheme: dark) {
.footer {
background-color: #121212;
}
.progress-bar {
border-radius: 15px;
position: relative;
width: 100%;
height: 30px;
background-color: #383636;
}
.progress,
.progress-percentage {
border-radius: 15px;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
}
.progress-percentage {
text-align: center;
z-index: 2;
line-height: 30px;
}
.progress {
z-index: 1;
background-color: #087363;
}
}
.vue-notification {
background: unset;
border-left: none;
}
.notification-title {
font-size: 1.5em;
font-weight: bold;
}
.notification-content {
font-size: 1.2em;
}
.vue-notification-wrapper {
cursor: pointer;
padding-top: 0.5em;
}

Binary file not shown.

Binary file not shown.

View file

@ -1,380 +0,0 @@
<template>
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showCompleted = !showCompleted">
<span class="icon-text title is-4">
<span class="icon">
<i :class="showCompleted ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
</span>
<span>History <span v-if="hasItems">({{ getTotal }})</span></span>
</span>
</h1>
<div v-if="showCompleted">
<div class="columns is-multiline is-mobile has-text-centered" v-if="hasItems">
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-ghost is-inverted"
@click="masterSelectAll = !masterSelectAll">
<span class="icon-text is-block">
<span class="icon">
<i :class="!masterSelectAll ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" />
</span>
<span v-if="!masterSelectAll">Select All</span>
<span v-else>Unselect All</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected"
@click="$emit('deleteItem', 'completed', selectedElms); selectedElms = []">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Remove Selected</span>
</span>
</button>
</div>
<div class="column is-half-mobile" v-if="hasCompleted">
<button type="button" class="button is-fullwidth is-primary is-inverted" @click="clearCompleted">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-circle-check" />
</span>
<span>Clear Completed</span>
</span>
</button>
</div>
<div class="column is-half-mobile" v-if="hasIncomplete">
<button type="button" class="button is-fullwidth is-info is-inverted" @click="clearIncomplete">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-circle-xmark" />
</span>
<span>Clear Incomplete</span>
</span>
</button>
</div>
<div class="column is-half-mobile" v-if="hasIncomplete">
<button type="button" class="button is-fullwidth is-warning is-inverted" @click="requeueIncomplete">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-rotate-right" />
</span>
<span>Re-queue Incomplete</span>
</span>
</button>
</div>
<div class="column is-1-tablet">
<button type="button" class="button is-fullwidth" @click="direction = direction === 'desc' ? 'asc' : 'desc'">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid" :class="direction === 'desc' ? 'fa-arrow-down-a-z' : 'fa-arrow-up-a-z'" />
</span>
</span>
</button>
</div>
</div>
<div class="columns is-multiline">
<LazyLoader :unrender="true" :min-height="210" class="column is-6" v-for="item in sortCompleted" :key="item._id">
<div class="card"
:class="{ 'is-bordered-danger': item.status === 'error', 'is-bordered-info': item.live_in || item.is_live }">
<header class="card-header has-tooltip">
<div class="card-header-title is-text-overflow is-block" v-tooltip="item.title">
<a v-if="item.filename" referrerpolicy="no-referrer" :href="makeDownload(config, item, 'm3u8')"
@click.prevent="$emit('playItem', item)">
{{ item.title }}
</a>
<span v-else>{{ item.title }}</span>
</div>
<div class="card-header-icon" v-if="item.filename">
<a :href="makeDownload(config, item)" :download="item.filename?.split('/').reverse()[0]"
class="has-text-primary" v-tooltip="'Download item.'">
<span class="icon"><i class="fa-solid fa-download" /></span>
</a>
</div>
</header>
<div class="card-content">
<div class="columns is-mobile is-multiline">
<div class="column is-12" v-if="item.live_in">
<span class="has-text-info">
LIVE stream is scheduled to start at {{ moment(item.live_in).format() }}
</span>
</div>
<div class="column is-12" v-if="item.error">
<span class="has-text-danger">{{ item.error }}</span>
</div>
<div class="column is-12" v-if="showMessage(item)">
<span class="has-text-danger">{{ item.msg }}</span>
</div>
<div class="column is-half-mobile has-text-centered">
<span v-if="!item.live_in && !item.is_live">
<span class="icon-text">
<span class="icon"
:class="{ 'has-text-success': item.status === 'finished', 'has-text-danger': item.status !== 'finished' }">
<i :class="setIcon(item)" />
</span>
<span v-if="item.status == 'finished' && item.is_live">Stream Ended</span>
<span v-else>{{ ucFirst(item.status) }}</span>
</span>
</span>
<span v-else>
<span class="icon-text">
<span class="icon has-text-info">
<i class="fa-solid fa-calendar" />
</span>
<span>Live Stream</span>
</span>
</span>
</div>
<div class="column is-half-mobile has-text-centered">
<span :date-datetime="item.datetime" v-tooltip="moment(item.datetime).format('YYYY-M-DD H:mm Z')">
{{ moment(item.datetime).fromNow() }}
</span>
</div>
<div class="column is-half-mobile has-text-centered" v-if="item.live_in && item.status != 'finished'">
<span :date-datetime="item.datetime"
v-tooltip="'Will start at: ' + moment(item.live_in).format('YYYY-M-DD H:mm Z')">
{{ moment(item.live_in).fromNow() }}
</span>
</div>
<div class="column is-half-mobile has-text-centered" v-if="item.file_size">
{{ formatBytes(item.file_size) }}
</div>
<div class="column is-half-mobile has-text-centered">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
:value="item._id">
Select
</label>
</div>
</div>
<div class="columns is-mobile is-multiline">
<div class="column is-half-mobile" v-if="item.status != 'finished'">
<a class="button is-warning is-fullwidth" v-tooltip="'Re-queue incomplete download.'"
@click="reQueueItem(item)">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-rotate-right" />
</span>
<span>Re-queue</span>
</span>
</a>
</div>
<div class="column is-half-mobile">
<a class="button is-danger is-fullwidth" @click="$emit('deleteItem', 'completed', item._id)">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Remove</span>
</span>
</a>
</div>
<div class="column is-half-mobile" v-if="config.app?.keep_archive && item.status != 'finished'">
<a class="button is-danger is-light is-fullwidth" v-tooltip="'Add link to archive.'"
@click="$emit('archiveItem', 'completed', item)">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-box-archive" />
</span>
<span>Archive</span>
</span>
</a>
</div>
<div class="column is-half-mobile">
<a referrerpolicy="no-referrer" class="button is-link is-fullwidth" target="_blank" :href="item.url">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-up-right-from-square" />
</span>
<span>Visit Link</span>
</span>
</a>
</div>
</div>
</div>
</div>
</LazyLoader>
</div>
<div class="content has-text-centered" v-if="!hasItems">
<p v-if="config.isConnected">
<span class="icon-text">
<span class="icon has-text-success">
<i class="fa-solid fa-circle-check" />
</span>
<span>No records.</span>
</span>
</p>
<p v-else>
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-spinner fa-spin" />
</span>
<span>Connecting...</span>
</span>
</p>
</div>
</div>
</template>
<script setup>
import { defineProps, computed, ref, watch, defineEmits } from 'vue';
import moment from "moment";
import { useStorage } from '@vueuse/core'
import LazyLoader from './LazyLoader'
import { makeDownload, formatBytes, ucFirst } from '~/utils/index'
const emits = defineEmits(['deleteItem', 'addItem', 'playItem', 'archiveItem'])
const config = useConfigStore()
const stateStore = useStateStore()
const selectedElms = ref([]);
const masterSelectAll = ref(false);
const showCompleted = useStorage('showCompleted', true)
const direction = useStorage('sortCompleted', 'desc')
watch(masterSelectAll, (value) => {
for (const key in stateStore.history) {
const element = stateStore.history[key];
if (value) {
selectedElms.value.push(element._id);
} else {
selectedElms.value = [];
}
}
})
const sortCompleted = computed(() => {
const thisDirection = direction.value;
return Object.values(stateStore.history).sort((a, b) => {
if ('asc' === thisDirection) {
return new Date(a.datetime) - new Date(b.datetime);
}
return new Date(b.datetime) - new Date(a.datetime);
})
})
const hasSelected = computed(() => selectedElms.value.length > 0)
const hasItems = computed(() => stateStore.count('history') > 0)
const getTotal = computed(() => stateStore.count('history'));
const showMessage = (item) => {
if (!item?.msg || item.msg === item?.error) {
return false
}
return item.msg.length > 0;
}
const hasIncomplete = computed(() => {
if (Object.keys(stateStore.history)?.length < 0) {
return false;
}
for (const key in stateStore.history) {
const element = stateStore.history[key];
if (element.status !== 'finished') {
return true;
}
}
return false;
})
const hasCompleted = computed(() => {
if (Object.keys(stateStore.history)?.length < 0) {
return false;
}
for (const key in stateStore.history) {
const element = stateStore.history[key];
if (element.status === 'finished') {
return true;
}
}
return false;
})
const clearCompleted = () => {
const state = confirm('Are you sure you want to clear all completed downloads?');
if (false === state) {
return;
}
const keys = {};
for (const key in stateStore.history) {
if (stateStore.history[key].status === 'finished') {
keys[key] = stateStore.history[key]._id;
stateStore.remove('history', key);
}
}
}
const clearIncomplete = () => {
if (false === confirm('Are you sure you want to clear all incomplete downloads?')) {
return;
}
for (const key in stateStore.history) {
if (stateStore.history[key].status !== 'finished') {
stateStore.remove('history', key);
}
}
}
const setIcon = item => {
if (item.status === 'finished' && item.is_live) {
return 'fa-solid fa-globe';
}
if (item.status === 'finished') {
return 'fa-solid fa-circle-check';
}
if (item.status === 'error') {
return 'fa-solid fa-circle-xmark';
}
if (item.status === 'canceled') {
return 'fa-solid fa-eject';
}
return 'fa-solid fa-circle';
}
const requeueIncomplete = () => {
if (false === confirm('Are you sure you want to re-queue all incomplete downloads?')) {
return false;
}
for (const key in stateStore.history) {
const item = stateStore.history[key];
if (item.status !== 'finished') {
emits('deleteItem', 'completed', key);
emits('addItem', {
url: item.url,
format: item.format,
quality: item.quality,
folder: item.folder,
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
});
}
}
}
const reQueueItem = (item) => {
emits('deleteItem', 'completed', item._id);
emits('addItem', {
url: item.url,
format: item.format,
quality: item.quality,
folder: item.folder,
ytdlp_config: item.ytdlp_config,
ytdlp_cookies: item.ytdlp_cookies,
output_template: item.output_template,
});
}
</script>

View file

@ -1,76 +0,0 @@
<template>
<div ref="targetEl" :style="`min-height:${fixedMinHeight ? fixedMinHeight : (minHeight)}px`">
<slot v-if="shouldRender"/>
</div>
</template>
<script>
import {nextTick, ref} from 'vue'
import {useIntersectionObserver} from '@vueuse/core'
function onIdle(cb = () => {
}) {
if ("requestIdleCallback" in window) {
window.requestIdleCallback(cb);
} else {
setTimeout(() => nextTick(cb), 300);
}
}
export default {
props: {
renderOnIdle: Boolean,
unrender: Boolean,
minHeight: Number,
unrenderDelay: {
type: Number,
default: 6000,
},
},
setup(props) {
const shouldRender = ref(false);
const targetEl = ref();
const fixedMinHeight = ref(0);
let unrenderTimer;
let renderTimer;
const {stop} = useIntersectionObserver(targetEl, ([{isIntersecting}]) => {
if (isIntersecting) {
// perhaps the user re-scrolled to a component that was set to unrender. In that case stop the un-rendering timer
clearTimeout(unrenderTimer);
// if we're dealing under-rendering lets add a waiting period of 200ms before rendering.
// If a component enters the viewport and also leaves it within 200ms it will not render at all.
// This saves work and improves performance when user scrolls very fast
renderTimer = setTimeout(() => (shouldRender.value = true), props.unrender ? 200 : 0);
shouldRender.value = true;
if (!props.unrender) {
stop();
}
} else if (props.unrender) {
// if the component was set to render, cancel that
clearTimeout(renderTimer);
unrenderTimer = setTimeout(() => {
if (targetEl.value?.clientHeight) {
fixedMinHeight.value = targetEl.value.clientHeight;
}
shouldRender.value = false;
}, props.unrenderDelay);
}
}, {
rootMargin: "600px",
}
);
if (props.renderOnIdle) {
onIdle(() => {
shouldRender.value = true;
if (!props.unrender) {
stop();
}
});
}
return {targetEl, shouldRender, fixedMinHeight};
},
};
</script>

View file

@ -1,291 +0,0 @@
<template>
<h1 class="mt-3 is-size-3 is-clickable is-unselectable" @click="showQueue = !showQueue">
<span class="icon-text title is-4">
<span class="icon">
<i class="fas" :class="showQueue ? 'fa-solid fa-arrow-up' : 'fa-solid fa-arrow-down'" />
</span>
<span>Queue <span v-if="hasQueuedItems">({{ stateStore.count('queue') }})</span></span>
</span>
</h1>
<div v-if="showQueue">
<div class="columns is-multiline is-mobile has-text-centered" v-if="hasQueuedItems">
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-ghost" @click="masterSelectAll = !masterSelectAll">
<span class="icon-text is-block">
<span class="icon">
<i class="fas" :class="!masterSelectAll ? 'fa-regular fa-square-check' : 'fa-regular fa-square'" />
</span>
<span v-if="!masterSelectAll">Select All</span>
<span v-else>Unselect All</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<button type="button" class="button is-fullwidth is-danger" :disabled="!hasSelected"
@click="$emit('deleteItem', 'queue', selectedElms); selectedElms = []">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Cancel Selected</span>
</span>
</button>
</div>
</div>
<div class="columns is-multiline">
<LazyLoader :unrender="true" :min-height="265" class="column is-6" v-for="item in stateStore.history"
:key="item._id">
<div class="card">
<header class="card-header has-tooltip" v-tooltip="item.title">
<div class="card-header-title has-text-centered is-text-overflow is-block">
{{ item.title }}
</div>
</header>
<div class="card-content">
<div class="columns is-multiline is-mobile">
<div class="column is-12">
<div class="progress-bar is-round">
<div class="progress-percentage">{{ updateProgress(item) }}</div>
<div class="progress" :style="{ width: percentPipe(item.percent) + '%' }"></div>
</div>
</div>
<div class="column is-half-mobile has-text-centered">
<span class="icon-text">
<span class="icon" :class="{ 'has-text-success': item.status == 'downloading' }">
<i class="fas" :class="setIcon(item)" />
</span>
<span v-if="item.status == 'downloading' && item.is_live">Live Streaming</span>
<span v-else>{{ ucFirst(item.status) }}</span>
</span>
</div>
<div class="column is-half-mobile has-text-centered">
<span :data-datetime="item.datetime"
v-tooltip="moment(item.datetime).format('MMMM Do YYYY, h:mm:ss a')">
{{ moment(item.datetime).fromNow() }}
</span>
</div>
<div class="column is-half-mobile has-text-centered">
<label class="checkbox is-block">
<input class="completed-checkbox" type="checkbox" v-model="selectedElms" :id="'checkbox-' + item._id"
:value="item._id">
Select
</label>
</div>
</div>
<div class="columns is-multiline is-mobile">
<div class="column is-half-mobile">
<button class="button is-danger is-fullwidth" @click="confirmCancel(item);">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-trash-can" />
</span>
<span>Cancel</span>
</span>
</button>
</div>
<div class="column is-half-mobile">
<a referrerpolicy="no-referrer" class="button is-link is-fullwidth" target="_blank" :href="item.url">
<span class="icon-text is-block">
<span class="icon">
<i class="fa-solid fa-up-right-from-square" />
</span>
<span>Visit Link</span>
</span>
</a>
</div>
</div>
</div>
</div>
</LazyLoader>
</div>
<div class="content has-text-centered" v-if="!hasQueuedItems">
<p v-if="socket.isConnected">
<span class="icon-text">
<span class="icon has-text-success">
<i class="fa-solid fa-circle-check" />
</span>
<span>No queued items.</span>
</span>
</p>
<p v-else>
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-spinner fa-spin" />
</span>
<span>Connecting...</span>
</span>
</p>
</div>
</div>
</template>
<script setup>
import moment from 'moment'
import { useStorage } from '@vueuse/core'
import LazyLoader from '~/components/LazyLoader'
import { ucFirst } from '~/utils/index'
const config = useConfigStore();
const stateStore = useStateStore();
const socket = useSocketStore();
const selectedElms = ref([]);
const masterSelectAll = ref(false);
const showQueue = useStorage('showQueue', true)
const actionId = ref(null);
watch(masterSelectAll, (value) => {
for (const key in stateStore.queue) {
const element = stateStore.queue[key];
if (value) {
selectedElms.value.push(element._id);
} else {
selectedElms.value = [];
}
}
})
const hasSelected = computed(() => selectedElms.value.length > 0)
const hasQueuedItems = computed(() => stateStore.count('history') > 0)
const getTotal = computed(() => stateStore.count('history'));
const setIcon = item => {
if (item.status === 'downloading' && item.is_live) {
return 'fa-solid fa-globe';
}
if (item.status === 'downloading') {
return 'fa-solid fa-circle-check';
}
return 'fa-solid fa-spinner fa-spin';
}
const ETAPipe = value => {
if (value === null || 0 === value) {
return 'Live';
}
if (value < 60) {
return `${Math.round(value)}s`;
}
if (value < 3600) {
return `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`;
}
const hours = Math.floor(value / 3600)
const minutes = value % 3600
return `${hours}h ${Math.floor(minutes / 60)}m ${Math.round(minutes % 60)}s`;
}
const speedPipe = value => {
if (value === null || 0 === value) {
return '0KB/s';
}
const k = 1024;
const dm = 2;
const sizes = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s', 'EB/s', 'ZB/s', 'YB/s'];
const i = Math.floor(Math.log(value) / Math.log(k));
return parseFloat((value / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
const percentPipe = value => {
if (value === null || 0 === value) {
return '00.00';
}
return parseFloat(value).toFixed(2);
}
const updateProgress = (item) => {
let string = '';
if (item.status == 'preparing') {
return 'Preparing';
}
if (item.status != null) {
string += item.percent && !item.is_live ? percentPipe(item.percent) + '%' : 'Live';
}
string += item.speed ? ' - ' + speedPipe(item.speed) : ' - Waiting..';
if (item.status != null && item.eta) {
string += ' - ' + ETAPipe(item.eta);
}
return string;
}
const confirmCancel = item => {
if (true !== confirm(`Are you sure you want to cancel (${item.title})?`)) {
return false;
}
deleteItem('queue', item._id);
return true;
}
const deleteItem = (type, item) => {
const items = []
if (typeof item === 'object') {
for (const key in item) {
items.push(item[key]);
}
} else {
items.push(item);
}
if (items.length < 0) {
return;
}
actionId.value = makeId();
socket.emit('cancel_items', {
identifier: actionId.value,
ids: items,
});
}
const cancelHandler = async ev => {
if (!actionId.value) {
return;
}
const data = JSON.parse(ev)
if (data.identifier !== actionId.value) {
console.log('Invalid action identifier', data);
return;
}
if ('error' === data.status) {
notification('error', 'Failed to cancel item/s.');
return;
}
for (const key in items) {
const itemId = items[key];
if (itemId in json && json[itemId] === 'ok') {
if (true === stateStore.has('history', itemId)) {
stateStore.remove('history', itemId);
}
if (true === stateStore.has('queue', itemId)) {
notification('info', 'Download canceled: ' + ag(stateStore.get(itemId), 'title'));
stateStore.remove('queue', itemId);
}
}
}
}
onMounted(() => {
socket.on('cancel_items', cancelHandler)
})
onUnmounted(() => {
socket.off('cancel_items', cancelHandler)
})
</script>

View file

@ -1,53 +0,0 @@
<template>
<NuxtLayout name="error-layout">
<div>
<div class="columns is-multiline">
<div class="column is-12">
<h1 class="title is-4">
{{ error.statusCode }}<span v-if="error.statusMessage"> - {{ error.statusMessage }}</span>
</h1>
</div>
</div>
<div class="column is-12" v-if="error.message">
<div class="notification has-background-warning-90 has-text-dark">
<p>{{ error.message }}</p>
</div>
</div>
<div class="column is-12" v-if="error.stack">
<h2 class="title is-5 is-clickable" @click="showStacks = !showStacks">
<span class="icon-text">
<span class="icon">
<i v-if="showStacks" class="fas fa-arrow-up"></i>
<i v-else class="fas fa-arrow-down"></i>
</span>
<span>Stack trace</span>
</span>
</h2>
<pre v-if="showStacks"><code>{{ error.stack }}</code></pre>
</div>
<div class="column is-12">
<h2 class="title is-5">
<NuxtLink to="/">
<span class="icon-text">
<span class="icon"><i class="fas fa-home"></i></span>
<span>Back to Home</span>
</span>
</NuxtLink>
</h2>
</div>
</div>
</NuxtLayout>
</template>
<script setup>
defineProps({
error: {
type: Object,
required: true
}
})
const showStacks = ref(false)
</script>

View file

@ -1,167 +0,0 @@
<template>
<div id="main_container" class="container">
<nav class="navbar is-mobile is-dark">
<div class="navbar-brand pl-5">
<NuxtLink class="navbar-item has-tooltip-bottom" to="/"
v-tooltip="socket.isConnected ? 'Connected' : 'Connecting'">
<span class="icon-text">
<span class="icon"><i class="fas fa-home"></i></span>
<span :class="socket.isConnected ? 'has-text-success' : 'has-text-danger'"><b>YTPTube</b></span>
</span>
</NuxtLink>
</div>
<div class="navbar-end is-flex">
<div class="navbar-item">
<NuxtLink class="button is-dark has-tooltip-bottom" to="/console">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>Terminal</span>
</span>
</NuxtLink>
</div>
<div class="navbar-item">
<NuxtLink v-tooltip.bottom="'Toggle Add Form'" class="button is-dark has-tooltip-bottom" to="/add">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span>Add</span>
</span>
</NuxtLink>
</div>
<div class="navbar-item" v-if="config.tasks.length > 0">
<button v-tooltip.bottom="'Toggle Tasks'" class="button is-dark has-tooltip-bottom"
@click="config.showTasks = !config.showTasks">
<span class="icon"><i class="fa-solid fa-tasks" /></span>
<span class="is-hidden-mobile">Tasks</span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="selectedTheme = 'light'" v-if="'dark' === selectedTheme"
v-tooltip="'Switch to light theme'">
<span class="icon has-text-warning"><i class="fas fa-sun"></i></span>
</button>
<button class="button is-dark" @click="selectedTheme = 'dark'" v-if="'light' === selectedTheme"
v-tooltip="'Switch to dark theme'">
<span class="icon"><i class="fas fa-moon"></i></span>
</button>
</div>
<div class="navbar-item">
<button class="button is-dark" @click="reloadPage">
<span class="icon"><i class="fas fa-refresh"></i></span>
</button>
</div>
</div>
</nav>
<NuxtPage />
<div class="columns mt-3 is-mobile">
<div class="column is-8-mobile">
<div class="has-text-left" v-if="config.app?.version">
© {{ Year }} - <a href="https://github.com/ArabCoders/ytptube" target="_blank">YTPTube</a>
<span class="is-hidden-mobile">&nbsp;({{ config.app?.version || 'unknown' }})</span>
- <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>
<span class="is-hidden-mobile">&nbsp;({{ config?.app.ytdlp_version || 'unknown' }})</span>
</div>
</div>
<div class="column is-4-mobile" v-if="config.app?.started">
<div class="has-text-right">
<span class="user-hint"
v-tooltip="'App Started: ' + moment.unix(config.app?.started).format('YYYY-M-DD H:mm Z')">
{{ moment.unix(config.app?.started).fromNow() }}
</span>
</div>
</div>
</div>
<NuxtNotifications position="top right" :speed="800" :ignoreDuplicates="true" :width="340" :pauseOnHover="true" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import 'assets/css/bulma.css'
import 'assets/css/style.css'
import 'assets/css/all.css'
import { useStorage } from '@vueuse/core'
import moment from "moment";
const Year = new Date().getFullYear()
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
const bus = useEventBus('show_form')
const socket = useSocketStore()
const config = useConfigStore()
const applyPreferredColorScheme = scheme => {
for (let s = 0; s < document.styleSheets.length; s++) {
for (let i = 0; i < document.styleSheets[s].cssRules.length; i++) {
try {
const rule = document.styleSheets[s].cssRules[i]
if (rule && rule.media && rule.media.mediaText.includes("prefers-color-scheme")) {
switch (scheme) {
case "light":
rule.media.appendMedium("original-prefers-color-scheme")
if (rule.media.mediaText.includes("light")) {
rule.media.deleteMedium("(prefers-color-scheme: light)")
}
if (rule.media.mediaText.includes("dark")) {
rule.media.deleteMedium("(prefers-color-scheme: dark)")
}
break
case "dark":
rule.media.appendMedium("(prefers-color-scheme: light)")
rule.media.appendMedium("(prefers-color-scheme: dark)")
if (rule.media.mediaText.includes("original")) {
rule.media.deleteMedium("original-prefers-color-scheme")
}
break
default:
rule.media.appendMedium("(prefers-color-scheme: dark)")
if (rule.media.mediaText.includes("light")) {
rule.media.deleteMedium("(prefers-color-scheme: light)")
}
if (rule.media.mediaText.includes("original")) {
rule.media.deleteMedium("original-prefers-color-scheme")
}
break
}
}
} catch (e) {
console.debug(e)
}
}
}
}
onMounted(async () => {
try {
applyPreferredColorScheme(selectedTheme.value)
} catch (e) {
}
})
watch(selectedTheme, value => {
try {
applyPreferredColorScheme(value)
} catch (e) { }
})
const reloadPage = () => window.location.reload()
const toggleForm = () => {
console.log('emitting show_form');
bus.emit('show_form', { foo: 'bar' })
dEvent('show_form', { foo: 'bar' })
}
</script>
<style>
.user-hint {
user-select: none;
cursor: help;
border-bottom: 1px dotted;
}
</style>

View file

@ -1,3 +0,0 @@
<template>
<slot />
</template>

View file

@ -1,21 +0,0 @@
<template>
<div class="container">
<nav class="navbar is-dark mb-5">
<div class="navbar-brand pl-5">
<NuxtLink class="navbar-item" to="/">
<span class="icon-text">
<span class="icon"><i class="fas fa-home"></i></span>
<span>Home</span>
</span>
</NuxtLink>
</div>
</nav>
<slot />
</div>
</template>
<script setup>
import 'assets/css/bulma.css'
import 'assets/css/style.css'
import 'assets/css/all.css'
</script>

View file

@ -1,54 +0,0 @@
import path from "path";
export default defineNuxtConfig({
ssr: false,
devtools: { enabled: false },
devServer: {
port: 8082,
host: "0.0.0.0",
},
runtimeConfig: {
public: {
domain: '/',
wss: ':8081/',
version: '2.0.0',
}
},
app: {
buildAssetsDir: "assets",
head: {
"meta": [
{ "charset": "utf-8" },
{ "name": "viewport", "content": "width=device-width, initial-scale=1.0, maximum-scale=1.0" },
{ "name": "theme-color", "content": "#000000" }
],
link: [{ rel: 'icon', type: 'image/png', href: '/favicon.png' }]
},
pageTransition: { name: 'page', mode: 'out-in' }
},
router: {
options: {
linkActiveClass: "is-selected",
}
},
modules: [
'@pinia/nuxt',
'@vueuse/nuxt',
'floating-vue/nuxt',
'nuxt3-notifications',
],
nitro: {
output: {
publicDir: path.join(__dirname, 'exported')
}
},
telemetry: false,
compatibilityDate: "2024-07-13",
})

View file

@ -1,31 +0,0 @@
{
"name": "nuxt-app",
"private": true,
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"web-types": "./web-types.json",
"dependencies": {
"@pinia/nuxt": "^0.5.1",
"@vueuse/core": "^10.9.0",
"@vueuse/nuxt": "^10.9.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"cronstrue": "^2.49.0",
"floating-vue": "^5.2.2",
"moment": "^2.30.1",
"nuxt": "^3.11.2",
"nuxt3-notifications": "^1.2.0",
"pinia": "^2.1.7",
"plyr": "^3.7.8",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"socket.io-client": "^4.7.2"
},
"devDependencies": {}
}

View file

@ -1,209 +0,0 @@
<template>
<main class="columns mt-2">
<div class="column">
<div class="box">
<div class="columns is-multiline">
<div class="column is-12">
<div class="control has-icons-left">
<input type="url" class="input" id="url" placeholder="Video or playlist link"
:disabled="!socket.isConnected || addInProgress" v-model="url">
<span class="icon is-small is-left">
<i class="fa-solid fa-link" />
</span>
</div>
</div>
<div class="column is-5">
<div class="field has-addons">
<div class="control">
<a href="#" class="button is-static">Preset</a>
</div>
<div class="control is-expanded">
<div class="select is-fullwidth">
<select id="preset" class="is-fullwidth" :disabled="!socket.isConnected" v-model="selectedPreset">
<option v-for="item in config.presets" :key="item.name" :value="item.format">
{{ item.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="column is-5">
<div class="field has-addons" v-tooltip="'Download path relative to ' + config.app.download_path">
<div class="control">
<a href="#" class="button is-static">Download Path</a>
</div>
<div class="control is-expanded">
<input type="text" class="input is-fullwidth" id="path" v-model="downloadPath" placeholder="Default"
:disabled="!socket.isConnected" list="directories">
</div>
</div>
</div>
<div class="column">
<button type="submit" class="button is-primary" @click="addDownload"
:class="{ 'is-loading': !socket.isConnected || addInProgress }"
:disabled="!socket.isConnected || addInProgress || !url">
<span class="icon"><i class="fa-solid fa-plus" /></span>
<span>Add</span>
</button>
</div>
<div class="column">
<button type="submit" class="button is-info" @click="showAdvanced = !showAdvanced"
v-tooltip="'Show advanced options'" :class="{ 'is-loading': !socket.isConnected }"
:disabled="!socket.isConnected">
<span class="icon"><i class="fa-solid fa-cog" /></span>
</button>
</div>
</div>
<div class="columns is-multiline" v-if="showAdvanced">
<div class="column is-12">
<div class="field">
<label class="label is-inline" for="output_format"
v-tooltip="'Default Format: ' + config.app.output_template">
Output Format
</label>
<div class="control">
<input type="text" class="input" v-model="output_template" id="output_format"
placeholder="Uses default format if non is given.">
</div>
<span class="subtitle is-6 has-text-info">
All format options can be found at <a class="has-text-danger" target="_blank"
referrerpolicy="no-referrer" href="https://github.com/yt-dlp/yt-dlp#output-template">this page</a>.
</span>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label is-inline" for="ytdlpConfig"
v-tooltip="'Extends current global yt-dlp config. (JSON)'">
JSON yt-dlp config
</label>
<div class="control">
<textarea class="textarea" id="ytdlpConfig" v-model="ytdlpConfig"
:disabled="!socket.isConnected"></textarea>
</div>
<span class="subtitle is-6 has-text-info">
Some config fields are ignored like cookiefile, path, and output_format etc.
Available option can be found at <a class="has-text-danger" target="_blank" referrerpolicy="no-referrer"
href="https://github.com/yt-dlp/yt-dlp/blob/a0b19d319a6ce8b7059318fa17a34b144fde1785/yt_dlp/YoutubeDL.py#L194">this
page</a>.
</span>
</div>
</div>
<div class="column is-6">
<div class="field">
<label class="label is-inline" for="ytdlpCookies" v-tooltip="'JSON exported cookies for downloading.'">
yt-dlp Cookies
</label>
<div class="control">
<textarea class="textarea" id="ytdlpCookies" v-model="ytdlpCookies"
:disabled="!socket.isConnected"></textarea>
</div>
<span class="subtitle is-6 has-text-info">
Use something like <a class="has-text-danger" href="https://github.com/jrie/flagCookies">flagCookies</a>
to extract cookies as JSON string.
</span>
</div>
</div>
<div class="column is-12 has-text-right">
<div class="field">
<div class="control">
<button type="submit" class="button is-danger" @click="resetConfig" :disabled="!socket.isConnected"
v-tooltip="'This configuration are stored locally in your browser.'">
<span class="icon">
<i class="fa-solid fa-trash" />
</span>
<span>Reset Local Configuration</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<datalist id="directories" v-if="config?.directories">
<option v-for="dir in config.directories" :key="dir" :value="dir" />
</datalist>
</main>
</template>
<script setup>
import { useStorage, useEventBus } from '@vueuse/core'
const config = useConfigStore();
const socket = useSocketStore();
const bus = useEventBus('item_added', 'task_edit');
const emits = defineEmits(['addItem']);
const selectedFormat = useStorage('selectedFormat', 'any')
const selectedPreset = useStorage('selectedPreset', 'default')
const selectedQuality = useStorage('selectedQuality', '')
const ytdlpConfig = useStorage('ytdlp_config', '')
const ytdlpCookies = useStorage('ytdlp_cookies', '')
const output_template = useStorage('output_template', null)
const downloadPath = useStorage('downloadPath', null)
const url = useStorage('downloadUrl', null)
const showAdvanced = useStorage('show_advanced', false)
const addInProgress = ref(false)
const addDownload = () => {
addInProgress.value = true;
socket.emit('add_url', {
url: url.value,
format: selectedFormat.value,
quality: selectedQuality.value,
folder: downloadPath.value,
ytdlp_config: ytdlpConfig.value,
ytdlp_cookies: ytdlpCookies.value,
output_template: output_template.value,
});
}
const resetConfig = () => {
if (!confirm('Are you sure you want to reset the local configuration? this will NOT delete any downloads or server configuration.')) {
return;
}
selectedFormat.value = 'any';
selectedQuality.value = '';
ytdlpConfig.value = '';
ytdlpCookies.value = '';
output_template.value = null;
url.value = '';
downloadPath.value = '';
}
bus.on((event, data) => {
const allowedEvents = ['item_added'];
if (!allowedEvents.includes(event)) {
return;
}
if ('item_added' === event) {
if (data?.status === 'ok') {
url.value = '';
}
addInProgress.value = false;
}
});
const statusHandler = async data => {
const { status, msg } = JSON.parse(data)
addInProgress.value = false
console.log(data)
if ('error' === status) {
notification('error', 'Add error', msg, 5000)
return
}
notification('success', 'Add success', msg, 3000)
await navigateTo('/')
}
onMounted(() => socket.on('status', statusHandler))
onUnmounted(() => socket.off('status', statusHandler))
</script>

View file

@ -1,162 +0,0 @@
<template>
<div class="mt-1 columns is-multiline">
<div class="column is-12 is-clearfix">
<h1 class="title is-4">Terminal</h1>
<div class="subtitle is-6">
You can use this terminal window to execute non-interactive commands. The interface is jailed to the
<code>yt-dlp</code>
</div>
</div>
<div class="column is-12">
<div class="card">
<header class="card-header">
<p class="card-header-title">
<span class="icon"><i class="fa-solid fa-terminal" /></span> Console Output
</p>
<p class="card-header-icon">
<span v-tooltip.top="'Clear console window'" class="icon" @click="clearOutput">
<i class="fa-solid fa-broom" /></span>
</p>
</header>
<section class="card-content p-0 m-0">
<div ref="terminal_window" style="min-height: 60vh;max-height:70vh;" />
</section>
<section class="card-content p-1 m-1">
<div class="field is-grouped">
<div class="control">
<span class="icon-text input is-unselectable">
<span class="icon"><i class="fa-solid fa-terminal" /></span>
<span>yt-dlp</span>
</span>
</div>
<div class="control is-expanded">
<input type="text" class="input" v-model="command" placeholder="--help" autocomplete="off"
ref="command_input" @keydown.enter="runCommand" :disabled="isLoading" id="command">
</div>
<p class="control">
<button class="button is-primary" type="button" :disabled="isLoading || '' === command"
@click="runCommand">
<span class="icon">
<i class="fa-solid fa-spinner" spin v-if="isLoading" />
<i class="fa-solid fa-paper-plane" v-else />
</span>
</button>
</p>
</div>
</section>
</div>
</div>
</div>
</template>
<script setup>
import "@xterm/xterm/css/xterm.css"
import { Terminal } from "@xterm/xterm"
import { FitAddon } from "@xterm/addon-fit"
const emitter = defineEmits(['runCommand', 'cli_clear']);
const terminal = ref()
const terminalFit = ref()
const command = ref('')
const terminal_window = ref()
const command_input = ref()
const isLoading = ref(false)
const socket = useSocketStore()
watch(() => isLoading.value, async value => {
if (value) {
return
}
command.value = ''
await nextTick();
focusInput()
}, { immediate: true })
const reSizeTerminal = () => {
if (!terminal.value) {
return
}
terminalFit.value.fit()
}
const runCommand = async () => {
if ('' === command.value) {
return
}
if (!terminal.value) {
terminal.value = new Terminal({
fontSize: 14,
fontFamily: "'JetBrains Mono', monospace",
cursorBlink: false,
cursorStyle: 'underline',
cols: 108,
rows: 10,
buffer: 1000,
disableStdin: true,
scrollback: 1000,
})
terminalFit.value = new FitAddon()
terminal.value.loadAddon(terminalFit.value)
terminal.value.open(terminal_window.value)
terminalFit.value.fit();
}
if ('clear' === command.value) {
clearOutput(true)
return
}
socket.emit('cli_post', command.value)
isLoading.value = true
terminal.value.writeln(`user@YTPTube ~`)
terminal.value.writeln(`$ yt-dlp ${command.value}`)
}
const clearOutput = async (withCommand = false) => {
if (terminal.value) {
terminal.value.clear()
}
if (true === withCommand) {
command.value = ''
}
focusInput()
}
const focusInput = () => {
if (!command_input.value) {
return
}
command_input.value.focus()
}
const writer = s => {
if (!terminal.value) {
return
}
terminal.value.writeln(s.line)
}
const loader = () => isLoading.value = false
onMounted(async () => {
window.addEventListener('resize', reSizeTerminal);
focusInput()
socket.off('cli_close', loader)
socket.off('cli_output', writer)
socket.on('cli_close', loader)
socket.on('cli_output', writer)
})
onUnmounted(() => {
socket.off('cli_close', loader)
socket.off('cli_output', writer)
window.removeEventListener('resize', reSizeTerminal)
});
</script>

View file

@ -1,10 +0,0 @@
<template>
<div>
<Queue />
<History />
</div>
</template>
<script setup>
useHead({ title: 'Index' })
</script>

View file

@ -1,33 +0,0 @@
let observer;
const AUTO_SCROLL_EVENTS = ['scroll', 'mousewheel', 'touchmove'];
let events_handlers = [];
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.directive('autoscroll', {
mounted: async el => {
let scrolledToBottom = true;
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
events_handlers[index] = () => {
scrolledToBottom = (el.scrollHeight - el.scrollTop) - el.clientHeight <= 80;
}
el.addEventListener(ev, events_handlers[index], { passive: true });
});
observer = new MutationObserver(_ => {
if (false === scrolledToBottom) {
return;
}
el.scrollTop = el.scrollHeight;
});
observer.observe(el, { childList: true, subtree: true });
},
unmounted: el => {
AUTO_SCROLL_EVENTS.forEach((ev, index) => {
el.removeEventListener(ev, events_handlers[index], { passive: true });
});
observer.disconnect();
},
})
});

View file

@ -1,65 +0,0 @@
const CONFIG_KEYS = {
isConnected: false,
showForm: false,
showConsole: false,
showTasks: false,
tasks: [],
app: {
host: '',
prefix: '',
keep_archive: false,
output_template: '',
},
presets: [
{
name: 'Default - Use Predefined yt-dlp Format',
format: 'default',
}
],
directories: [],
};
export const useConfigStore = defineStore('config', () => {
const state = reactive(CONFIG_KEYS);
const actions = {
add(key, value) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = value;
return;
}
state[key] = value;
},
get(key, defaultValue = null) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
return state[parentKey][subKey] || defaultValue;
}
return state[key] || defaultValue;
},
update(key, value) {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = value;
return;
}
state[key] = value;
},
getAll() {
return state;
},
setAll(data) {
Object.keys(data).forEach((key) => {
if (key.includes('.')) {
const [parentKey, subKey] = key.split('.');
state[parentKey][subKey] = data[key];
return;
}
state[key] = data[key];
});
},
}
return { ...toRefs(state), ...actions };
});

View file

@ -1,98 +0,0 @@
import { io } from "socket.io-client";
import { notification, ag } from "~/utils/index"
export const useSocketStore = defineStore('socket', () => {
const runtimeConfig = useRuntimeConfig()
const config = useConfigStore()
const stateStore = useStateStore()
const socket = ref(null);
const isConnected = ref(false);
const connect = () => {
socket.value = io(runtimeConfig.public.wss)
socket.value.on('connect', () => isConnected.value = true);
socket.value.on('disconnect', () => isConnected.value = false);
socket.value.on('initial_data', stream => {
const initialData = JSON.parse(stream)
config.setAll({
app: initialData['config'],
tasks: initialData['tasks'],
directories: initialData['directories'],
})
stateStore.addAll('queue', initialData['queue'] ?? {})
stateStore.addAll('history', initialData['done'] ?? {})
})
socket.value.on('added', stream => {
const item = JSON.parse(stream);
stateStore.add('queue', item._id, item);
notification('success', `Item queued successfully: ${ag(stateStore.get('queue', item._id, {}), 'title')}`);
});
socket.value.on('error', stream => {
const [item, error] = JSON.parse(stream);
notification('error', `${item?.id}: Error: ${error}`);
});
socket.value.on('completed', stream => {
const item = JSON.parse(stream);
if (true === stateStore.has('queue', item._id)) {
stateStore.move('queue', 'history', item._id);
return
}
stateStore.add('history', item);
});
socket.value.on('canceled', stream => {
const id = JSON.parse(stream);
if (true !== stateStore.has('queue', id)) {
return
}
notification('info', `Download canceled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id);
}
});
socket.value.on('cleared', stream => {
const id = JSON.parse(stream);
if (true !== stateStore.has('history', id)) {
return
}
stateStore.remove('history', id);
});
socket.value.on("updated", stream => {
const data = JSON.parse(stream);
let dl = stateStore.get('queue', data._id, {});
data.deleting = dl?.deleting;
stateStore.update('queue', data._id, data);
});
socket.value.on("update", stream => {
const data = JSON.parse(stream);
if (true === stateStore.has('history', data._id)) {
stateStore.update('history', data._id, data);
return;
}
});
}
const on = (event, callback) => socket.value.on(event, callback);
const off = (event, callback) => socket.value.off(event, callback);
const emit = (event, data) => socket.value.emit(event, data);
if (false === isConnected.value) {
connect();
}
return { connect, on, off, emit, socket, isConnected };
});

View file

@ -1,43 +0,0 @@
export const useStateStore = defineStore('state', () => {
const state = reactive({
queue: {},
history: {}
});
const actions = {
add(type, key, value) {
state[type][key] = value;
},
update(type, key, value) {
state[type][key] = value;
},
remove(type, key) {
if (state[type][key]) {
delete state[type][key];
}
},
get(type, key, defaultValue = null) {
return state[type][key] || defaultValue;
},
has(type, key) {
return !!state[type][key];
},
clearAll(type) {
state[type] = {};
},
addAll(type, data) {
state[type] = data;
},
move(fromType, toType, key) {
if (state[fromType][key]) {
state[toType][key] = state[fromType][key];
delete state[fromType][key];
}
},
count(type) {
return Object.keys(state[type]).length;
}
}
return { ...toRefs(state), ...actions };
});

View file

@ -1,7 +0,0 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false
}
}

View file

@ -1,34 +0,0 @@
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// for non arrays, length is undefined, so != 0
const isNotTruthy = (val: any) => val === undefined || val === false || val === null || val.length === 0;
/**
* Waits for the test function to return a truthy value.
*
* @param test - The function to test
* @param timeout_ms - The maximum time to wait in milliseconds.
* @param frequency - The frequency to check the test function in milliseconds.
*
* @returns The result of the test function.
*/
export default async function awaiter(test: Function, timeout_ms: number = 20 * 1000, frequency: number = 200) {
if (typeof (test) != "function") {
throw new Error("test should be a function in awaiter(test, [timeout_ms], [frequency])")
}
const endTime: number = Date.now() + timeout_ms;
let result = test();
while (isNotTruthy(result)) {
if (Date.now() > endTime) {
return false;
}
await sleep(frequency);
result = test();
}
return result;
}

View file

@ -1,94 +0,0 @@
/**
* Cache class
*
* @class
* @param {string} engine - The engine to use for caching. Either 'session' or 'local'.
* @throws {Error} If the engine is not supported.
*/
class Cache {
supportedEngines = ['session', 'local']
constructor(engine = 'session') {
if (!this.supportedEngines.includes(engine)) {
throw new Error(`Engine '${engine}' not supported.`)
}
if ('session' === engine) {
this.storage = window.sessionStorage
} else {
this.storage = window.localStorage
}
}
/**
* Set a value in the cache
*
* @param key {string}
* @param value {*}
* @param ttl {number|null} - Time to live in milliseconds. If null, the value will not expire.
*/
set(key, value, ttl = null) {
this.storage.setItem(key, JSON.stringify({value, ttl, time: Date.now()}))
}
/**
* Get a value from the cache
*
* @param key {string}
* @returns {any}
*/
get(key) {
const item = this.storage.getItem(key)
if (null === item) {
return null
}
try {
const {value, ttl, time} = JSON.parse(item)
if (null !== ttl && Date.now() - time > ttl) {
this.remove(key)
return null
}
return value
} catch (e) {
return item
}
}
/**
* Remove a value from the cache
*
* @param key {string}
*/
remove(key) {
this.storage.removeItem(key)
}
/**
* Clear the cache
*
* @param {function|null} filter - A filter function to remove only specific keys.
*/
clear(filter = null) {
if (null !== filter) {
Object.keys(this.storage ?? {}).filter(filter).forEach(k => this.storage.removeItem(k))
} else {
this.storage.clear()
}
}
/**
* Check if a key is in the cache
*
* @param key {string}
* @returns {boolean}
*/
has(key) {
return null !== this.get(key)
}
}
const useSessionCache = () => new Cache('session')
const useLocalCache = () => new Cache('local')
export {useSessionCache, useLocalCache}

View file

@ -1,441 +0,0 @@
import { useNotification } from '@kyvg/vue3-notification'
import { useStorage } from '@vueuse/core'
const { notify } = useNotification()
const AG_SEPARATOR = '.'
/**
* Get value from object or function
*
* @param {Function|*} obj
* @returns {*}
*/
const getValue = (obj) => 'function' === typeof obj ? obj() : obj
/**
* Get value from object or function and return default value if it's undefined or null
*
* @param {Object|Array} obj The object to get the value from.
* @param {string} path The path to the value.
* @param {*} defaultValue The default value to return if the path is not found.
*
* @returns {*} The value at the path or the default value.
*/
const ag = (obj, path, defaultValue = null) => {
const keys = path.split(AG_SEPARATOR)
let at = obj
for (let key of keys) {
if (typeof at === 'object' && null !== at && key in at) {
at = at[key]
} else {
return getValue(defaultValue)
}
}
return getValue(null === at ? defaultValue : at)
}
/**
* Set value in object by path
*
* @param {Object} obj The object to set the value in.
* @param {string} path The path to the value.
* @param {*} value The value to set.
*
* @returns {Object} The object with the value set.
*/
const ag_set = (obj, path, value) => {
const keys = path.split(AG_SEPARATOR)
let at = obj
while (keys.length > 0) {
if (keys.length === 1) {
if (typeof at === 'object' && at !== null) {
at[keys.shift()] = value
} else {
throw new Error(`Cannot set value at this path (${path}) because it's not an object.`)
}
} else {
const key = keys.shift()
if (!at[key]) {
at[key] = {}
}
at = at[key]
}
}
return obj
}
/**
* Wait for an element to be loaded in the DOM
*
* @param {string} sel The selector of the element.
* @param {Function} callback The callback function.
*
* @returns {void}
*/
const awaitElement = (sel, callback) => {
let interval = undefined
let $elm = document.querySelector(sel)
if ($elm) {
callback($elm, sel)
return
}
interval = setInterval(() => {
let $elm = document.querySelector(sel)
if ($elm) {
clearInterval(interval)
callback($elm, sel)
}
}, 200)
}
/**
* Display a notification
*
* @param {string} type The type of the notification.
* @param {string} title The title of the notification.
* @param {string} text The text of the notification.
* @param {number} duration The duration of the notification.
*
* @returns {void}
*/
const notification = (type, title, text, duration = 3000) => {
let classes = ''
const notificationType = type.toLowerCase()
switch (notificationType) {
case 'info':
default:
classes = 'has-background-info has-text-white'
break
case 'success':
classes = 'has-background-success has-text-white'
break
case 'warning':
classes = 'has-background-warning has-text-white'
break
case 'error':
case 'crit':
classes = 'has-background-danger has-text-white'
if (3000 === duration) {
duration = 10000
}
break
}
return notify({ title, text, type: classes, duration })
}
/**
* Replace tags in text with values from context
*
* @param {string} text The text with tags
* @param {object} context The context with values
*
* @returns {string} The text with replaced tags
*/
const r = (text, context = {}) => {
const tagLeft = '{'
const tagRight = '}'
if (!text.includes(tagLeft) || !text.includes(tagRight)) {
return text
}
const pattern = new RegExp(`${tagLeft}([\\w_.]+)${tagRight}`, 'g')
const matches = text.match(pattern)
if (!matches) {
return text
}
let replacements = {}
matches.forEach(match => replacements[match] = ag(context, match.slice(1, -1), ''))
for (let key in replacements) {
text = text.replace(new RegExp(key, 'g'), replacements[key])
}
return text
}
const copyText = (str, notify = true) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(str).then(() => {
if (notify) {
notification('success', 'Success', 'Text copied to clipboard.')
}
}).catch((error) => {
console.error('Failed to copy.', error)
if (notify) {
notification('error', 'Error', 'Failed to copy to clipboard.')
}
})
return
}
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
if (notify) {
notification('success', 'Success', 'Text copied to clipboard.')
}
}
/**
* Dispatch custom event.
*
* @param {string} eventName The name of the event.
* @param {object} detail The detail object.
*
* @returns {Boolean} The return value of dispatchEvent.
*/
const dEvent = (eventName, detail = {}) => window.dispatchEvent(new CustomEvent(eventName, { detail }))
/**
* Make pagination
*
* @param {number} current The current page.
* @param {number} last The last page.
* @param {number} delta The delta.
*
* @returns {Array} The pagination array.
*/
const makePagination = (current, last, delta = 5) => {
let pagination = []
if (last < 2) {
return pagination
}
const strR = '-'.repeat(9 + `${last}`.length)
const left = current - delta, right = current + delta + 1
for (let i = 1; i <= last; i++) {
if (i === 1 || i === last || (i >= left && i < right)) {
if (i === left && i > 2) {
pagination.push({
page: 0, text: strR, selected: false,
})
}
pagination.push({
page: i, text: `Page #${i}`, selected: i === current
})
if (i === right - 1 && i < last - 1) {
pagination.push({
page: 0, text: strR, selected: false,
})
}
}
}
return pagination
}
/**
* Encodes a path string to be used in a URL
*
* @param {string} item - The path string to encode
*
* @returns {string} - The encoded path string
*/
const encodePath = item => {
// -- manually encode #
if (!item) {
return item
}
item = item.replace(/#/g, '%23')
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
}
/**
* Request content from the API. This function will automatically add the API token to the request headers.
* And prefix the URL with the API URL and path.
*
* @param {string} url - The URL to request
* @param {object} options - The request options
*
* @returns {Promise<Response>} - The response from the API
*/
const request = (url, options = {}) => {
const runtimeConfig = useRuntimeConfig()
const token = useStorage('token', null)
options = options || {}
options.method = options.method || 'GET'
options.headers = options.headers || {}
if (token && undefined === options.headers['Authorization']) {
options.headers['Authorization'] = 'Token ' + token.value
}
if (undefined === options.headers['Content-Type']) {
if (!(options?.body instanceof FormData)) {
options.headers['Content-Type'] = 'application/json'
}
}
if (undefined === options.headers['Accept']) {
options.headers['Accept'] = 'application/json'
}
return fetch(url.startsWith('/') ? runtimeConfig.public.domain + url : url, options)
}
/**
* Remove the ANSI colors from the text
*
* @param {string} text - The text to remove the colors from
*
* @returns {string} - The text without the colors.
*/
const removeANSIColors = text => text?.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') ?? text
const dec2hex = dec => dec.toString(16).padStart(2, "0")
const makeId = len => Array.from(window.crypto.getRandomValues(new Uint8Array((len || 40) / 2)), dec2hex).join('')
const basename = (path, ext = '') => {
if (!path) {
return ''
}
const segments = path.replace(/\\/g, '/').split('/')
let base = segments.pop()
while (segments.length && base === '') {
base = segments.pop()
}
if (ext && base.endsWith(ext) && base !== ext) {
base = base.substring(0, base.length - ext.length)
}
return base
}
const dirname = filePath => {
const lastIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
if (-1 === lastIndex) {
return '.'
}
if (0 === lastIndex) {
return filePath[0]
}
return filePath.substring(0, lastIndex)
}
const iTrim = (str, delim, position = 'both') => {
if (!str) {
return str
}
if (!delim) {
throw new Error('Delimiter is required')
}
if ("]" === delim) {
delim = "\\]"
}
if ("\\" === delim) {
delim = "\\\\"
}
if (['both', 'start'].includes(position)) {
str = str.replace(new RegExp("^[" + delim + "]+", "g"), "")
}
if (['both', 'end'].includes(position)) {
str = str.replace(new RegExp("[" + delim + "]+$", "g"), "")
}
return str
}
const eTrim = (str, delim) => iTrim(str, delim, 'end')
const sTrim = (str, delim) => iTrim(str, delim, 'start')
const ucFirst = str => (!str) ? str : str.charAt(0).toUpperCase() + str.slice(1)
/**
* Get query parameters from the URL
*
* @param {string} url - The URL to get the query parameters from
*
* @returns {Object} - The query parameters
*/
const getQueryParams = (url = window.location.search) => Object.fromEntries(new URLSearchParams(url).entries())
/**
* Make download URL.
*
* @param {Object} config
* @param {Object} item
* @param {string} base
*
* @returns {string} The download URL
*/
const makeDownload = (config, item, base = 'download') => {
let baseDir = 'download' === base ? `${base}/` : 'player/playlist/';
if (item.folder) {
item.folder = item.folder.replace(/#/g, '%23');
baseDir += item.folder + '/';
}
let url = config.app.url_host + encodePath(config.app.url_prefix + baseDir + item.filename);
return ('m3u8' === base) ? url + '.m3u8' : url;
}
/**
* Format a file size
*
* @param {number} bytes
* @param {number} decimals
*
* @returns {string} The formatted file size
*/
const formatBytes = (bytes, decimals = 2) => {
if (!+bytes) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
export {
ag_set,
ag,
awaitElement,
notification,
copyText,
dEvent,
makePagination,
request,
r,
encodePath,
removeANSIColors,
makeId,
basename,
iTrim,
eTrim,
sTrim,
ucFirst,
dirname,
getQueryParams,
makeDownload,
formatBytes,
}

View file

@ -1,58 +0,0 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"framework": "vue",
"js-types-syntax": "typescript",
"description-markup": "markdown",
"framework-config": {
"enable-when": {
"file-extensions": [
"vue"
]
}
},
"contributions": {
"html": {
"vue-directives": [
{
"name": "tooltip",
"description": "Create a tooltip for an element.",
"doc-url": "",
"attribute-value": {
"type": "string",
"required": true
},
"modifiers": [
{
"name": "top",
"description": "Position the tooltip at the top of the element."
},
{
"name": "bottom",
"description": "Position the tooltip at the bottom of the element."
},
{
"name": "left",
"description": "Position the tooltip at the left of the element."
},
{
"name": "right",
"description": "Position the tooltip at the right of the element."
}
]
},
{
"name": "autoscroll",
"description": "Automatically scroll to an element.",
"doc-url": ""
}
],
"vue-components": [
{
"name": "VTooltip",
"description": "Create more advanced tooltips.",
"doc-url": "https://floating-vue.starpad.dev/guide/component#tooltip"
}
]
}
}
}

File diff suppressed because it is too large Load diff