Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion Pages/Index.cshtml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page
@page
@model IndexModel
@inject DiscChangerService dcs
@{
Expand All @@ -19,6 +19,7 @@
<div class="boxed">
<div>
<button id="edit" class="btn btn-settings" onclick="toggle_config()"><i title="Toggle access to settings" class="fas fa-cogs fa-lg"></i></button>
<a href="/Search" class="btn btn-settings" title="Search discs by artist, title, or track"><i class="fas fa-search fa-lg"></i></a>
</div><div>
<input type="range" title="Disc Display Size (@(discDisplaySize)rem)" min="10" max="50" id="disc_display_size" onchange="change_display_size(this)" class="disc-display-size-slider" value="@discDisplaySize" />
</div>
Expand Down Expand Up @@ -186,4 +187,80 @@
$(this).parents(".popover").hide();
});
</script>
@{
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)
{
<style>
@@keyframes discPulse {
0% { box-shadow: 0 0 0 0 rgba(220,53,69,1), inset 0 0 0 4px rgba(220,53,69,0.9); background-color: rgba(220,53,69,0.55); }
50% { box-shadow: 0 0 0 18px rgba(220,53,69,0), inset 0 0 0 4px rgba(220,53,69,0.6); background-color: rgba(220,53,69,0.8); }
100% { box-shadow: 0 0 0 0 rgba(220,53,69,0), inset 0 0 0 4px rgba(220,53,69,0.9); background-color: rgba(220,53,69,0.55); }
}
.disc-highlight {
animation: discPulse 1s ease-in-out 5;
background-color: rgba(220,53,69,0.55) !important;
border: 3px solid rgba(220,53,69,1);
border-radius: 4px;
}
</style>
<script>
$(document).ready(function () {
var key = '@actionKey';
var slot = '@targetSlot';
var doPlay = @(doPlay ? "true" : "false");

// Set slot input field
var inp = document.getElementById('disc_number_' + key);
if (inp) {
inp.value = slot;
var ta = document.getElementById('title_album_number_' + key);
var ct = document.getElementById('chapter_track_number_' + key);
if (ta) ta.value = '';
if (ct) ct.value = '';
}

// Find the disc element
var disc = document.querySelector('[data-changer="' + key + '"][data-slot="' + slot + '"]');

if (disc) {
// Scroll so disc is centered on screen
disc.scrollIntoView({ behavior: 'smooth', block: 'center' });

// Apply pulsing highlight - stays until user interacts
disc.classList.add('disc-highlight');

// Remove highlight on any user scroll or click
function removeHighlight() {
disc.classList.remove('disc-highlight');
window.removeEventListener('scroll', removeHighlight);
window.removeEventListener('click', removeHighlight);
window.removeEventListener('touchstart', removeHighlight);
}
// Give time for scroll animation before listening
setTimeout(function() {
window.addEventListener('scroll', removeHighlight);
window.addEventListener('click', removeHighlight);
window.addEventListener('touchstart', removeHighlight);
}, 1200);
}

// Play: wait for SignalR connection to be ready
if (doPlay) {
connection.start().then(function() {
discDirect(key);
}).catch(function(err) {
// Connection already started - invoke directly
discDirect(key);
});
}
});
</script>
}
}
127 changes: 127 additions & 0 deletions Pages/Search.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@page
@model SearchModel
@{
ViewData["Title"] = "Search - DiscChanger.NET";
}

<nav class="navbar navbar-expand-md sticky-top navbar-light bg-secondary">
<a href="/" class="navbar-brand ms-2"><i class="fas fa-compact-disc fa-lg"></i></a>
<div class="container-fluid">
<form method="get" class="d-flex flex-grow-1 me-3" role="search">
<input class="form-control me-2"
type="search"
name="q"
value="@Model.Query"
placeholder="Search by artist, album title, track, or slot number..."
autofocus
autocomplete="off" />
<button class="btn btn-primary" type="submit">
<i class="fas fa-search"></i> Search
</button>
</form>
<a href="/" class="btn btn-outline-light btn-sm">
<i class="fas fa-th"></i> Back to Discs
</a>
</div>
</nav>

