initial nuxtjs ui skeleton
This commit is contained in:
parent
3ecb3c8690
commit
62d5d29bfc
34 changed files with 43625 additions and 3 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
.git
|
.git
|
||||||
.venv
|
.venv
|
||||||
ui/.angular
|
ui/
|
||||||
ui/node_modules
|
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -2,7 +2,8 @@
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/frontend/node_modules
|
**/node_modules/*
|
||||||
|
**/.nuxt/*
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/ui/.idea
|
/ui/.idea
|
||||||
|
|
|
||||||
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
|
|
@ -73,6 +73,23 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"justMyCode": true
|
"justMyCode": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Node: NuxtJS",
|
||||||
|
"request": "launch",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"run",
|
||||||
|
"dev",
|
||||||
|
"--",
|
||||||
|
"--port",
|
||||||
|
"8082"
|
||||||
|
],
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"type": "node",
|
||||||
|
"cwd": "${workspaceFolder}/ui",
|
||||||
|
"env": {
|
||||||
|
"VUE_APP_BASE_URL": "http://localhost:8081",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
ui/.gitignore
vendored
Normal file
23
ui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 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
Normal file
10607
ui/assets/css/all.css
Normal file
File diff suppressed because it is too large
Load diff
966
ui/assets/css/bulma-switch.css
Normal file
966
ui/assets/css/bulma-switch.css
Normal file
|
|
@ -0,0 +1,966 @@
|
||||||
|
.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
Normal file
24828
ui/assets/css/bulma.css
vendored
Normal file
File diff suppressed because it is too large
Load diff
190
ui/assets/css/style.css
Normal file
190
ui/assets/css/style.css
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ui/assets/webfonts/JetBrainsMono-Regular.woff2
Normal file
BIN
ui/assets/webfonts/JetBrainsMono-Regular.woff2
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-brands-400.ttf
Normal file
BIN
ui/assets/webfonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-brands-400.woff2
Normal file
BIN
ui/assets/webfonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-regular-400.ttf
Normal file
BIN
ui/assets/webfonts/fa-regular-400.ttf
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-regular-400.woff2
Normal file
BIN
ui/assets/webfonts/fa-regular-400.woff2
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-solid-900.ttf
Normal file
BIN
ui/assets/webfonts/fa-solid-900.ttf
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-solid-900.woff2
Normal file
BIN
ui/assets/webfonts/fa-solid-900.woff2
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-v4compatibility.ttf
Normal file
BIN
ui/assets/webfonts/fa-v4compatibility.ttf
Normal file
Binary file not shown.
BIN
ui/assets/webfonts/fa-v4compatibility.woff2
Normal file
BIN
ui/assets/webfonts/fa-v4compatibility.woff2
Normal file
Binary file not shown.
394
ui/components/History.vue
Normal file
394
ui/components/History.vue
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|
<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 props = defineProps({
|
||||||
|
completed: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedElms = ref([]);
|
||||||
|
const masterSelectAll = ref(false);
|
||||||
|
const showCompleted = useStorage('showCompleted', true)
|
||||||
|
const direction = useStorage('sortCompleted', 'desc')
|
||||||
|
|
||||||
|
watch(masterSelectAll, (value) => {
|
||||||
|
for (const key in props.completed) {
|
||||||
|
const element = props.completed[key];
|
||||||
|
if (value) {
|
||||||
|
selectedElms.value.push(element._id);
|
||||||
|
} else {
|
||||||
|
selectedElms.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const sortCompleted = computed(() => {
|
||||||
|
const thisDirection = direction.value;
|
||||||
|
return Object.values(props.completed).sort((a, b) => {
|
||||||
|
if (thisDirection === 'asc') {
|
||||||
|
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(() => Object.keys(props.completed)?.length > 0)
|
||||||
|
const getTotal = computed(() => Object.keys(props.completed)?.length);
|
||||||
|
|
||||||
|
const showMessage = (item) => {
|
||||||
|
if (!item?.msg || item.msg === item?.error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.msg.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasIncomplete = computed(() => {
|
||||||
|
if (Object.keys(props.completed)?.length < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in props.completed) {
|
||||||
|
const element = props.completed[key];
|
||||||
|
if (element.status !== 'finished') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasCompleted = computed(() => {
|
||||||
|
if (Object.keys(props.completed)?.length < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in props.completed) {
|
||||||
|
const element = props.completed[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 props.completed) {
|
||||||
|
if (props.completed[key].status === 'finished') {
|
||||||
|
keys[key] = props.completed[key]._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emits('deleteItem', 'completed', keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearIncomplete = () => {
|
||||||
|
const state = confirm('Are you sure you want to clear all incomplete downloads?');
|
||||||
|
if (false === state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = {};
|
||||||
|
|
||||||
|
for (const key in props.completed) {
|
||||||
|
if (props.completed[key].status !== 'finished') {
|
||||||
|
keys[key] = props.completed[key]._id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emits('deleteItem', 'completed', keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 props.completed) {
|
||||||
|
const item = props.completed[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>
|
||||||
76
ui/components/LazyLoader.vue
Normal file
76
ui/components/LazyLoader.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<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>
|
||||||
237
ui/components/Queue.vue
Normal file
237
ui/components/Queue.vue
Normal file
|
|
@ -0,0 +1,237 @@
|
||||||
|
<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">({{ getTotal }})</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 queue" :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>{{ capitalize(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="confirmDelete(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="config.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 { defineProps, defineEmits, ref, watch, computed } from 'vue';
|
||||||
|
import moment from "moment";
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import LazyLoader from './LazyLoader'
|
||||||
|
|
||||||
|
const emit = defineEmits(['deleteItem']);
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
queue: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedElms = ref([]);
|
||||||
|
const masterSelectAll = ref(false);
|
||||||
|
const showQueue = useStorage('showQueue', true)
|
||||||
|
|
||||||
|
watch(masterSelectAll, (value) => {
|
||||||
|
for (const key in props.queue) {
|
||||||
|
const element = props.queue[key];
|
||||||
|
if (value) {
|
||||||
|
selectedElms.value.push(element._id);
|
||||||
|
} else {
|
||||||
|
selectedElms.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const hasSelected = computed(() => selectedElms.value.length > 0)
|
||||||
|
const hasQueuedItems = computed(() => Object.keys(props.queue)?.length > 0)
|
||||||
|
const getTotal = computed(() => Object.keys(props.queue)?.length);
|
||||||
|
|
||||||
|
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 confirmDelete = (item) => {
|
||||||
|
if (!confirm(`Are you sure you want to delete (${item.title})?`)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('deleteItem', 'queue', item._id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
54
ui/error.vue
Normal file
54
ui/error.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<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>
|
||||||
135
ui/layouts/default.vue
Normal file
135
ui/layouts/default.vue
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
<template>
|
||||||
|
<div id="main_container" class="container" :class="{'is-max-fullwidth': expandContainer}">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<a class="navbar-item is-hidden-tablet" id="top" href="#bottom">
|
||||||
|
<span class="icon"><i class="fas fa-arrow-down"></i></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="navbar-burger burger" @click="showMenu = !showMenu">
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-menu is-unselectable" :class="{'is-active':showMenu}">
|
||||||
|
<div class="navbar-end pr-3">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<NuxtPage />
|
||||||
|
|
||||||
|
<div class="columns is-multiline is-mobile mt-3">
|
||||||
|
<div class="column is-12 is-hidden-tablet has-text-centered">
|
||||||
|
<a href="#top" id="bottom" class="button">
|
||||||
|
<span class="icon"><i class="fas fa-arrow-up"></i> </span>
|
||||||
|
<span>Go to Top</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="column is-6 is-9-mobile has-text-left"></div>
|
||||||
|
<div class="column is-6 is-4-mobile has-text-right">
|
||||||
|
v{{ VERSION }}
|
||||||
|
</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 {notification} from '~/utils/index'
|
||||||
|
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
|
||||||
|
const selectedTheme = useStorage('theme', (() => window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')())
|
||||||
|
const showMenu = ref(false)
|
||||||
|
const VERSION = ref(runtimeConfig.public.version)
|
||||||
|
const expandContainer = ref(false)
|
||||||
|
|
||||||
|
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()
|
||||||
|
</script>
|
||||||
3
ui/layouts/empty.vue
Normal file
3
ui/layouts/empty.vue
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<template>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
21
ui/layouts/errorLayout.vue
Normal file
21
ui/layouts/errorLayout.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<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>
|
||||||
54
ui/nuxt.config.ts
Normal file
54
ui/nuxt.config.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
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",
|
||||||
|
})
|
||||||
31
ui/package.json
Normal file
31
ui/package.json
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"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": {}
|
||||||
|
}
|
||||||
109
ui/pages/index.vue
Normal file
109
ui/pages/index.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Queue :config="config" :queue="downloading" @deleteItem="deleteItem" />
|
||||||
|
<History :config="config" :completed="completed" @deleteItem="deleteItem" @addItem="addItem"
|
||||||
|
@playItem="playItem" @archiveItem="archiveItem" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
const bus = useEventBus('item_added', 'show_form', 'task_edit')
|
||||||
|
|
||||||
|
const config = reactive({
|
||||||
|
isConnected: false,
|
||||||
|
app: {},
|
||||||
|
tasks: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const runtimeConfig = useRuntimeConfig()
|
||||||
|
const socket = ref()
|
||||||
|
const downloading = reactive({})
|
||||||
|
const completed = reactive({})
|
||||||
|
const cli_output = ref([])
|
||||||
|
const cli_isLoading = ref(false)
|
||||||
|
|
||||||
|
useHead({ title: 'Index' })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
socket.value = io(runtimeConfig.public.wss, {
|
||||||
|
path: document.location.pathname + 'socket.io',
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('connect', () => config.isConnected = true);
|
||||||
|
socket.value.on('disconnect', () => config.isConnected = false);
|
||||||
|
|
||||||
|
socket.value.on('initial_data', stream => {
|
||||||
|
const initialData = JSON.parse(stream);
|
||||||
|
config.app = initialData['config'];
|
||||||
|
config.tasks = initialData['tasks'];
|
||||||
|
config.directories = initialData['directories'];
|
||||||
|
|
||||||
|
for (const id in initialData['queue']) {
|
||||||
|
downloading[id] = initialData['queue'][id];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const id in initialData['done']) {
|
||||||
|
completed[id] = initialData['done'][id];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.value.on('added', stream => {
|
||||||
|
const item = JSON.parse(stream);
|
||||||
|
downloading[item._id] = item;
|
||||||
|
toast.success(`Item queued successfully: ${downloading[item._id]?.title}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('error', stream => {
|
||||||
|
const [item, error] = JSON.parse(stream);
|
||||||
|
toast.error(`${item?.id}: Error: ${error}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('completed', stream => {
|
||||||
|
const item = JSON.parse(stream);
|
||||||
|
if (item._id in downloading) {
|
||||||
|
delete downloading[item._id];
|
||||||
|
}
|
||||||
|
completed[item._id] = item;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('canceled', stream => {
|
||||||
|
const id = JSON.parse(stream);
|
||||||
|
if (false === (id in downloading)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.info('Download canceled: ' + downloading[id]?.title);
|
||||||
|
delete downloading[id];
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('cleared', stream => {
|
||||||
|
const id = JSON.parse(stream);
|
||||||
|
if (false === (id in completed)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete completed[id];
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on("updated", stream => {
|
||||||
|
const data = JSON.parse(stream);
|
||||||
|
let dl = downloading[data._id] ?? {};
|
||||||
|
data.deleting = dl?.deleting;
|
||||||
|
downloading[data._id] = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on("update", stream => {
|
||||||
|
const data = JSON.parse(stream);
|
||||||
|
if (false === (data._id in completed)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
completed[data._id] = data;
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.value.on('cli_close', () => cli_isLoading.value = false);
|
||||||
|
socket.value.on('cli_output', s => cli_output.value.push(s));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
33
ui/plugins/autoscroll.js
Normal file
33
ui/plugins/autoscroll.js
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
});
|
||||||
7
ui/tsconfig.json
Normal file
7
ui/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowImportingTsExtensions": false
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ui/utils/awaiter.ts
Normal file
34
ui/utils/awaiter.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
94
ui/utils/cache.js
Normal file
94
ui/utils/cache.js
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
/**
|
||||||
|
* 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}
|
||||||
444
ui/utils/index.js
Normal file
444
ui/utils/index.js
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if ('crit' === notificationType) {
|
||||||
|
log_error(`${title} ${text}`)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
}
|
||||||
58
ui/web-types.json
Normal file
58
ui/web-types.json
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
{
|
||||||
|
"$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
Normal file
5207
ui/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue