feat(web): score heat scale, drop release row expand
Score cells interpolate red-to-green relative to the scores in the current deck; the click-to-expand panel only repeated row content.
This commit is contained in:
+40
-45
@@ -18,7 +18,6 @@ import {
|
||||
bucketOf,
|
||||
formatAudio,
|
||||
formatHdr,
|
||||
formatPublishDate,
|
||||
formatResolution,
|
||||
formatScore,
|
||||
formatSeeders,
|
||||
@@ -879,6 +878,28 @@ function releasesMain(): ReleasesView {
|
||||
}
|
||||
setStatus(null);
|
||||
|
||||
// score colour is relative to this list (issue 111): highest green,
|
||||
// lowest red, seven interpolated stops between
|
||||
let min = Number.POSITIVE_INFINITY;
|
||||
let max = Number.NEGATIVE_INFINITY;
|
||||
for (const release of releases) {
|
||||
if (release.score !== null) {
|
||||
if (release.score < min) {
|
||||
min = release.score;
|
||||
}
|
||||
if (release.score > max) {
|
||||
max = release.score;
|
||||
}
|
||||
}
|
||||
}
|
||||
const scoreStop = (score: number | null): number | null => {
|
||||
if (score === null || min > max) {
|
||||
return null;
|
||||
}
|
||||
// all-equal scores are jointly the best the list offers
|
||||
return max === min ? SCORE_STOPS : Math.round(((score - min) / (max - min)) * SCORE_STOPS);
|
||||
};
|
||||
|
||||
// the API pre-sorts by bucket then score; splitting preserves order
|
||||
const buckets = {
|
||||
eligible: releases.filter((release) => bucketOf(release) === "eligible"),
|
||||
@@ -890,7 +911,7 @@ function releasesMain(): ReleasesView {
|
||||
eligible.count.textContent = String(buckets.eligible.length);
|
||||
if (buckets.eligible.length > 0) {
|
||||
for (const release of buckets.eligible) {
|
||||
eligible.rows.append(releaseRow(release, movie, "eligible", actions));
|
||||
eligible.rows.append(releaseRow(release, movie, "eligible", scoreStop, actions));
|
||||
}
|
||||
} else {
|
||||
// §9.3: over-strict filters must be visible, not silently absent
|
||||
@@ -907,7 +928,7 @@ function releasesMain(): ReleasesView {
|
||||
const bucket = collapsed[name];
|
||||
bucket.section.hidden = false;
|
||||
for (const release of rows) {
|
||||
bucket.rows.append(releaseRow(release, movie, name, actions));
|
||||
bucket.rows.append(releaseRow(release, movie, name, scoreStop, actions));
|
||||
}
|
||||
setToggle(bucket, rows.length);
|
||||
}
|
||||
@@ -1240,21 +1261,30 @@ interface ReleaseActions {
|
||||
notify: (text: string, tone?: "fault") => void;
|
||||
}
|
||||
|
||||
/** One classified release: chips lead, the name is secondary (§9.3). */
|
||||
/** Stops in the score heat scale — keep in step with the --score-* tokens. */
|
||||
const SCORE_STOPS = 6;
|
||||
|
||||
type ScoreStopper = (score: number | null) => number | null;
|
||||
|
||||
/**
|
||||
* One classified release: chips lead, the name is secondary (§9.3). The
|
||||
* score chip is coloured on the list-relative heat scale (issue 111);
|
||||
* nothing on the line is clickable — grab is its own control.
|
||||
*/
|
||||
function releaseRow(
|
||||
release: MovieRelease,
|
||||
movie: LibraryMovie,
|
||||
bucket: "eligible" | "waived" | "rejected",
|
||||
scoreStop: ScoreStopper,
|
||||
actions: ReleaseActions,
|
||||
): HTMLLIElement {
|
||||
const item = document.createElement("li");
|
||||
item.className = "rel";
|
||||
|
||||
const line = document.createElement("button");
|
||||
line.type = "button";
|
||||
const line = document.createElement("div");
|
||||
line.className = "rel-line";
|
||||
line.setAttribute("aria-expanded", "false");
|
||||
|
||||
const stop = scoreStop(release.score);
|
||||
const columns: [string, string, string][] = [
|
||||
["cw-score", "score", formatScore(release.score)],
|
||||
["cw-res", "resolution", formatResolution(release.parsed)],
|
||||
@@ -1272,9 +1302,9 @@ function releaseRow(
|
||||
if (value === "—") {
|
||||
span.classList.add("dim");
|
||||
}
|
||||
// the world's non-colour eligible mark rides the leading chip
|
||||
if (bucket === "eligible" && width === "cw-score") {
|
||||
span.dataset.verdict = "eligible";
|
||||
// the heat scale replaces the flat eligible green on this cell
|
||||
if (width === "cw-score" && stop !== null) {
|
||||
span.dataset.score = String(stop);
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -1342,41 +1372,6 @@ function releaseRow(
|
||||
}
|
||||
item.append(note);
|
||||
|
||||
const full = document.createElement("div");
|
||||
full.className = "rel-full";
|
||||
full.hidden = true;
|
||||
line.addEventListener("click", () => {
|
||||
if (full.childElementCount === 0) {
|
||||
const raw = document.createElement("p");
|
||||
raw.className = "rel-raw readout";
|
||||
raw.textContent = release.name;
|
||||
full.append(raw);
|
||||
const meta = document.createElement("p");
|
||||
meta.className = "rel-meta readout dim";
|
||||
const published = formatPublishDate(release.publish_date);
|
||||
// hdr and audio leave the chip line under 40rem; the expand carries them
|
||||
meta.textContent = [
|
||||
`indexer ${release.indexer_id}`,
|
||||
published === null ? null : `published ${published}`,
|
||||
`hdr ${formatHdr(release.parsed)}`,
|
||||
`audio ${formatAudio(release.parsed)}`,
|
||||
`${release.size} bytes`,
|
||||
]
|
||||
.filter((part) => part !== null)
|
||||
.join(" · ");
|
||||
full.append(meta);
|
||||
if (bucket === "waived" && waiverOverride(release.rejected_rule) !== null) {
|
||||
const waiver = document.createElement("p");
|
||||
waiver.className = "rel-meta readout dim";
|
||||
waiver.textContent = "waive + grab writes this title's override, then grabs";
|
||||
full.append(waiver);
|
||||
}
|
||||
}
|
||||
full.hidden = !full.hidden;
|
||||
line.setAttribute("aria-expanded", String(!full.hidden));
|
||||
});
|
||||
item.append(full);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user