Add a source picker to search and use the full window width

Search now queries YouTube and SoundCloud together by default and
interleaves the two result lists so neither buries the other; a picker
left of the input narrows it to one source (plus the local library when
configured), and a prefix typed into the query still outranks it.

The panel was capped at 1180px, which left most of a wide screen empty —
it now scales to 1680px and gives the search column the extra room.

A dead link also reported "could not parse yt-dlp's response", which
described our parser rather than the problem; it now shows yt-dlp's own
error line, or says the link did not open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-09 00:11:29 +03:00
co-authored by Claude Opus 5
parent f84489e27e
commit 971fd65b1f
8 changed files with 96 additions and 21 deletions
+4 -2
View File
@@ -42,8 +42,10 @@ export const api = {
state: (serverId: string) => request<ServerStateResponse>(`/api/servers/${serverId}/state`),
search: (serverId: string, query: string) =>
request<{ tracks: Track[] }>(`/api/servers/${serverId}/search?q=${encodeURIComponent(query)}`),
search: (serverId: string, query: string, source: string) =>
request<{ tracks: Track[] }>(
`/api/servers/${serverId}/search?q=${encodeURIComponent(query)}&source=${source}`,
),
play: (
serverId: string,
+18 -1
View File
@@ -16,8 +16,11 @@ interface Props {
onError(message: string | null): void;
}
const SOURCE_KEY = "mbot.searchSource";
export function SearchPanel({ serverId, canControl, localLibrary, onError }: Props) {
const [query, setQuery] = useState("");
const [source, setSource] = useState<string>(() => localStorage.getItem(SOURCE_KEY) ?? "all");
const [results, setResults] = useState<Track[]>([]);
const [busy, setBusy] = useState(false);
const [lastAdded, setLastAdded] = useState<string | null>(null);
@@ -38,7 +41,7 @@ export function SearchPanel({ serverId, canControl, localLibrary, onError }: Pro
setQuery("");
return;
}
const { tracks } = await api.search(serverId, value);
const { tracks } = await api.search(serverId, value, source);
setResults(tracks);
setSearched(true);
} catch (err) {
@@ -62,6 +65,20 @@ export function SearchPanel({ serverId, canControl, localLibrary, onError }: Pro
<div className="card grow">
<h2>Поиск</h2>
<form className="search-form" onSubmit={submit}>
<select
value={source}
onChange={(event) => {
setSource(event.target.value);
localStorage.setItem(SOURCE_KEY, event.target.value);
}}
disabled={!canControl}
title="Где искать"
>
<option value="all">Везде</option>
<option value="youtube">YouTube</option>
<option value="soundcloud">SoundCloud</option>
{localLibrary && <option value="local">Медиатека</option>}
</select>
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
+15 -4
View File
@@ -105,9 +105,9 @@ select:focus {
}
.app {
max-width: 1180px;
max-width: 1680px;
margin: 0 auto;
padding: 20px 18px 60px;
padding: 20px 24px 60px;
}
.topbar {
@@ -151,11 +151,17 @@ select:focus {
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
gap: 18px;
align-items: stretch;
}
@media (min-width: 1500px) {
.layout {
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
}
}
.layout > div {
display: flex;
flex-direction: column;
@@ -180,7 +186,7 @@ select:focus {
.card.grow {
display: flex;
flex-direction: column;
min-height: 460px;
min-height: 360px;
}
.card.grow .track-list {
@@ -431,6 +437,11 @@ select:focus {
margin-bottom: 12px;
}
.search-form select {
width: auto;
flex: 0 0 auto;
}
.hint {
color: var(--muted);
font-size: 12.5px;