implemented core stores

This commit is contained in:
2024-02-07 14:35:04 +01:00
parent b9b0fde520
commit 00df98233d
23 changed files with 2283 additions and 2 deletions

47
ui/src/App.svelte Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />