implemented core stores
This commit is contained in:
47
ui/src/App.svelte
Normal file
47
ui/src/App.svelte
Normal file
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import svelteLogo from './assets/svelte.svg'
|
||||
import viteLogo from '/vite.svg'
|
||||
import Counter from './lib/Counter.svelte'
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank" rel="noreferrer">
|
||||
<img src={viteLogo} class="logo" alt="Vite Logo" />
|
||||
</a>
|
||||
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
|
||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + Svelte</h1>
|
||||
|
||||
<div class="card">
|
||||
<Counter />
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out <a href="https://github.com/sveltejs/kit#readme" target="_blank" rel="noreferrer">SvelteKit</a>, the official Svelte app framework powered by Vite!
|
||||
</p>
|
||||
|
||||
<p class="read-the-docs">
|
||||
Click on the Vite and Svelte logos to learn more
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.svelte:hover {
|
||||
filter: drop-shadow(0 0 2em #ff3e00aa);
|
||||
}
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
79
ui/src/app.css
Normal file
79
ui/src/app.css
Normal file
@@ -0,0 +1,79 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
1
ui/src/assets/svelte.svg
Normal file
1
ui/src/assets/svelte.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
10
ui/src/lib/Counter.svelte
Normal file
10
ui/src/lib/Counter.svelte
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
let count: number = 0
|
||||
const increment = () => {
|
||||
count += 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={increment}>
|
||||
count is {count}
|
||||
</button>
|
||||
160
ui/src/lib/RPCClient.ts
Normal file
160
ui/src/lib/RPCClient.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { DLMetadata, RPCRequest, RPCResponse, RPCResult } from './types'
|
||||
|
||||
type DownloadRequestArgs = {
|
||||
url: string,
|
||||
args: string,
|
||||
pathOverride?: string,
|
||||
renameTo?: string,
|
||||
playlist?: boolean
|
||||
}
|
||||
|
||||
export class RPCClient {
|
||||
private seq: number
|
||||
private httpEndpoint: string
|
||||
private readonly _socket$: WebSocket
|
||||
private readonly token?: string
|
||||
|
||||
constructor(httpEndpoint: string, webSocketEndpoint: string, token?: string) {
|
||||
this.seq = 0
|
||||
this.httpEndpoint = httpEndpoint
|
||||
this.token = token
|
||||
this._socket$ = new WebSocket(
|
||||
token ? `${webSocketEndpoint}?token=${token}` : webSocketEndpoint
|
||||
)
|
||||
}
|
||||
|
||||
public get socket() {
|
||||
return this._socket$
|
||||
}
|
||||
|
||||
private incrementSeq() {
|
||||
return String(this.seq++)
|
||||
}
|
||||
|
||||
private send(req: RPCRequest) {
|
||||
this._socket$.send(JSON.stringify({
|
||||
...req,
|
||||
id: this.incrementSeq(),
|
||||
}))
|
||||
}
|
||||
|
||||
private argsSanitizer(args: string) {
|
||||
return args
|
||||
.split(' ')
|
||||
.map(a => a.trim().replaceAll("'", '').replaceAll('"', ''))
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
private async sendHTTP<T>(req: RPCRequest) {
|
||||
const res = await fetch(this.httpEndpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Authentication': this.token ?? ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...req,
|
||||
id: this.incrementSeq(),
|
||||
})
|
||||
})
|
||||
const data: RPCResponse<T> = await res.json()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
public download(req: DownloadRequestArgs) {
|
||||
if (!req.url) {
|
||||
return
|
||||
}
|
||||
|
||||
const rename = req.args.includes('-o')
|
||||
? req.args
|
||||
.substring(req.args.indexOf('-o'))
|
||||
.replaceAll("'", '')
|
||||
.replaceAll('"', '')
|
||||
.split('-o')
|
||||
.map(s => s.trim())
|
||||
.join('')
|
||||
.split(' ')
|
||||
.at(0) ?? ''
|
||||
: ''
|
||||
|
||||
const sanitizedArgs = this.argsSanitizer(
|
||||
req.args.replace('-o', '').replace(rename, '')
|
||||
)
|
||||
|
||||
if (req.playlist) {
|
||||
return this.sendHTTP({
|
||||
method: 'Service.ExecPlaylist',
|
||||
params: [{
|
||||
URL: req.url,
|
||||
Params: sanitizedArgs,
|
||||
Path: req.pathOverride,
|
||||
Rename: req.renameTo || rename,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
this.sendHTTP({
|
||||
method: 'Service.Exec',
|
||||
params: [{
|
||||
URL: req.url.split('?list').at(0)!,
|
||||
Params: sanitizedArgs,
|
||||
Path: req.pathOverride,
|
||||
Rename: req.renameTo || rename,
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
public formats(url: string) {
|
||||
if (url) {
|
||||
return this.sendHTTP<DLMetadata>({
|
||||
method: 'Service.Formats',
|
||||
params: [{
|
||||
URL: url.split('?list').at(0)!,
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public running() {
|
||||
this.send({
|
||||
method: 'Service.Running',
|
||||
params: [],
|
||||
})
|
||||
}
|
||||
|
||||
public kill(id: string) {
|
||||
this.sendHTTP({
|
||||
method: 'Service.Kill',
|
||||
params: [id],
|
||||
})
|
||||
}
|
||||
|
||||
public killAll() {
|
||||
this.sendHTTP({
|
||||
method: 'Service.KillAll',
|
||||
params: [],
|
||||
})
|
||||
}
|
||||
|
||||
public freeSpace() {
|
||||
return this.sendHTTP<number>({
|
||||
method: 'Service.FreeSpace',
|
||||
params: [],
|
||||
})
|
||||
}
|
||||
|
||||
public directoryTree() {
|
||||
return this.sendHTTP<string[]>({
|
||||
method: 'Service.DirectoryTree',
|
||||
params: [],
|
||||
})
|
||||
}
|
||||
|
||||
public updateExecutable() {
|
||||
return this.sendHTTP({
|
||||
method: 'Service.UpdateExecutable',
|
||||
params: []
|
||||
})
|
||||
}
|
||||
}
|
||||
38
ui/src/lib/store.ts
Normal file
38
ui/src/lib/store.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { derived, readable, writable } from 'svelte/store'
|
||||
import { RPCClient } from './RpcClient'
|
||||
import type { RPCResponse, RPCResult } from './types'
|
||||
|
||||
export const rpcHost = writable<string>(localStorage.getItem('rpcHost') ?? 'localhost')
|
||||
export const rpcPort = writable<number>(Number(localStorage.getItem('rpcPort')) ?? 3033)
|
||||
|
||||
export const rpcWebToken = writable<string>(localStorage.getItem('rpcWebToken') ?? '')
|
||||
|
||||
export const serverApiEndpoint = derived(
|
||||
[rpcHost, rpcPort],
|
||||
([$host, $port]) => window.location.port == ''
|
||||
? `${window.location.protocol}//${$host}`
|
||||
: `${window.location.protocol}//${$host}:${$port}`
|
||||
)
|
||||
|
||||
export const websocketRpcEndpoint = derived(
|
||||
[rpcHost, rpcPort],
|
||||
([$host, $port]) => window.location.port == ''
|
||||
? `${window.location.protocol.startsWith('https') ? 'wss:' : 'ws:'}//${$host}/rpc/ws`
|
||||
: `${window.location.protocol.startsWith('https') ? 'wss:' : 'ws:'}//${$host}:${$port}/rpc/ws`
|
||||
)
|
||||
|
||||
export const httpPostRpcEndpoint = derived(
|
||||
serverApiEndpoint,
|
||||
$ep => window.location.port == '' ? `${$ep}/rpc/http` : `${$ep}/rpc/http`
|
||||
)
|
||||
|
||||
export const rpcClient = derived(
|
||||
[httpPostRpcEndpoint, websocketRpcEndpoint, rpcWebToken],
|
||||
([$http, $ws, $token]) => new RPCClient($http, $ws, $token)
|
||||
)
|
||||
|
||||
export const downloads = readable<RPCResponse<RPCResult[]>>({
|
||||
id: '',
|
||||
error: null,
|
||||
result: [],
|
||||
})
|
||||
90
ui/src/lib/types.ts
Normal file
90
ui/src/lib/types.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
export type RPCMethods =
|
||||
| "Service.Exec"
|
||||
| "Service.Kill"
|
||||
| "Service.Clear"
|
||||
| "Service.Running"
|
||||
| "Service.KillAll"
|
||||
| "Service.FreeSpace"
|
||||
| "Service.Formats"
|
||||
| "Service.ExecPlaylist"
|
||||
| "Service.DirectoryTree"
|
||||
| "Service.UpdateExecutable"
|
||||
|
||||
export type RPCRequest = {
|
||||
method: RPCMethods
|
||||
params?: any[]
|
||||
id?: string
|
||||
}
|
||||
|
||||
export type RPCResponse<T> = Readonly<{
|
||||
result: T
|
||||
error: number | null
|
||||
id?: string
|
||||
}>
|
||||
|
||||
type DownloadInfo = {
|
||||
url: string
|
||||
filesize_approx?: number
|
||||
resolution?: string
|
||||
thumbnail: string
|
||||
title: string
|
||||
vcodec?: string
|
||||
acodec?: string
|
||||
ext?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type DownloadProgress = {
|
||||
speed: number
|
||||
eta: number
|
||||
percentage: string
|
||||
process_status: number
|
||||
}
|
||||
|
||||
export type RPCResult = Readonly<{
|
||||
id: string
|
||||
progress: DownloadProgress
|
||||
info: DownloadInfo
|
||||
}>
|
||||
|
||||
export type RPCParams = {
|
||||
URL: string
|
||||
Params?: string
|
||||
}
|
||||
|
||||
export type DLMetadata = {
|
||||
formats: Array<DLFormat>
|
||||
best: DLFormat
|
||||
thumbnail: string
|
||||
title: string
|
||||
}
|
||||
|
||||
export type DLFormat = {
|
||||
format_id: string
|
||||
format_note: string
|
||||
fps: number
|
||||
resolution: string
|
||||
vcodec: string
|
||||
acodec: string
|
||||
filesize_approx: number
|
||||
}
|
||||
|
||||
export type DirectoryEntry = {
|
||||
name: string
|
||||
path: string
|
||||
size: number
|
||||
shaSum: string
|
||||
modTime: string
|
||||
isVideo: boolean
|
||||
isDirectory: boolean
|
||||
}
|
||||
|
||||
export type DeleteRequest = Pick<DirectoryEntry, 'path' | 'shaSum'>
|
||||
|
||||
export type PlayRequest = Pick<DirectoryEntry, 'path'>
|
||||
|
||||
export type CustomTemplate = {
|
||||
id: string
|
||||
name: string
|
||||
content: string
|
||||
}
|
||||
8
ui/src/main.ts
Normal file
8
ui/src/main.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
|
||||
export default app
|
||||
2
ui/src/vite-env.d.ts
vendored
Normal file
2
ui/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user