feat: multi-tag badges for showAll — amber for unmatched, accent for matched
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s
- server: add extractAllTags() returning all tag labels for a series/movie - server: showAll now includes items with ANY tag (not just user-matched); non-admin path unchanged (must match current user's tag) - server: replace userTag with allTags[] + matchedUserTag on every download object - frontend: render all tags in header; unmatched tags amber (left), matched user tag in accent colour (rightmost); only visible in showAll mode - css: add --unmatched-tag-bg/color variables to all three themes (light, dark, mono) and .download-user-badge.unmatched style
This commit is contained in:
@@ -434,11 +434,20 @@ function createDownloadCard(download) {
|
|||||||
infoDiv.appendChild(movie);
|
infoDiv.appendChild(movie);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAll && download.userTag) {
|
if (showAll && download.allTags && download.allTags.length > 0) {
|
||||||
const userBadge = document.createElement('span');
|
const unmatchedTags = download.allTags.filter(t => t !== download.matchedUserTag);
|
||||||
userBadge.className = 'download-user-badge';
|
for (const tag of unmatchedTags) {
|
||||||
userBadge.textContent = download.userTag;
|
const badge = document.createElement('span');
|
||||||
header.appendChild(userBadge);
|
badge.className = 'download-user-badge unmatched';
|
||||||
|
badge.textContent = tag;
|
||||||
|
header.appendChild(badge);
|
||||||
|
}
|
||||||
|
if (download.matchedUserTag) {
|
||||||
|
const matchedBadge = document.createElement('span');
|
||||||
|
matchedBadge.className = 'download-user-badge';
|
||||||
|
matchedBadge.textContent = download.matchedUserTag;
|
||||||
|
header.appendChild(matchedBadge);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const details = document.createElement('div');
|
const details = document.createElement('div');
|
||||||
|
|||||||
@@ -64,6 +64,8 @@
|
|||||||
--footer-text: rgba(255, 255, 255, 0.9);
|
--footer-text: rgba(255, 255, 255, 0.9);
|
||||||
--input-bg: #ffffff;
|
--input-bg: #ffffff;
|
||||||
--select-bg: #ffffff;
|
--select-bg: #ffffff;
|
||||||
|
--unmatched-tag-bg: #fff3e0;
|
||||||
|
--unmatched-tag-color: #e65100;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] {
|
[data-theme="dark"] {
|
||||||
@@ -100,6 +102,8 @@
|
|||||||
--footer-text: rgba(200, 200, 220, 0.8);
|
--footer-text: rgba(200, 200, 220, 0.8);
|
||||||
--input-bg: #2a2a3d;
|
--input-bg: #2a2a3d;
|
||||||
--select-bg: #2a2a3d;
|
--select-bg: #2a2a3d;
|
||||||
|
--unmatched-tag-bg: #3d2a00;
|
||||||
|
--unmatched-tag-color: #ffb74d;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="mono"] {
|
[data-theme="mono"] {
|
||||||
@@ -136,6 +140,8 @@
|
|||||||
--footer-text: rgba(180, 180, 180, 0.7);
|
--footer-text: rgba(180, 180, 180, 0.7);
|
||||||
--input-bg: #252525;
|
--input-bg: #252525;
|
||||||
--select-bg: #252525;
|
--select-bg: #252525;
|
||||||
|
--unmatched-tag-bg: #2a2a2a;
|
||||||
|
--unmatched-tag-color: #a0a0a0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Base ===== */
|
/* ===== Base ===== */
|
||||||
@@ -734,6 +740,12 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-user-badge.unmatched {
|
||||||
|
background: var(--unmatched-tag-bg);
|
||||||
|
color: var(--unmatched-tag-color);
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===== Status Button ===== */
|
/* ===== Status Button ===== */
|
||||||
.status-btn {
|
.status-btn {
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ function extractUserTag(tags, tagMap) {
|
|||||||
return userTag ? userTag.label : null;
|
return userTag ? userTag.label : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return all resolved tag labels for a series/movie
|
||||||
|
function extractAllTags(tags, tagMap) {
|
||||||
|
if (!tags || tags.length === 0) return [];
|
||||||
|
if (tagMap) {
|
||||||
|
return tags.map(id => tagMap.get(id)).filter(Boolean);
|
||||||
|
}
|
||||||
|
return tags.map(t => t && t.label).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
// Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim
|
// Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim
|
||||||
function sanitizeTagLabel(input) {
|
function sanitizeTagLabel(input) {
|
||||||
if (!input) return '';
|
if (!input) return '';
|
||||||
@@ -224,8 +233,10 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (sonarrMatch && sonarrMatch.seriesId) {
|
if (sonarrMatch && sonarrMatch.seriesId) {
|
||||||
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
||||||
if (series) {
|
if (series) {
|
||||||
const userTag = extractUserTag(series.tags, sonarrTagMap);
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
const dlObj = {
|
const dlObj = {
|
||||||
type: 'series',
|
type: 'series',
|
||||||
title: nzbName,
|
title: nzbName,
|
||||||
@@ -239,7 +250,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
eta: slot.timeleft,
|
eta: slot.timeleft,
|
||||||
seriesName: series.title,
|
seriesName: series.title,
|
||||||
episodeInfo: sonarrMatch,
|
episodeInfo: sonarrMatch,
|
||||||
userTag: userTag
|
allTags,
|
||||||
|
matchedUserTag: matchedUserTag || null
|
||||||
};
|
};
|
||||||
const issues = getImportIssues(sonarrMatch);
|
const issues = getImportIssues(sonarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
if (issues) dlObj.importIssues = issues;
|
||||||
@@ -262,8 +274,10 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (radarrMatch && radarrMatch.movieId) {
|
if (radarrMatch && radarrMatch.movieId) {
|
||||||
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
||||||
if (movie) {
|
if (movie) {
|
||||||
const userTag = extractUserTag(movie.tags, radarrTagMap);
|
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(movie.tags, radarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
const dlObj = {
|
const dlObj = {
|
||||||
type: 'movie',
|
type: 'movie',
|
||||||
title: nzbName,
|
title: nzbName,
|
||||||
@@ -277,7 +291,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
eta: slot.timeleft,
|
eta: slot.timeleft,
|
||||||
movieName: movie.title,
|
movieName: movie.title,
|
||||||
movieInfo: radarrMatch,
|
movieInfo: radarrMatch,
|
||||||
userTag: userTag
|
allTags,
|
||||||
|
matchedUserTag: matchedUserTag || null
|
||||||
};
|
};
|
||||||
const issues = getImportIssues(radarrMatch);
|
const issues = getImportIssues(radarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
if (issues) dlObj.importIssues = issues;
|
||||||
@@ -317,8 +332,10 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (sonarrMatch && sonarrMatch.seriesId) {
|
if (sonarrMatch && sonarrMatch.seriesId) {
|
||||||
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
||||||
if (series) {
|
if (series) {
|
||||||
const userTag = extractUserTag(series.tags, sonarrTagMap);
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
const dlObj = {
|
const dlObj = {
|
||||||
type: 'series',
|
type: 'series',
|
||||||
title: nzbName,
|
title: nzbName,
|
||||||
@@ -328,7 +345,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
completedAt: slot.completed_time,
|
completedAt: slot.completed_time,
|
||||||
seriesName: series.title,
|
seriesName: series.title,
|
||||||
episodeInfo: sonarrMatch,
|
episodeInfo: sonarrMatch,
|
||||||
userTag: userTag
|
allTags,
|
||||||
|
matchedUserTag: matchedUserTag || null
|
||||||
};
|
};
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
@@ -349,8 +367,10 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (radarrMatch && radarrMatch.movieId) {
|
if (radarrMatch && radarrMatch.movieId) {
|
||||||
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
||||||
if (movie) {
|
if (movie) {
|
||||||
const userTag = extractUserTag(movie.tags, radarrTagMap);
|
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(movie.tags, radarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
const dlObj = {
|
const dlObj = {
|
||||||
type: 'movie',
|
type: 'movie',
|
||||||
title: nzbName,
|
title: nzbName,
|
||||||
@@ -360,7 +380,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
completedAt: slot.completed_time,
|
completedAt: slot.completed_time,
|
||||||
movieName: movie.title,
|
movieName: movie.title,
|
||||||
movieInfo: radarrMatch,
|
movieInfo: radarrMatch,
|
||||||
userTag: userTag
|
allTags,
|
||||||
|
matchedUserTag: matchedUserTag || null
|
||||||
};
|
};
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
@@ -417,15 +438,18 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (sonarrMatch && sonarrMatch.seriesId) {
|
if (sonarrMatch && sonarrMatch.seriesId) {
|
||||||
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
|
||||||
if (series) {
|
if (series) {
|
||||||
const userTag = extractUserTag(series.tags, sonarrTagMap);
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`);
|
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`);
|
||||||
const download = mapTorrentToDownload(torrent);
|
const download = mapTorrentToDownload(torrent);
|
||||||
download.type = 'series';
|
download.type = 'series';
|
||||||
download.coverArt = getCoverArt(series);
|
download.coverArt = getCoverArt(series);
|
||||||
download.seriesName = series.title;
|
download.seriesName = series.title;
|
||||||
download.episodeInfo = sonarrMatch;
|
download.episodeInfo = sonarrMatch;
|
||||||
download.userTag = userTag;
|
download.allTags = allTags;
|
||||||
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
const sonarrIssues = getImportIssues(sonarrMatch);
|
const sonarrIssues = getImportIssues(sonarrMatch);
|
||||||
if (sonarrIssues) download.importIssues = sonarrIssues;
|
if (sonarrIssues) download.importIssues = sonarrIssues;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -448,15 +472,18 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (radarrMatch && radarrMatch.movieId) {
|
if (radarrMatch && radarrMatch.movieId) {
|
||||||
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
|
||||||
if (movie) {
|
if (movie) {
|
||||||
const userTag = extractUserTag(movie.tags, radarrTagMap);
|
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(movie.tags, radarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`);
|
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`);
|
||||||
const download = mapTorrentToDownload(torrent);
|
const download = mapTorrentToDownload(torrent);
|
||||||
download.type = 'movie';
|
download.type = 'movie';
|
||||||
download.coverArt = getCoverArt(movie);
|
download.coverArt = getCoverArt(movie);
|
||||||
download.movieName = movie.title;
|
download.movieName = movie.title;
|
||||||
download.movieInfo = radarrMatch;
|
download.movieInfo = radarrMatch;
|
||||||
download.userTag = userTag;
|
download.allTags = allTags;
|
||||||
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
const radarrIssues = getImportIssues(radarrMatch);
|
const radarrIssues = getImportIssues(radarrMatch);
|
||||||
if (radarrIssues) download.importIssues = radarrIssues;
|
if (radarrIssues) download.importIssues = radarrIssues;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -479,15 +506,18 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) {
|
if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) {
|
||||||
const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series;
|
const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series;
|
||||||
if (series) {
|
if (series) {
|
||||||
const userTag = extractUserTag(series.tags, sonarrTagMap);
|
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(series.tags, sonarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`);
|
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`);
|
||||||
const download = mapTorrentToDownload(torrent);
|
const download = mapTorrentToDownload(torrent);
|
||||||
download.type = 'series';
|
download.type = 'series';
|
||||||
download.coverArt = getCoverArt(series);
|
download.coverArt = getCoverArt(series);
|
||||||
download.seriesName = series.title;
|
download.seriesName = series.title;
|
||||||
download.episodeInfo = sonarrHistoryMatch;
|
download.episodeInfo = sonarrHistoryMatch;
|
||||||
download.userTag = userTag;
|
download.allTags = allTags;
|
||||||
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = series.path || null;
|
download.targetPath = series.path || null;
|
||||||
@@ -508,15 +538,18 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
if (radarrHistoryMatch && radarrHistoryMatch.movieId) {
|
if (radarrHistoryMatch && radarrHistoryMatch.movieId) {
|
||||||
const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie;
|
const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie;
|
||||||
if (movie) {
|
if (movie) {
|
||||||
const userTag = extractUserTag(movie.tags, radarrTagMap);
|
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
|
||||||
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
|
const allTags = extractAllTags(movie.tags, radarrTagMap);
|
||||||
|
const hasAnyTag = allTags.length > 0;
|
||||||
|
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
|
||||||
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`);
|
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`);
|
||||||
const download = mapTorrentToDownload(torrent);
|
const download = mapTorrentToDownload(torrent);
|
||||||
download.type = 'movie';
|
download.type = 'movie';
|
||||||
download.coverArt = getCoverArt(movie);
|
download.coverArt = getCoverArt(movie);
|
||||||
download.movieName = movie.title;
|
download.movieName = movie.title;
|
||||||
download.movieInfo = radarrHistoryMatch;
|
download.movieInfo = radarrHistoryMatch;
|
||||||
download.userTag = userTag;
|
download.allTags = allTags;
|
||||||
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = movie.path || null;
|
download.targetPath = movie.path || null;
|
||||||
|
|||||||
Reference in New Issue
Block a user