diff --git a/Pages/Index.cshtml b/Pages/Index.cshtml index 4a7dec7..eb9fd44 100644 --- a/Pages/Index.cshtml +++ b/Pages/Index.cshtml @@ -1,4 +1,4 @@ -@page +@page @model IndexModel @inject DiscChangerService dcs @{ @@ -19,6 +19,7 @@
+
@@ -186,4 +187,80 @@ $(this).parents(".popover").hide(); }); + @{ + var highlightSlot = Request.Query["highlight"].ToString(); + var playSlot = Request.Query["play"].ToString(); + var actionKey = Request.Query["key"].ToString(); + var doPlay = !string.IsNullOrEmpty(playSlot) && !string.IsNullOrEmpty(actionKey); + var doHighlight = !string.IsNullOrEmpty(highlightSlot) && !string.IsNullOrEmpty(actionKey); + var targetSlot = doPlay ? playSlot : (doHighlight ? highlightSlot : ""); + } + @if (doPlay || doHighlight) + { + + + } } diff --git a/Pages/Search.cshtml b/Pages/Search.cshtml new file mode 100644 index 0000000..598f50d --- /dev/null +++ b/Pages/Search.cshtml @@ -0,0 +1,127 @@ +@page +@model SearchModel +@{ + ViewData["Title"] = "Search - DiscChanger.NET"; +} + + + +
+ + @if (!string.IsNullOrEmpty(Model.Query)) + { +
+ @if (Model.Results.Count == 0) + { +

No results found for @Model.Query

+ } + else + { +

+ Found @Model.Results.Count result@(Model.Results.Count != 1 ? "s" : "") for @Model.Query +

+ } +
+ + @if (Model.Results.Count > 0) + { + + } + } + else + { +
+ +

Enter an artist name, album title, track title, or slot number above to search your disc library.

+
+ } +
+ +@inject DiscChangerService discChangerService diff --git a/Pages/Search.cshtml.cs b/Pages/Search.cshtml.cs new file mode 100644 index 0000000..e06c17d --- /dev/null +++ b/Pages/Search.cshtml.cs @@ -0,0 +1,133 @@ +/* Copyright 2020 Hugo Lyppens + + Search.cshtml.cs is part of DiscChanger.NET. + + DiscChanger.NET is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + DiscChanger.NET is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DiscChanger.NET. If not, see . +*/ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using DiscChanger.Models; + +namespace DiscChanger.Pages +{ + public class SearchModel : PageModel + { + private readonly ILogger _logger; + public DiscChangerService discChangerService; + + public string Query { get; set; } = string.Empty; + public List Results { get; set; } = new(); + + public SearchModel(DiscChangerService discChangerService, ILogger logger) + { + this.discChangerService = discChangerService; + _logger = logger; + } + + public void OnGet(string q) + { + if (string.IsNullOrWhiteSpace(q)) + return; + + Query = q.Trim(); + string queryLower = Query.ToLowerInvariant(); + + foreach (var dc in discChangerService.DiscChangers) + { + var discs = dc.Discs; + if (discs == null) continue; + + List> discList; + lock (discs) + { + discList = discs.ToList(); + } + + foreach (var kvp in discList) + { + var disc = kvp.Value; + if (disc == null) continue; + + string slot = disc.Slot ?? string.Empty; + string artist = disc.GetArtist() ?? string.Empty; + string title = disc.getTitle() ?? string.Empty; + var tracks = disc.GetTracks(); + + // Match slot number exactly + bool slotMatch = slot.Equals(Query, StringComparison.OrdinalIgnoreCase); + + // Match artist or title (case-insensitive contains) + bool artistMatch = artist.Contains(queryLower, StringComparison.OrdinalIgnoreCase); + bool titleMatch = title.Contains(queryLower, StringComparison.OrdinalIgnoreCase); + + // Match any track title + string matchedTrack = null; + if (tracks != null) + { + var trackMatch = tracks.FirstOrDefault(t => + t.Title != null && + t.Title.Contains(queryLower, StringComparison.OrdinalIgnoreCase)); + matchedTrack = trackMatch?.Title; + } + + if (slotMatch || artistMatch || titleMatch || matchedTrack != null) + { + string matchType = slotMatch ? "Slot" : + artistMatch ? "Artist" : + titleMatch ? "Title" : "Track"; + + Results.Add(new SearchResult + { + ChangerName = dc.Name, + ChangerKey = dc.Key, + Slot = slot, + Artist = artist, + Title = title, + ArtUrl = disc.DataGD3Match?.GetArtFileURL() ?? disc.DataMusicBrainz?.GetArtFileURL(), + MatchType = matchType, + MatchedTrack = matchedTrack + }); + } + } + } + + // Sort: slot matches first, then artist, then title, then track + Results.Sort((a, b) => + { + int order(string t) => t switch { "Slot" => 0, "Artist" => 1, "Title" => 2, _ => 3 }; + int cmp = order(a.MatchType).CompareTo(order(b.MatchType)); + if (cmp != 0) return cmp; + // Within same match type, sort by slot number + if (int.TryParse(a.Slot, out int sa) && int.TryParse(b.Slot, out int sb)) + return sa.CompareTo(sb); + return string.Compare(a.Slot, b.Slot, StringComparison.OrdinalIgnoreCase); + }); + } + + public class SearchResult + { + public string ChangerName { get; set; } + public string ChangerKey { get; set; } + public string Slot { get; set; } + public string Artist { get; set; } + public string Title { get; set; } + public string ArtUrl { get; set; } + public string MatchType { get; set; } + public string MatchedTrack { get; set; } + } + } +} diff --git a/README.md b/README.md index 6425ca3..19e72d4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ # DiscChanger.NET + +EDIT IN THIS BRANCH: +## Disc Search + +Adds a search feature so discs can be found by artist, album title, track title, or slot number without scrolling the full 400-disc grid. + +**What's new:** +- New `/Search` page — single search box covering artist, album, track, and slot +- Search icon added to the main navbar +- Clicking a result jumps back to the main grid, centers and highlights the disc (pulsing red border, 5s), and optionally auto-plays it + +**Files changed:** +- `Pages/Search.cshtml`, `Pages/Search.cshtml.cs` — new search page +- `Pages/Index.cshtml` — navbar icon + highlight/play handling for `?highlight=` / `?play=` query params + +No changes to controllers, the SignalR hub, or existing JS files. + +Full usage guide (including how to verify the changer connection via the settings screen): [doc/Search-Feature-Guide.md](doc/Search-Feature-Guide.md) +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + ASP.NET Core Solution to manage discs in disc changers diff --git a/doc/Search-Feature-Guide.md b/doc/Search-Feature-Guide.md new file mode 100644 index 0000000..a5a24b5 --- /dev/null +++ b/doc/Search-Feature-Guide.md @@ -0,0 +1,73 @@ +# DiscChanger.NET — Search Feature Guide + +## 1. What Changed + +Three files were added or modified to bring search to DiscChanger.NET: + +| File | Change | +|---|---| +| `Pages/Search.cshtml` + `Pages/Search.cshtml.cs` | **New** — a standalone search page that reads your existing disc library (`DiscChangers.json` and the `Discs/` metadata) and returns matches. | +| `Pages/Index.cshtml` | **Modified** — added a search icon to the navbar, plus logic to receive a disc selection from the Search page, scroll to it, highlight it, and optionally play it. | + +No existing controllers, the SignalR hub, or JavaScript files were touched. The feature is fully self-contained on top of the existing app. + +**Search covers four fields at once**, in one search box: +- Artist +- Album title +- Track title +- Slot number + +## 2. Where It Shows Up in the UI + +- **Navbar** — a magnifying-glass icon now appears next to the settings gear on the main page. +- **`/Search` page** — a new page with a single search box. Results appear as cards: cover art thumbnail, artist, album title, slot number, and (if you searched a track) the matching track name. +- **Main disc grid** — clicking a search result sends you back to the main page, which: + - Smooth-scrolls the matching disc to the **center** of the screen + - Wraps it in a **pulsing red box** with a red tint — 5 pulses over 5 seconds, then it stays highlighted (border + tint) until you scroll, click, or tap elsewhere on the page + - If you clicked **Play** instead of just viewing, it also sends the play command to the changer once the live connection is ready + +## 3. How to Use Search + +1. Click the **magnifying glass icon** in the navbar (next to the gear icon). +2. Type a search term — an artist name, album title, track name, or a slot number. +3. Review the results — each card shows the disc's art, artist/title, slot, and matching track if relevant. +4. Click a result: + - Clicking the **cover art or disc info** → jumps to the main grid and highlights that disc (view only) + - Clicking **Play** → jumps to the main grid, highlights the disc, and starts playback automatically +5. On the main grid, the highlighted disc is impossible to miss — a solid red border and pulsing red background. It stays highlighted until you interact with the page again. + +## 4. Using the Settings Screen to Confirm the Changer Connection + +Before search (or anything else) can control real hardware, the changer itself needs to be configured and connected. This is done from the **settings gear icon** on the main page — independent of the search feature, but essential for the "Play" action to actually do anything. + +1. Click the **gear icon** in the navbar to open the settings/config panel. +2. If no changer is listed yet, click **Add** to create one. If it already exists, select it to edit. +3. Fill in: + - **Port** — the serial device path. On Linux this is typically `/dev/ttyUSB0` (or `/dev/ttyACM0` for some adapters); on Windows it's a `COM` port like `COM3`. + - **Changer type** — select the matching Sony model (e.g. DVP-CX777ES for RS-232-only changers). +4. Click **Test** — this attempts to open the serial port and query the changer directly. + - **Success** means the app can talk to the hardware — you're good to save. + - **Failure** usually means one of: + - Wrong port path (double-check `/dev/ttyUSB0` exists — see below) + - The changer isn't powered on or the cable isn't connected + - A Linux permissions issue (see note below) +5. Once Test succeeds, click **OK / Save** to commit the configuration. +6. Back on the main page, the **power button** should now reflect the real state of the changer (lit/on if the changer is powered on). This matters beyond just cosmetics — some UI actions (like populating the "Go" fields when clicking a track in the popover) are gated on this power state being accurately read from the hardware. + +### Linux-specific note: serial port permissions + +On Linux, USB-serial adapters are owned by the `dialout` group by default. If Test keeps failing even though the adapter shows up in `lsusb`, check: +```bash +groups $USER +``` +If `dialout` isn't listed, add yourself to it and log out/back in (group changes require a fresh session): +```bash +sudo usermod -aG dialout $USER +``` +Then retry Test in the settings screen. + +## 5. Summary + +- Search is a fast way to find any disc by artist, album, track, or slot without scrolling the full grid. +- Results connect back to the live grid with a clear visual highlight and optional one-click play. +- The settings screen is where the actual hardware link is configured and verified — search and play only work end-to-end once that connection Tests successfully.