<div class="container-fluid mt-3">

@if (!string.IsNullOrEmpty(Model.Query))
{
<div class="mb-3">
@if (Model.Results.Count == 0)
{
<p class="text-muted">No results found for <strong>@Model.Query</strong></p>
}
else
{
<p class="text-muted">
Found <strong>@Model.Results.Count</strong> result@(Model.Results.Count != 1 ? "s" : "") for <strong>@Model.Query</strong>
</p>
}
</div>

@if (Model.Results.Count > 0)
{
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5 g-3">
@foreach (var result in Model.Results)
{
<div class="col">
<div class="card h-100 shadow-sm">
@* Clicking the art/header navigates to main page and scrolls to this disc *@
<a href="/?highlight=@result.Slot&key=@result.ChangerKey"
class="text-decoration-none text-reset">
@if (!string.IsNullOrEmpty(result.ArtUrl))
{
<img src="@result.ArtUrl"
class="card-img-top"
alt="Cover art for @result.Title"
style="object-fit:cover;height:180px;cursor:pointer;"
onerror="this.style.display='none'" />
}
else
{
<div class="d-flex align-items-center justify-content-center bg-secondary text-white"
style="height:180px;cursor:pointer;">
<i class="fas fa-compact-disc fa-4x opacity-50"></i>
</div>
}
<div class="card-body py-2 px-3">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="badge bg-secondary">Slot @result.Slot</span>
<span class="badge @(result.MatchType == "Track" ? "bg-info text-dark" : result.MatchType == "Slot" ? "bg-warning text-dark" : "bg-success")">
@result.MatchType
</span>
</div>
@if (!string.IsNullOrEmpty(result.Artist))
{
<div class="fw-bold text-truncate" title="@result.Artist">@result.Artist</div>
}
@if (!string.IsNullOrEmpty(result.Title))
{
<div class="text-muted small text-truncate" title="@result.Title">@result.Title</div>
}
@if (!string.IsNullOrEmpty(result.MatchedTrack))
{
<div class="mt-1 small text-info">
<i class="fas fa-music"></i> @result.MatchedTrack
</div>
}
@if (discChangerService.DiscChangers.Count > 1)
{
<div class="text-muted small mt-1">
<i class="fas fa-hdd"></i> @result.ChangerName
</div>
}
</div>
</a>
@* Play button sends play command via main page *@
<div class="card-footer p-1 d-flex gap-1">
<a href="/?highlight=@result.Slot&key=@result.ChangerKey"
class="btn btn-sm btn-outline-secondary flex-fill"
title="Go to disc on main page">
<i class="fas fa-th"></i> View
</a>
<a href="/?play=@result.Slot&key=@result.ChangerKey"
class="btn btn-sm btn-danger flex-fill"
title="Play this disc now">
<i class="fas fa-play"></i> Play
</a>
</div>
</div>
</div>
}
</div>
}
}
else
{
<div class="text-center text-muted mt-5">
<i class="fas fa-search fa-3x mb-3 d-block opacity-50"></i>
<p>Enter an artist name, album title, track title, or slot number above to search your disc library.</p>
</div>
}
</div>

@inject DiscChangerService discChangerService
133 changes: 133 additions & 0 deletions Pages/Search.cshtml.cs
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<SearchModel> _logger;
public DiscChangerService discChangerService;

public string Query { get; set; } = string.Empty;
public List<SearchResult> Results { get; set; } = new();

public SearchModel(DiscChangerService discChangerService, ILogger<SearchModel> 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<KeyValuePair<string, Disc>> 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; }
}
}
}
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
<img src="doc/DiscChanger.NET.png" />

Expand Down
Loading