sticking with pure vue for now.
This commit is contained in:
parent
cf8f971785
commit
ec6b8ec85f
37 changed files with 0 additions and 44187 deletions
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
|
|
@ -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
23
ui/.gitignore
vendored
|
|
@ -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
|
||||
10607
ui/assets/css/all.css
10607
ui/assets/css/all.css
File diff suppressed because it is too large
Load diff
|
|
@ -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
24828
ui/assets/css/bulma.css
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
53
ui/error.vue
53
ui/error.vue
|
|
@ -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>
|
||||
|
|
@ -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"> ({{ config.app?.version || 'unknown' }})</span>
|
||||
- <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>
|
||||
<span class="is-hidden-mobile"> ({{ 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>
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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",
|
||||
})
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
209
ui/pages/add.vue
209
ui/pages/add.vue
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<Queue />
|
||||
<History />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
useHead({ title: 'Index' })
|
||||
</script>
|
||||
|
|
@ -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();
|
||||
},
|
||||
})
|
||||
});
|
||||
|
|
@ -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 };
|
||||
});
|
||||
|
|
@ -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 };
|
||||
});
|
||||
|
|
@ -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 };
|
||||
});
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": false
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
5207
ui/yarn.lock
5207
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue