Compare commits
4 Commits
60eb0d3ff1
...
2ddf210a5f
Author | SHA1 | Date | |
---|---|---|---|
2ddf210a5f | |||
bef20ccc5a | |||
c713fcaae8 | |||
32d131db67 |
@ -9,10 +9,10 @@
|
|||||||
<title>Solid App</title>
|
<title>Solid App</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter+Tight:ital,wght@0,100..900;1,100..900&family=Inter:wght@100..900&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-theme="black">
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
||||||
|
11
frontend/src/icons/ArrowClockwiseIcon.tsx
Normal file
11
frontend/src/icons/ArrowClockwiseIcon.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export function ArrowClockwiseIcon(props: {
|
||||||
|
fill: string,
|
||||||
|
class?: string,
|
||||||
|
size?: number,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg class={props.class} width={props.size ?? 32} height={props.size ?? 32} fill={props.fill} viewBox="0 0 256 256">
|
||||||
|
<path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1-5.66-13.66l17-17-10.55-9.65-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,1,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60l10.93,10L226.34,50.3A8,8,0,0,1,240,56Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
12
frontend/src/icons/PlayIcon.tsx
Normal file
12
frontend/src/icons/PlayIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
export function PlayIcon(props: {
|
||||||
|
fill: string,
|
||||||
|
class?: string,
|
||||||
|
size?: number,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<svg class={props.class} width={props.size ?? 32} height={props.size ?? 32} fill={props.fill} viewBox="0 0 256 256">
|
||||||
|
<path d="M240,128a15.74,15.74,0,0,1-7.6,13.51L88.32,229.65a16,16,0,0,1-16.2.3A15.86,15.86,0,0,1,64,216.13V39.87a15.86,15.86,0,0,1,8.12-13.82,16,16,0,0,1,16.2.3L232.4,114.49A15.74,15.74,0,0,1,240,128Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
@ -2,8 +2,48 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Dark mode */
|
||||||
|
:root {
|
||||||
|
--c-bg: #09090b;
|
||||||
|
--c-on-bg: white;
|
||||||
|
--c-border-1: #a1a1aa;
|
||||||
|
|
||||||
|
--c-bg-inversed: rgba(228, 228, 240, 0.75);
|
||||||
|
|
||||||
|
--c-primary: #c4b5fd;
|
||||||
|
--c-on-primary: #09090b;
|
||||||
|
|
||||||
|
--c-primary-bg: #1f162c;
|
||||||
|
--c-primary-bg-2: #332a3f;
|
||||||
|
--c-on-primary-bg: #f5f3ff;
|
||||||
|
|
||||||
|
--c-primary-border: #6b58a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode */
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--c-bg: white;
|
||||||
|
--c-on-bg: #09090b;
|
||||||
|
--c-border-1: #a1a1aa;
|
||||||
|
|
||||||
|
--c-bg-inversed: rgba(39, 39, 42, 0.75);
|
||||||
|
|
||||||
|
--c-primary: #7c3aed;
|
||||||
|
--c-on-primary: white;
|
||||||
|
|
||||||
|
--c-primary-bg: #f5f3ff;
|
||||||
|
--c-primary-bg-2: #ede9fe;
|
||||||
|
--c-on-primary-bg: #110627;
|
||||||
|
|
||||||
|
--c-primary-border: #c4b5fd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-optical-sizing: auto;
|
font-optical-sizing: auto;
|
||||||
font-variation-settings: "slnt" 0;
|
font-variation-settings: "slnt" 0;
|
||||||
|
background-color: var(--c-bg);
|
||||||
|
color: var(--c-on-bg);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ import { batch, createEffect, createMemo, createResource, createSignal, onMount
|
|||||||
import { GetAlbumCover, GetRandomAlbums, TriggerAlbumReload } from "../../wailsjs/go/main/App";
|
import { GetAlbumCover, GetRandomAlbums, TriggerAlbumReload } from "../../wailsjs/go/main/App";
|
||||||
import { main } from "../../wailsjs/go/models";
|
import { main } from "../../wailsjs/go/models";
|
||||||
import { createStore } from "solid-js/store";
|
import { createStore } from "solid-js/store";
|
||||||
|
import { ArrowClockwiseIcon } from "../icons/ArrowClockwiseIcon";
|
||||||
|
import { PlayIcon } from "../icons/PlayIcon";
|
||||||
|
|
||||||
type AlbumsData = {
|
type AlbumsData = {
|
||||||
status: "loading" | "ok",
|
status: "loading" | "ok",
|
||||||
@ -24,7 +26,7 @@ export function Home() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Loads the random albums from Go.
|
// Loads the random albums from Go.
|
||||||
// This function, when called multiple times, returns the same set.
|
// This function returns the same set when called multiple times.
|
||||||
const loadAlbums = async() => {
|
const loadAlbums = async() => {
|
||||||
const response = await GetRandomAlbums();
|
const response = await GetRandomAlbums();
|
||||||
|
|
||||||
@ -54,16 +56,16 @@ export function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`min-h-screen ${hidden() ? "opacity-0" : "opacity-100"} transition-opacity`}>
|
<div class={`min-h-screen ${hidden() ? "opacity-0" : "opacity-100"} transition-opacity`}>
|
||||||
<h1 class="font-black text-2xl pt-6 pb-4 pl-2">
|
<h1 class="font-title font-black text-2xl pt-6 pb-4 pl-2">
|
||||||
Random albums
|
<span class="text-c-primary">Random albums</span>
|
||||||
<button
|
<button
|
||||||
class="text-xs font-normal mx-1 p-1 rounded underline hover:bg-zinc-900"
|
class="mx-1 p-1 rounded underline hover:bg-c-primary-bg-2 cursor-pointer"
|
||||||
onClick={triggerAlbumReload}
|
onClick={triggerAlbumReload}
|
||||||
>
|
>
|
||||||
Reload
|
<ArrowClockwiseIcon class="h-4 w-4 inline-block" fill="var(--c-primary)" />
|
||||||
</button>
|
</button>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="pb-4 overflow-scroll whitespace-nowrap">
|
<div class="pb-1 overflow-x-scroll whitespace-nowrap">
|
||||||
{els}
|
{els}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -109,22 +111,30 @@ function Album(props: { albums: Array<main.Album | null>, idx: number }) {
|
|||||||
const imgOpacity = createMemo(() => (imgReady() && !isNull() ? "opacity-100" : "opacity-0"));
|
const imgOpacity = createMemo(() => (imgReady() && !isNull() ? "opacity-100" : "opacity-0"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="inline-block mx-2 p-1 w-32 rounded bg-zinc-900">
|
<div class="inline-block mx-1 p-1 w-32 rounded bg-c-primary-bg text-c-on-primary-bg
|
||||||
<div class="w-30 h-30" >
|
hover:shadow hover:bg-c-primary-bg-2 transition-shadow group"
|
||||||
|
>
|
||||||
|
<div class="w-30 h-30 relative" >
|
||||||
<img
|
<img
|
||||||
class={`inline-block rounded w-30 h-30 transition-opacity ${imgOpacity()}`}
|
class={`inline-block rounded w-30 h-30 transition-opacity ${imgOpacity()}`}
|
||||||
src={imageBase64()}
|
src={imageBase64()}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="inline-block absolute bottom-1 right-1 bg-[var(--c-bg-inversed)] z-10 cursor-pointer h-8 w-8 rounded-full
|
||||||
|
hover:bg-c-primary transition-all opacity-0 group-hover:opacity-100"
|
||||||
|
>
|
||||||
|
<PlayIcon class="inline-block p-2 h-full w-full" fill="var(--c-primary-bg)" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={`text-sm overflow-hidden overflow-ellipsis pt-1 ${opacity()}`} title={albumName()}>
|
<div class={`font-title font-medium text-sm overflow-hidden overflow-ellipsis pt-1 ${opacity()}`} title={albumName()}>
|
||||||
{albumName()}
|
{albumName()}
|
||||||
</div>
|
</div>
|
||||||
<div class={`text-xs overflow-hidden overflow-ellipsis ${opacity()}`} title={artistName()}>
|
<div class={`text-xs overflow-hidden overflow-ellipsis ${opacity()}`} title={artistName()}>
|
||||||
{artistName()}
|
{artistName()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,26 @@
|
|||||||
import { Show, createSignal } from "solid-js";
|
import { Show, createSignal, onMount } from "solid-js";
|
||||||
import { Login as GoLogin } from "../../wailsjs/go/main/App";
|
import { Login as GoLogin } from "../../wailsjs/go/main/App";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
|
function runAndDelay(ms: number, fn: () => void): Promise<void> {
|
||||||
|
fn();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = (ms: number, fn: () => void) => new Promise((resolve) => setTimeout(() => {
|
||||||
|
fn();
|
||||||
|
resolve(null);
|
||||||
|
}, ms));
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [server, setServer] = createSignal("");
|
const [server, setServer] = createSignal("");
|
||||||
const [username, setUsername] = createSignal("");
|
const [username, setUsername] = createSignal("");
|
||||||
const [password, setPassword] = createSignal("");
|
const [password, setPassword] = createSignal("");
|
||||||
const [fade, setFade] = createSignal(false);
|
const [fadeUI, setFadeUI] = createSignal(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const login = async(ev: Event) => {
|
const login = async(ev: Event) => {
|
||||||
@ -17,54 +29,55 @@ export function Login() {
|
|||||||
setError("");
|
setError("");
|
||||||
|
|
||||||
GoLogin(server(), username(), password())
|
GoLogin(server(), username(), password())
|
||||||
.then(() => {
|
.then(() => runAndDelay(150, () => setFadeUI(true)))
|
||||||
setFade(true);
|
.then(() => navigate("/home"))
|
||||||
setTimeout(() => {
|
|
||||||
navigate("/home");
|
|
||||||
}, 150);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setError(err);
|
setError(err);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setFadeUI(true);
|
||||||
|
delay(150, () => setFadeUI(false));
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`w-screen h-screen flex items-center justify-center ${fade() ? "opacity-0" : "opacity-100"} transition-opacity`}>
|
<div class={`w-screen h-screen flex items-center justify-center ${fadeUI() ? "opacity-0" : "opacity-100"} transition-opacity`}>
|
||||||
<div class="w-80">
|
<div class="max-w-80 px-1 overflow-hidden">
|
||||||
<form onSubmit={login} >
|
<form onSubmit={login}>
|
||||||
<h1 class="text-center font-black text-xl">Login</h1>
|
<h1 class="text-center font-title font-black text-xl text-c-primary mb-4">Login to Navidrome</h1>
|
||||||
<label class="form-control" for="login-server">
|
<label for="login-server">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text text-xs">Server URL</span>
|
<span class="font-title label-text text-xs">Server URL:</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="login-server" type="text"
|
<input id="login-server" type="text"
|
||||||
onInput={(e) => setServer(e.target.value)}
|
onInput={(e) => setServer(e.target.value)}
|
||||||
class="input input-bordered"
|
class="border border-c-primary-border rounded bg-c-primary-bg inline-block w-full p-1"
|
||||||
placeholder="https://"
|
placeholder="https://"
|
||||||
required
|
required
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="form-control" for="login-username">
|
<label for="login-username">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text text-xs">Username</span>
|
<span class="font-title label-text text-xs">Username</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="login-username" type="text"
|
<input id="login-username" type="text"
|
||||||
onInput={(e) => setUsername(e.target.value)}
|
onInput={(e) => setUsername(e.target.value)}
|
||||||
class="input input-bordered"
|
class="border border-c-primary-border rounded bg-c-primary-bg inline-block w-full p-1"
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
required
|
required
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="form-control" for="login-password">
|
<label for="login-password">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text text-xs">Password</span>
|
<span class="font-title label-text text-xs">Password</span>
|
||||||
</div>
|
</div>
|
||||||
<input id="login-password" type="password"
|
<input id="login-password" type="password"
|
||||||
onInput={(e) => setPassword(e.target.value)}
|
onInput={(e) => setPassword(e.target.value)}
|
||||||
class="input input-bordered"
|
class="border border-c-primary-border rounded bg-c-primary-bg inline-block w-full p-1"
|
||||||
placeholder="********"
|
placeholder="********"
|
||||||
required
|
required
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
@ -72,7 +85,9 @@ export function Login() {
|
|||||||
</label>
|
</label>
|
||||||
<br />
|
<br />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<button type="submit" class={`btn btn-primary ${loading() ? "animate-pulse" : ""}`}
|
<button
|
||||||
|
type="submit"
|
||||||
|
class={`rounded font-title bg-c-primary text-c-on-primary py-2 px-4 mt-6 ${loading() ? "animate-pulse" : ""}`}
|
||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import daisyui from "daisyui";
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
@ -10,45 +8,20 @@ export default {
|
|||||||
spacing: {
|
spacing: {
|
||||||
"30": "7.5rem",
|
"30": "7.5rem",
|
||||||
},
|
},
|
||||||
|
fontFamily: {
|
||||||
|
"title": ["'Inter Tight'", "Inter", "sans-serif"],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
"c-bg": "var(--c-bg)",
|
||||||
|
"c-on-bg": "var(--c-on-bg)",
|
||||||
|
"c-border-1": "var(--c-border-1)",
|
||||||
|
"c-primary": "var(--c-primary)",
|
||||||
|
"c-on-primary": "var(--c-on-primary)",
|
||||||
|
"c-primary-bg": "var(--c-primary-bg)",
|
||||||
|
"c-primary-bg-2": "var(--c-primary-bg-2)",
|
||||||
|
"c-on-primary-bg": "var(--c-on-primary-bg)",
|
||||||
|
"c-primary-border": "var(--c-primary-border)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
|
||||||
daisyui,
|
|
||||||
],
|
|
||||||
daisyui: {
|
|
||||||
themes: [
|
|
||||||
"light",
|
|
||||||
"dark",
|
|
||||||
"cupcake",
|
|
||||||
"bumblebee",
|
|
||||||
"emerald",
|
|
||||||
"corporate",
|
|
||||||
"synthwave",
|
|
||||||
"retro",
|
|
||||||
"cyberpunk",
|
|
||||||
"valentine",
|
|
||||||
"halloween",
|
|
||||||
"garden",
|
|
||||||
"forest",
|
|
||||||
"aqua",
|
|
||||||
"lofi",
|
|
||||||
"pastel",
|
|
||||||
"fantasy",
|
|
||||||
"wireframe",
|
|
||||||
"black",
|
|
||||||
"luxury",
|
|
||||||
"dracula",
|
|
||||||
"cmyk",
|
|
||||||
"autumn",
|
|
||||||
"business",
|
|
||||||
"acid",
|
|
||||||
"lemonade",
|
|
||||||
"night",
|
|
||||||
"coffee",
|
|
||||||
"winter",
|
|
||||||
"dim",
|
|
||||||
"nord",
|
|
||||||
"sunset",
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
6
main.go
6
main.go
@ -26,10 +26,8 @@ func main() {
|
|||||||
Title: "my-wails-solid",
|
Title: "my-wails-solid",
|
||||||
Width: 1024,
|
Width: 1024,
|
||||||
Height: 768,
|
Height: 768,
|
||||||
MinWidth: 1024,
|
MinWidth: 250,
|
||||||
MinHeight: 768,
|
MinHeight: 400,
|
||||||
MaxWidth: 1280,
|
|
||||||
MaxHeight: 800,
|
|
||||||
DisableResize: false,
|
DisableResize: false,
|
||||||
Fullscreen: false,
|
Fullscreen: false,
|
||||||
Frameless: false,
|
Frameless: false,
|
||||||
|
@ -23,12 +23,12 @@ var randomAlbums []Album
|
|||||||
var serverUrl = ""
|
var serverUrl = ""
|
||||||
var client = resty.New()
|
var client = resty.New()
|
||||||
|
|
||||||
// (Tries to) login to a remote navidrome server
|
// Attempts to login to a remote navidrome server
|
||||||
func (a *App) Login(server, username, password string) (bool, error) {
|
func (a *App) Login(server, username, password string) (bool, error) {
|
||||||
log.Print("begin Login to server")
|
log.Print("begin Login to server")
|
||||||
// client := resty.New()
|
// client := resty.New()
|
||||||
|
|
||||||
// TODO: check server for leading https and trailing /, normalize
|
// TODO: check server string for leading https and trailing /, normalize
|
||||||
|
|
||||||
successData := AuthSuccess{}
|
successData := AuthSuccess{}
|
||||||
errorData := AuthError{}
|
errorData := AuthError{}
|
||||||
|
Loading…
Reference in New Issue
Block a user