frontend interface
This commit is contained in:
parent
5878cf9d8c
commit
47168d1cbc
8
crates/frontend/.gitignore
vendored
Normal file
8
crates/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/target
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
*.tsbuildinfo
|
||||||
|
*.mjs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
openapi.json
|
14
crates/frontend/Cargo.toml
Normal file
14
crates/frontend/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "elnafo-radio-frontend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["L-Nafaryus <l.nafaryus@elnafo.ru>"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
ignore = "0.4.22"
|
||||||
|
npm_rs = "1.0.0"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
askama = { version = "0.12.1", features = ["with-axum"] }
|
||||||
|
askama_axum = "0.4.0"
|
||||||
|
rust-embed = "8.3.0"
|
2
crates/frontend/askama.toml
Normal file
2
crates/frontend/askama.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[general]
|
||||||
|
dirs = ["templates"]
|
20
crates/frontend/build.rs
Normal file
20
crates/frontend/build.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use ignore::Walk;
|
||||||
|
use npm_rs::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
for entry in Walk::new(".")
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|e| !e.path().is_dir())
|
||||||
|
.filter(|e| e.file_name() != "package-lock.json")
|
||||||
|
{
|
||||||
|
println!("cargo:rerun-if-changed={}", entry.path().display());
|
||||||
|
}
|
||||||
|
|
||||||
|
NpmEnv::default()
|
||||||
|
.with_node_env(&NodeEnv::from_cargo_profile().unwrap_or_default())
|
||||||
|
.init_env()
|
||||||
|
.install(None)
|
||||||
|
.run("build")
|
||||||
|
.exec()
|
||||||
|
.unwrap();
|
||||||
|
}
|
13
crates/frontend/index.html
Normal file
13
crates/frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/resources/assets/logo.svg">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Materia Dev</title>
|
||||||
|
</head>
|
||||||
|
<body class="h-full text-zinc-200 font-sans ">
|
||||||
|
<div id="app" class="flex flex-col h-full"></div>
|
||||||
|
<script type="module" src="src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4188
crates/frontend/package-lock.json
generated
Normal file
4188
crates/frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
crates/frontend/package.json
Normal file
40
crates/frontend/package.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "materia-frontend",
|
||||||
|
"version": "0.0.5",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build-check": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"generate-client": "openapi --input ./openapi.json --output ./src/client_old/ --client axios --name Client",
|
||||||
|
"openapi-ts": "openapi-ts --input ./openapi.json --output ./src/client/ --client @hey-api/client-axios"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@catppuccin/tailwindcss": "^0.1.6",
|
||||||
|
"@hey-api/client-axios": "^0.2.3",
|
||||||
|
"autoprefixer": "^10.4.18",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"filesize": "^10.1.6",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"vue": "^3.3.11",
|
||||||
|
"vue-router": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@hey-api/openapi-ts": "^0.53.0",
|
||||||
|
"@tsconfig/node18": "^18.2.2",
|
||||||
|
"@types/node": "^18.19.3",
|
||||||
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
||||||
|
"@vue/tsconfig": "^0.5.0",
|
||||||
|
"npm-run-all2": "^6.1.1",
|
||||||
|
"openapi-typescript-codegen": "^0.29.0",
|
||||||
|
"typescript": "~5.3.0",
|
||||||
|
"vite": "^5.0.10",
|
||||||
|
"vue-tsc": "^2.0.29"
|
||||||
|
}
|
||||||
|
}
|
6
crates/frontend/postcss.config.js
Normal file
6
crates/frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
7
crates/frontend/src/App.vue
Normal file
7
crates/frontend/src/App.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterLink, RouterView } from 'vue-router';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
69
crates/frontend/src/assets/style.css
Normal file
69
crates/frontend/src/assets/style.css
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-ctp-crust;
|
||||||
|
font-family: Inter,sans-serif;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-ctp-lavender;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
@apply w-full pl-3 pr-3 pt-2 pb-2 rounded border bg-ctp-mantle border-ctp-overlay0 hover:border-ctp-overlay1 focus:border-ctp-lavender text-ctp-text outline-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-file {
|
||||||
|
@apply block w-full border rounded cursor-pointer bg-ctp-base border-ctp-surface0 text-ctp-subtext0 focus:outline-none;
|
||||||
|
@apply file:bg-ctp-mantle file:border-ctp-surface0 file:mr-5 file:py-2 file:px-3 file:h-full file:border-y-0 file:border-l-0 file:border-r file:text-ctp-blue hover:file:cursor-pointer hover:file:bg-ctp-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@apply text-ctp-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
@apply pt-1 pb-1 pl-3 pr-3 sm:pt-2 sm:pb-2 sm:pl-5 sm:pr-5 rounded bg-ctp-peach border hover:bg-ctp-overlay0/20 text-ctp-blue cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
@apply button text-ctp-base cursor-pointer border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hline {
|
||||||
|
@apply border-t border-ctp-surface0 ml-0 mr-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-ctp-text pt-5 pb-5 border-b border-ctp-overlay0 mb-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
@apply text-ctp-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
@apply inline-block select-none text-center overflow-visible w-6 h-6 stroke-ctp-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.bg-grid {
|
||||||
|
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
|
||||||
|
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px),
|
||||||
|
linear-gradient(90deg, rgba(0, 0, 0, 0) 0px, rgba(187, 65, 143, 1) 10%,
|
||||||
|
rgba(187, 65, 143, 1) 2px, rgba(0, 0, 0, 0) 0px);
|
||||||
|
background-size: 2em 4em, 6em 2em;
|
||||||
|
transform: perspective(500px) rotateX(60deg) scale(0.5);
|
||||||
|
transform-origin: 50% 0%;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
@apply absolute w-[250%] -left-[75%] h-[200%];
|
||||||
|
}
|
||||||
|
}
|
44
crates/frontend/src/client.ts
Normal file
44
crates/frontend/src/client.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
import { createClient, createConfig, type Options } from '@hey-api/client-axios';
|
||||||
|
|
||||||
|
export const client = createClient(createConfig());
|
||||||
|
|
||||||
|
export type StationId = string;
|
||||||
|
|
||||||
|
export enum StationStatus {
|
||||||
|
Online = "Online",
|
||||||
|
Offline = "Offline",
|
||||||
|
Receive = "Receive"
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Playback {
|
||||||
|
Stopped = "Stopped",
|
||||||
|
Playing = "Playing",
|
||||||
|
Paused = "Paused"
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StationInfo = {
|
||||||
|
id: StationId;
|
||||||
|
name: string;
|
||||||
|
status: StationStatus;
|
||||||
|
url: string | null;
|
||||||
|
location: string | null;
|
||||||
|
genre: string | null;
|
||||||
|
playback: Playback | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SongInfo = {
|
||||||
|
artist: string | null;
|
||||||
|
title: string | null;
|
||||||
|
tags: Array<[string, string]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const stationsInfo = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => { return (options?.client ?? client).get<StationInfo[], unknown, ThrowOnError>({
|
||||||
|
...options,
|
||||||
|
url: '/api/stations'
|
||||||
|
}); };
|
||||||
|
|
||||||
|
export const songStatus = <ThrowOnError extends boolean = false>(options?: Options<unknown, ThrowOnError>) => { return (options?.client ?? client).get<SongInfo | null, unknown, ThrowOnError>({
|
||||||
|
...options,
|
||||||
|
url: '/api/status'
|
||||||
|
}); };
|
7
crates/frontend/src/components/DocumentationIcon.vue
Normal file
7
crates/frontend/src/components/DocumentationIcon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
40
crates/frontend/src/components/Error.vue
Normal file
40
crates/frontend/src/components/Error.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
const { value } = defineProps(["value"]);
|
||||||
|
|
||||||
|
const handleError = (e) => {
|
||||||
|
let entries = [];
|
||||||
|
let titlecase = (str: string) => {
|
||||||
|
if (!str) return str;
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (e?.response) {
|
||||||
|
if (e.response.status == 422) {
|
||||||
|
for (let detail of e.response.data.detail) {
|
||||||
|
entries.push(titlecase(detail.msg));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (e.response.data?.detail) {
|
||||||
|
entries.push(e.response.data.detail);
|
||||||
|
} else {
|
||||||
|
entries.push(e.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-if="value" class="mt-1 mb-1">
|
||||||
|
<p v-for="entry in handleError(value)"
|
||||||
|
class="text-center pt-3 pb-3 bg-ctp-red/25 rounded border border-ctp-red text-ctp-red">
|
||||||
|
{{ entry }}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
7
crates/frontend/src/components/ExternalIcon.vue
Normal file
7
crates/frontend/src/components/ExternalIcon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4">
|
||||||
|
<path
|
||||||
|
d="M4.61824 16.3959C5.01386 16 5.55038 15.7776 6.10981 15.7776C6.66932 15.7777 7.20584 16.0001 7.60137 16.396C7.99699 16.7919 8.21926 17.3289 8.21926 17.8888C8.21926 18.4487 7.99699 18.9857 7.60137 19.3816C7.20584 19.7776 6.66932 20 6.10981 20C5.55038 20 5.01386 19.7776 4.61824 19.3818C4.22236 18.9859 4 18.4489 4 17.8888C4 17.3288 4.22236 16.7917 4.61824 16.3959ZM20.4547 19.9436C20.0442 19.9436 19.6505 19.7803 19.3603 19.4897C19.07 19.1992 18.907 18.8051 18.9071 18.3943C18.9111 16.6164 18.5563 14.8561 17.8641 13.2187C16.8486 10.8089 15.1447 8.75272 12.9663 7.30797C10.7879 5.86328 8.23187 5.0945 5.61851 5.09759C5.20772 5.09819 4.81364 4.93521 4.52292 4.64468C4.23226 4.35423 4.06898 3.95992 4.06898 3.5488C4.06898 3.13768 4.23226 2.74346 4.52292 2.45292C4.81366 2.16238 5.20772 1.99948 5.61851 2.00001C7.77018 1.99785 9.901 2.42086 11.8889 3.24482C13.8767 4.06878 15.6824 5.27739 17.2025 6.80155C18.7253 8.32267 19.9332 10.1297 20.7564 12.1192C21.5796 14.1087 22.0022 16.241 22 18.3945C22.001 18.8051 21.8387 19.1994 21.5487 19.4901C21.2588 19.7808 20.8652 19.9439 20.4547 19.9436ZM13.6308 19.9436C13.2202 19.9436 12.8265 19.7803 12.5363 19.4897C12.246 19.1992 12.083 18.8051 12.0831 18.3943C12.0834 17.2585 11.7848 16.1426 11.2174 15.1593C10.6501 14.1756 9.83395 13.3589 8.851 12.7911C7.86813 12.2233 6.75325 11.9247 5.61835 11.925C5.06539 11.925 4.55444 11.6297 4.27792 11.1504C4.00148 10.6712 4.00148 10.0807 4.27792 9.60148C4.55444 9.12219 5.06539 8.82692 5.61835 8.82692C7.49753 8.82761 9.33491 9.38206 10.9011 10.4211C12.4674 11.4602 13.6932 12.938 14.4256 14.6698C14.9251 15.8477 15.1811 17.1147 15.178 18.3942C15.1781 18.8051 15.0152 19.1991 14.7249 19.4897C14.4347 19.7802 14.0413 19.9436 13.6308 19.9436Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
11
crates/frontend/src/components/InfoIcon.vue
Normal file
11
crates/frontend/src/components/InfoIcon.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<template>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M12.1845 6.49999C12.1845 7.32842 11.513 7.99999 10.6845 7.99999C9.8561 7.99999 9.18452 7.32842 9.18452 6.49999C9.18452 5.67156 9.8561 4.99999 10.6845 4.99999C11.513 4.99999 12.1845 5.67156 12.1845 6.49999Z"
|
||||||
|
fill="currentColor" />
|
||||||
|
<path d="M12 18.7213L12 10.7213L9 10.7213" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
<path d="M12 18.7213H14" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
crates/frontend/src/components/LocationIcon.vue
Normal file
7
crates/frontend/src/components/LocationIcon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" class="w-4" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||||||
|
<circle cx="12" cy="10" r="3"></circle>
|
||||||
|
</svg>
|
||||||
|
</template>
|
48
crates/frontend/src/components/Modal.vue
Normal file
48
crates/frontend/src/components/Modal.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps, defineEmits, ref } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["close-modal"]);
|
||||||
|
|
||||||
|
const closeModal = (action) => {
|
||||||
|
emit("close-modal", action);
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="isOpen" class="modal-mask">
|
||||||
|
<div class="modal-wrapper">
|
||||||
|
<div class="modal-container">
|
||||||
|
<div class="flex mb-4">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<button class="mr-0 ml-auto button" @click="closeModal">x</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<slot name="content"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-4">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-mask {
|
||||||
|
@apply fixed z-[666] top-0 left-0 w-full h-full;
|
||||||
|
@apply bg-ctp-crust bg-opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
@apply flex-grow w-full lg:w-[calc(100%-100px)] max-w-[1000px] ml-auto mr-auto lg:mt-20 lg:mb-20 p-5;
|
||||||
|
@apply bg-ctp-crust shadow-ctp-overlay0 border rounded border-ctp-surface1 text-ctp-text;
|
||||||
|
@apply h-screen lg:h-full;
|
||||||
|
}
|
||||||
|
</style>
|
12
crates/frontend/src/components/NavBar.vue
Normal file
12
crates/frontend/src/components/NavBar.vue
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative h-12">
|
||||||
|
<nav class="absolute w-full h-full flex justify-between items-center m-0 pl-3 pr-3 bg-ctp-peach">
|
||||||
|
<div class="items-center m-0 flex">
|
||||||
|
<slot name="left"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="items-center m-0 flex">
|
||||||
|
<slot name="right"></slot>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
6
crates/frontend/src/components/PauseIcon.vue
Normal file
6
crates/frontend/src/components/PauseIcon.vue
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<template>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5.163 3.819C5 4.139 5 4.559 5 5.4v13.2c0 .84 0 1.26.163 1.581a1.5 1.5 0 0 0 .656.655c.32.164.74.164 1.581.164h.2c.84 0 1.26 0 1.581-.163a1.5 1.5 0 0 0 .656-.656c.163-.32.163-.74.163-1.581V5.4c0-.84 0-1.26-.163-1.581a1.5 1.5 0 0 0-.656-.656C8.861 3 8.441 3 7.6 3h-.2c-.84 0-1.26 0-1.581.163a1.5 1.5 0 0 0-.656.656zm9 0C14 4.139 14 4.559 14 5.4v13.2c0 .84 0 1.26.164 1.581a1.5 1.5 0 0 0 .655.655c.32.164.74.164 1.581.164h.2c.84 0 1.26 0 1.581-.163a1.5 1.5 0 0 0 .655-.656c.164-.32.164-.74.164-1.581V5.4c0-.84 0-1.26-.163-1.581a1.5 1.5 0 0 0-.656-.656C17.861 3 17.441 3 16.6 3h-.2c-.84 0-1.26 0-1.581.163a1.5 1.5 0 0 0-.655.656z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
7
crates/frontend/src/components/PlayIcon.vue
Normal file
7
crates/frontend/src/components/PlayIcon.vue
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5.4855 3.14251C5.78671 2.96178 6.16065 2.95235 6.47059 3.11765L21.4706 11.1176C21.7965 11.2914 22 11.6307 22 12C22 12.3693 21.7965 12.7086 21.4706 12.8824L6.47059 20.8824C6.16065 21.0477 5.78671 21.0382 5.4855 20.8575C5.1843 20.6768 5 20.3513 5 20V4C5 3.64874 5.1843 3.32323 5.4855 3.14251Z"
|
||||||
|
fill="currentcolor"></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
68
crates/frontend/src/components/Player.vue
Normal file
68
crates/frontend/src/components/Player.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import PlayIcon from "@/components/PlayIcon.vue";
|
||||||
|
import PauseIcon from "@/components/PauseIcon.vue";
|
||||||
|
import VinylIcon from "@/components/VinylIcon.vue";
|
||||||
|
import { api, store } from "@";
|
||||||
|
import { ref, onUpdated, onMounted } from "vue";
|
||||||
|
|
||||||
|
const player = store.usePlayer();
|
||||||
|
const audioRef = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
player.register(audioRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex sticky w-full mx-auto bottom-2 px-2">
|
||||||
|
<div class="flex rounded-md m-2 bg-ctp-base mx-auto w-full border-2 border-ctp-mantle">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="h-20 flex rounded-l-md aspect-square items-center justify-center bg-ctp-overlay0">
|
||||||
|
<VinylIcon class="p-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col justify-center px-4 py-4">
|
||||||
|
<span class="flex-grow font-bold">{{ player.station ? "ASD" : "Unknown" }}</span>
|
||||||
|
<span class="text-ctp-subtext0">Location</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-8 h-20 p-2">
|
||||||
|
<input type="range" class="slider" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button @click="player.toggle()"
|
||||||
|
class="h-full w-20 inline-flex rounded items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||||
|
<PlayIcon v-if="!player.playing" class="text-ctp-peach w-8 h-8" />
|
||||||
|
<PauseIcon v-else class="text-ctp-peach w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<audio class="hidden" ref="audioRef" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.slider[type=range] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
vertical-align: middle;
|
||||||
|
@apply appearance-none bg-ctp-surface0 hover:bg-ctp-surface1 w-4 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
@apply appearance-none bg-ctp-peach rounded-full cursor-pointer w-4 border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
@apply appearance-none bg-ctp-peach rounded-full cursor-pointer w-4 border-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-progress-value {
|
||||||
|
@apply bg-ctp-peach/20 w-4 rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-progress {
|
||||||
|
@apply bg-ctp-peach/20 w-4 rounded-full;
|
||||||
|
}
|
||||||
|
</style>
|
75
crates/frontend/src/components/Station.vue
Normal file
75
crates/frontend/src/components/Station.vue
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { api } from "@";
|
||||||
|
import PlayIcon from "@/components/PlayIcon.vue";
|
||||||
|
import ExternalIcon from "@/components/ExternalIcon.vue";
|
||||||
|
import LocationIcon from "@/components/LocationIcon.vue"
|
||||||
|
import TicketIcon from "@/components/TicketIcon.vue";
|
||||||
|
import VinylIcon from "@/components/VinylIcon.vue";
|
||||||
|
import InfoIcon from "@/components/InfoIcon.vue";
|
||||||
|
import Tooltip from "@/components/Tooltip.vue";
|
||||||
|
import { store } from "@";
|
||||||
|
|
||||||
|
const player = store.usePlayer();
|
||||||
|
|
||||||
|
const { stationInfo } = defineProps({
|
||||||
|
stationInfo: api.StationInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = () => {
|
||||||
|
if (stationInfo.status === api.StationStatus.Receive) {
|
||||||
|
return api.StationStatus.Online;
|
||||||
|
}
|
||||||
|
if (stationInfo.playback === api.Playback.Stopped || stationInfo.playback === api.Playback.Paused) {
|
||||||
|
return api.StationStatus.Offline;
|
||||||
|
}
|
||||||
|
return stationInfo.status;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex rounded-xl m-2 bg-ctp-base">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="h-20 sm:h-36 flex rounded-l-xl aspect-square items-center justify-center bg-ctp-maroon/50">
|
||||||
|
<VinylIcon class="p-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full flex flex-col justify-center px-4 space-y-2">
|
||||||
|
<div class="flex h-8 sm:h-10 items-center">
|
||||||
|
<span class="flex-grow font-bold text-ctp-peach text-xl sm:text-2xl">{{ stationInfo.name }}</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="hidden sm:flex flex-col space-y-1">
|
||||||
|
<div v-if="stationInfo.location" class="flex items-center space-x-1">
|
||||||
|
<LocationIcon class="text-ctp-subtext0" />
|
||||||
|
<span class="text-ctp-subtext0">{{ stationInfo.location }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="stationInfo.genre" class="flex items-center space-x-1">
|
||||||
|
<TicketIcon class="text-ctp-subtext0" />
|
||||||
|
<span class="text-ctp-subtext0">{{ stationInfo.genre }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center pr-4 space-y-2">
|
||||||
|
<div class="flex items-center justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||||
|
<Tooltip :text="status()">
|
||||||
|
<InfoIcon class="text-ctp-peach" />
|
||||||
|
</Tooltip>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||||
|
<ExternalIcon class="text-ctp-peach" />
|
||||||
|
</button>
|
||||||
|
<button v-if="status()" @click="player.load(stationInfo, true)"
|
||||||
|
class="p-1.5 sm:p-2.5 w-8 sm:w-10 inline-flex rounded-md items-center justify-center bg-ctp-mantle hover:bg-ctp-mantle/50">
|
||||||
|
<PlayIcon class="text-ctp-peach" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
13
crates/frontend/src/components/TicketIcon.vue
Normal file
13
crates/frontend/src/components/TicketIcon.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path
|
||||||
|
d="M24.782 1.606h-7.025l-16.151 16.108 12.653 12.681 16.135-16.093v-7.096l-5.613-5.6zM29.328 13.859l-15.067 15.027-11.147-11.171 15.083-15.044h6.143l4.988 4.976v6.211z"
|
||||||
|
fill="currentcolor">
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
d="M21.867 7.999c0 1.173 0.956 2.128 2.133 2.128s2.133-0.954 2.133-2.128c0-1.174-0.956-2.129-2.133-2.129s-2.133 0.955-2.133 2.129zM25.066 7.999c0 0.585-0.479 1.062-1.066 1.062s-1.066-0.476-1.066-1.062c0-0.586 0.478-1.063 1.066-1.063s1.066 0.477 1.066 1.063z"
|
||||||
|
fill="currentcolor">
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
45
crates/frontend/src/components/Tooltip.vue
Normal file
45
crates/frontend/src/components/Tooltip.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
const { text } = defineProps(["text"]);
|
||||||
|
const isShow = ref(false);
|
||||||
|
const posX = ref(0);
|
||||||
|
const posY = ref(0);
|
||||||
|
const anchor = ref(null);
|
||||||
|
|
||||||
|
const showTooltip = (event, value) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (isShow.value)
|
||||||
|
return;
|
||||||
|
|
||||||
|
posX.value = anchor.value.getBoundingClientRect().left;
|
||||||
|
posY.value = anchor.value.getBoundingClientRect().top;
|
||||||
|
isShow.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeTooltip = () => {
|
||||||
|
isShow.value = false;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="anchor" @mouseover="showTooltip" @mouseleave="closeTooltip" style="display: unset">
|
||||||
|
<slot></slot>
|
||||||
|
<div class="tooltip" :style="{ top: posY + 32 + 'px', left: posX + 16 + 'px' }" v-if="isShow">
|
||||||
|
{{
|
||||||
|
text
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.tooltip {
|
||||||
|
@apply hidden sm:inline z-[666] absolute bg-ctp-mantle rounded p-2 border border-ctp-surface0 max-w-[600px];
|
||||||
|
overflow-wrap: break-all;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
42
crates/frontend/src/components/VinylIcon.vue
Normal file
42
crates/frontend/src/components/VinylIcon.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<template>
|
||||||
|
<svg viewBox="0 0 187.62987 187.62987" version="1.1" id="svg1" xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs id="defs1" />
|
||||||
|
<g id="layer1" transform="translate(-254,-327.14301)">
|
||||||
|
<g id="g22" style="display:inline" transform="translate(-12.05508,278.08297)">
|
||||||
|
<path id="path29"
|
||||||
|
style="fill:#000000;fill-opacity:0.500678;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:0.250793"
|
||||||
|
d="M 359.87002,49.060055 A 93.814941,93.814941 0 0 0 266.05508,142.875 93.814941,93.814941 0 0 0 359.87002,236.68995 93.814941,93.814941 0 0 0 453.68497,142.875 93.814941,93.814941 0 0 0 359.87002,49.060055 Z m 0,92.013505 a 1.801514,1.801514 0 0 1 1.80144,1.80144 1.801514,1.801514 0 0 1 -1.80144,1.80144 1.801514,1.801514 0 0 1 -1.80144,-1.80144 1.801514,1.801514 0 0 1 1.80144,-1.80144 z" />
|
||||||
|
<path id="path31"
|
||||||
|
style="fill:#000000;fill-opacity:0.500678;stroke:none;stroke-width:1.10062;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 359.87002,111.42486 a 31.450164,31.450164 0 0 0 -31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,31.45014 31.450164,31.450164 0 0 0 31.45014,-31.45014 31.450164,31.450164 0 0 0 -31.45014,-31.45014 z m 0,29.46744 a 1.9827772,1.9827772 0 0 1 1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,1.9827 1.9827772,1.9827772 0 0 1 -1.9827,-1.9827 1.9827772,1.9827772 0 0 1 1.9827,-1.9827 z" />
|
||||||
|
<g id="g21" style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089">
|
||||||
|
<path
|
||||||
|
d="m 293.5304,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 0,36.64663 29.70797,66.3546 66.3546,66.35461 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-8.90592 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 M 302.42131,142.875 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path21" />
|
||||||
|
<path
|
||||||
|
d="m 359.87006,218.07661 c 0.0563,-0.004 0.11267,-0.009 0.16899,-0.0134 m 0,-8.84856 c -0.0563,0.005 -0.11265,0.0101 -0.16899,0.015 m -66.3546,-66.35461 c 0.005,-0.0563 0.01,-0.11266 0.015,-0.16898 m -8.84861,-4e-5 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c 10e-6,41.5327 33.66891,75.2016 75.20161,75.20161"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path20" />
|
||||||
|
<path
|
||||||
|
d="m 284.66841,142.87497 c 0.004,-0.0563 0.009,-0.11266 0.0134,-0.16898 m -8.72505,0 c -0.005,0.0563 -0.009,0.11265 -0.0134,0.16898 m 0,0 c -4e-5,46.35143 37.57523,83.9267 83.92666,83.92666 m 0,0 c 0.0563,-0.004 0.11266,-0.009 0.16899,-0.0134 m 0,0 v -1e-5 m 0,-8.72504 c -0.0563,0.005 -0.11265,0.009 -0.16899,0.0134"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path19" />
|
||||||
|
<path
|
||||||
|
d="m 302.43632,142.70602 c -0.005,0.0563 -0.0101,0.11265 -0.015,0.16898 m 0,0 c 1.3e-4,31.72798 25.72071,57.44856 57.44869,57.44869 m 0,0 c 0.0563,-0.005 0.11267,-0.01 0.16899,-0.015 m 0,-9.79165 c -0.0563,0.006 -0.11265,0.0111 -0.16899,0.0165 m 0,0 c -26.32115,8e-5 -47.65867,-21.33744 -47.65859,-47.65859 m 0,0 c 0.005,-0.0563 0.0109,-0.11266 0.0165,-0.16898 m 0,0 5e-5,5e-5"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path12" />
|
||||||
|
<path
|
||||||
|
d="m 359.87006,85.426307 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,0 -4e-5,-1e-6 m 0,9.791651 c 0.0563,-0.0056 0.11264,-0.01113 0.16898,-0.01654 m 0,0 c 26.32115,-7.9e-5 47.65867,21.337443 47.65859,47.658593"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path8" />
|
||||||
|
<path
|
||||||
|
d="m 359.87002,76.52039 c -0.0563,0.0049 -0.11266,0.0099 -0.16898,0.01499 m 0,8.905916 c 0.0563,-0.0051 0.11265,-0.01008 0.16898,-0.01499 m 0,0 c 31.72819,-1.6e-4 57.44908,25.720504 57.44921,57.448694"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path6" />
|
||||||
|
<path
|
||||||
|
d="m 359.70104,76.53538 c 0.0563,-0.0051 0.11265,-0.01006 0.16898,-0.01499 m 0,0 c 36.64664,2e-6 66.35461,29.70797 66.35461,66.35461 m 0,0 c -0.005,0.0563 -0.01,0.11266 -0.015,0.16898 m 8.84856,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c -1e-5,-41.5327 -33.66891,-75.201604 -75.20161,-75.201615 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,0 h 4e-5"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path4" />
|
||||||
|
<path
|
||||||
|
d="m 435.07163,142.875 c -0.004,0.0563 -0.009,0.11265 -0.0134,0.16898 m 8.72505,0 c 0.005,-0.0563 0.009,-0.11265 0.0134,-0.16898 m 0,0 c 4e-5,-46.351434 -37.57523,-83.926703 -83.92666,-83.926664 m 0,0 c -0.0563,0.0044 -0.11266,0.0089 -0.16898,0.01344 m 0,8.725049 c 0.0563,-0.0045 0.11265,-0.009 0.16898,-0.01344"
|
||||||
|
style="fill:none;fill-opacity:0.500678;stroke:#676767;stroke-opacity:0.304089" id="path2" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</template>
|
9
crates/frontend/src/index.ts
Normal file
9
crates/frontend/src/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export * as plugins from "@/plugins";
|
||||||
|
export { router } from "@/router";
|
||||||
|
export { client } from "@/client.ts";
|
||||||
|
export * as api from "@/client.ts";
|
||||||
|
export * as schemas from "@/client/schemas.gen.ts";
|
||||||
|
export * as api_types from "@/client/types.gen.ts";
|
||||||
|
export * as store from "@/store";
|
||||||
|
export * as style from "@/assets/style.css";
|
||||||
|
export * as types from "@/types";
|
21
crates/frontend/src/main.ts
Normal file
21
crates/frontend/src/main.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import App from "@/App.vue";
|
||||||
|
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import { plugins, router, client, style } from "@";
|
||||||
|
|
||||||
|
|
||||||
|
const debug = import.meta.hot;
|
||||||
|
|
||||||
|
client.setConfig({
|
||||||
|
baseURL: debug ? "http://localhost:54605" : "/",
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
createApp(App)
|
||||||
|
.use(createPinia())
|
||||||
|
.use(router)
|
||||||
|
.directive("click-outside", plugins.clickOutside)
|
||||||
|
.directive("tooltip", plugins.tooltip)
|
||||||
|
.mount('#app');
|
||||||
|
|
34
crates/frontend/src/plugins/directives.ts
Normal file
34
crates/frontend/src/plugins/directives.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export const clickOutside = {
|
||||||
|
beforeMount: function(element: any, binding: any) {
|
||||||
|
element.clickOutsideEvent = function(event: any) {
|
||||||
|
if (!(element == event.target || element.contains(event.target))) {
|
||||||
|
binding.value(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener("click", element.clickOutsideEvent);
|
||||||
|
document.body.addEventListener("contextmenu", element.clickOutsideEvent);
|
||||||
|
},
|
||||||
|
unmounted: function(element: any) {
|
||||||
|
document.body.removeEventListener("click", element.clickOutsideEvent);
|
||||||
|
document.body.removeEventListener("contextmenu", element.clickOutsideEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tooltip = {
|
||||||
|
beforeMount: function (element: any, binding: any) {
|
||||||
|
element.tooltip = function (event) {
|
||||||
|
let target = event.target;
|
||||||
|
if (target.offsetWidth < target.scrollWidth) {
|
||||||
|
target.setAttribute('title', binding.value?.text ? binding.value.text : event.target.textContent);
|
||||||
|
} else {
|
||||||
|
target.hasAttribute('title') && target.removeAttribute('title');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('mouseover', element.tooltip);
|
||||||
|
},
|
||||||
|
unmounted: function(element: any) {
|
||||||
|
document.body.removeEventListener("mouseover", element.tooltip);
|
||||||
|
}
|
||||||
|
};
|
1
crates/frontend/src/plugins/index.ts
Normal file
1
crates/frontend/src/plugins/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "@/plugins/directives";
|
19
crates/frontend/src/router.ts
Normal file
19
crates/frontend/src/router.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { createRouter, createWebHistory, useRoute } from "vue-router";
|
||||||
|
import { store, api, schemas } from "@";
|
||||||
|
|
||||||
|
|
||||||
|
export const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: "/", name: "home",
|
||||||
|
component: () => import("@/views/Home.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)*", name: "not-found",
|
||||||
|
component: () => import("@/views/NotFound.vue")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
49
crates/frontend/src/store.ts
Normal file
49
crates/frontend/src/store.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { ref, type Ref } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import axios, { CancelToken } from "axios";
|
||||||
|
import { api } from "@";
|
||||||
|
|
||||||
|
export const usePlayer = defineStore("player", () => {
|
||||||
|
const station = ref(null);
|
||||||
|
const playing = ref(false);
|
||||||
|
const instance = ref(null);
|
||||||
|
|
||||||
|
const register = (_instance) => {
|
||||||
|
instance.value = _instance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = (station: api.StationInfo, start: bool = false) => {
|
||||||
|
station.value = station;
|
||||||
|
instance.value.src = station.value.url;
|
||||||
|
instance.value.load();
|
||||||
|
if (start) {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const play = () => {
|
||||||
|
if (!instance.value.src)
|
||||||
|
return;
|
||||||
|
instance.value.play();
|
||||||
|
playing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pause = () => {
|
||||||
|
if (!instance.value.src)
|
||||||
|
return;
|
||||||
|
instance.value.pause();
|
||||||
|
playing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (playing.value) {
|
||||||
|
pause();
|
||||||
|
} else {
|
||||||
|
play();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return { station, playing, load, instance, register, play, pause, toggle };
|
||||||
|
});
|
30
crates/frontend/src/types.ts
Normal file
30
crates/frontend/src/types.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { api_types } from "@";
|
||||||
|
import { CancelToken } from "axios";
|
||||||
|
|
||||||
|
export type Error = object;
|
||||||
|
|
||||||
|
export type RepositoryItemMeta = {
|
||||||
|
selected: bool;
|
||||||
|
clickCount: number;
|
||||||
|
clickTimer: (object | null);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryItemType = "directory" | "file";
|
||||||
|
|
||||||
|
export type RepositoryItemInfo = {
|
||||||
|
info: (api_types.DirectoryInfo | api_types.FileInfo);
|
||||||
|
meta: RepositoryItemMeta;
|
||||||
|
type: RepositoryItemType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RepositoryContent = RepositoryItemInfo[];
|
||||||
|
|
||||||
|
export type UploadFileStatus = "success" | "fail" | "transfer" | "idle";
|
||||||
|
|
||||||
|
export type UploadFile = {
|
||||||
|
content: File;
|
||||||
|
status: UploadFileStatus;
|
||||||
|
progress: number;
|
||||||
|
cancel: CancelToken | null;
|
||||||
|
error: string | null;
|
||||||
|
};
|
40
crates/frontend/src/views/Base.vue
Normal file
40
crates/frontend/src/views/Base.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { router, api, store } from "@";
|
||||||
|
import NavBar from "@/components/NavBar.vue";
|
||||||
|
import DocumentationIcon from "@/components/DocumentationIcon.vue";
|
||||||
|
import Player from "@/components/Player.vue";
|
||||||
|
import Error from "@/components/Error.vue";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<NavBar>
|
||||||
|
<template #left>
|
||||||
|
<RouterLink class="link-button" to="/">Home</RouterLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<RouterLink class="link-button" to="/auth/signin">Sign In</RouterLink>
|
||||||
|
</template>
|
||||||
|
</NavBar>
|
||||||
|
|
||||||
|
<main class="w-full max-w-[1000px] mx-auto">
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Player />
|
||||||
|
|
||||||
|
<footer class="flex justify-between pb-2 pt-2 pl-5 pr-5 bg-ctp-mantle">
|
||||||
|
<a href="https://vcs.elnafo.ru/L-Nafaryus" class="text-ctp-peach">Made by L-Nafaryus, 2024</a>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<a href="/api/docs" class="flex justify-center items-center space-x-1 text-ctp-peach">
|
||||||
|
<DocumentationIcon />
|
||||||
|
<span>API</span>
|
||||||
|
</a>
|
||||||
|
</footer>
|
||||||
|
</template>
|
45
crates/frontend/src/views/Home.vue
Normal file
45
crates/frontend/src/views/Home.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Base from "@/views/Base.vue";
|
||||||
|
import PlayIcon from "@/components/PlayIcon.vue";
|
||||||
|
import ExternalIcon from "@/components/ExternalIcon.vue";
|
||||||
|
import LocationIcon from "@/components/LocationIcon.vue";
|
||||||
|
import Error from "@/components/Error.vue";
|
||||||
|
import Station from "@/components/Station.vue";
|
||||||
|
import { api } from "@";
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
|
||||||
|
const error = ref(null);
|
||||||
|
const stations = ref(null);
|
||||||
|
const update = ref(null);
|
||||||
|
|
||||||
|
const stationsInfo = async () => {
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
await api.stationsInfo({ throwOnError: true })
|
||||||
|
.then(async stationsInfo => {
|
||||||
|
stations.value = stationsInfo.data;
|
||||||
|
console.log(stations.value);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
stations.value = null;
|
||||||
|
error.value = "Failed to get stations list";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await stationsInfo();
|
||||||
|
update.value = setInterval(stationsInfo, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(update.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Base>
|
||||||
|
<Error :value="error" />
|
||||||
|
<Station :stationInfo="station" v-for="station in stations" />
|
||||||
|
</Base>
|
||||||
|
</template>
|
10
crates/frontend/src/views/NotFound.vue
Normal file
10
crates/frontend/src/views/NotFound.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Base from "@/views/Base.vue";
|
||||||
|
import Error from "@/components/Error.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Base>
|
||||||
|
<Error>404 Not Found</Error>
|
||||||
|
</Base>
|
||||||
|
</template>
|
32
crates/frontend/tailwind.config.js
Normal file
32
crates/frontend/tailwind.config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./index.html", "./src/**/*.{vue,ts,js}"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
"border-spin": {
|
||||||
|
"100%": {
|
||||||
|
transform: "rotate(-360deg)",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"border-roll": {
|
||||||
|
"100%": {
|
||||||
|
"background-position": "200% 0",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"border-spin": "border-spin 7s linear infinite",
|
||||||
|
"border-roll": "border-roll 5s linear infinite"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require("@catppuccin/tailwindcss")({
|
||||||
|
prefix: "ctp",
|
||||||
|
defaultFlavour: "macchiato"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
14
crates/frontend/templates/base.html
Normal file
14
crates/frontend/templates/base.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="h-full">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/resources/assets/logo.svg">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Materia</title>
|
||||||
|
<script type="module" crossorigin src="/resources/assets/index.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/resources/assets/index.css">
|
||||||
|
</head>
|
||||||
|
<body class="h-full bg-zinc-900 text-zinc-200 font-sans">
|
||||||
|
<div id="{{ view }}" class="flex flex-col h-full"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
21
crates/frontend/tsconfig.app.json
Normal file
21
crates/frontend/tsconfig.app.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": [
|
||||||
|
"env.d.ts",
|
||||||
|
"src/**/*",
|
||||||
|
"src/**/*.vue"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/**/__tests__/*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
crates/frontend/tsconfig.json
Normal file
11
crates/frontend/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.vite.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
20
crates/frontend/tsconfig.vite.json
Normal file
20
crates/frontend/tsconfig.vite.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@tsconfig/node18/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": [
|
||||||
|
"node",
|
||||||
|
"vite"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
27
crates/frontend/vite.config.ts
Normal file
27
crates/frontend/vite.config.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { fileURLToPath, URL } from "node:url"
|
||||||
|
import { defineConfig } from "vite"
|
||||||
|
import vue from "@vitejs/plugin-vue"
|
||||||
|
import vueJsx from "@vitejs/plugin-vue-jsx"
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueJsx(),
|
||||||
|
],
|
||||||
|
build: {
|
||||||
|
//outDir: path.resolve(__dirname, "./frontend"),
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: "resources/assets/[name].js",
|
||||||
|
assetFileNames: "resources/assets/[name][extname]",
|
||||||
|
chunkFileNames: "resources/assets/[name].js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user