feat: classify gear and weapon mods
This commit is contained in:
@@ -14,6 +14,39 @@ type MissingReason = typing.Literal[
|
||||
"not_applicable",
|
||||
"redacted",
|
||||
]
|
||||
type ScreenshotFamily = typing.Literal[
|
||||
"weapon",
|
||||
"gear",
|
||||
"skill_chip",
|
||||
"os_protocol",
|
||||
"specialization",
|
||||
"weapon_mod",
|
||||
"gear_mod",
|
||||
"unknown",
|
||||
]
|
||||
type CanonicalRecordType = typing.Literal[
|
||||
"weapon",
|
||||
"weapon_talent",
|
||||
"weapon_augmentation_profile",
|
||||
"gear",
|
||||
"brand_set",
|
||||
"gear_talent",
|
||||
"gear_augmentation_profile",
|
||||
"skill_chip_set",
|
||||
"skill_chip_set_bonus",
|
||||
"skill_chip_roll_pool",
|
||||
"skill_chip_talent",
|
||||
"os_protocol",
|
||||
"os_protocol_secondary_profile",
|
||||
"os_protocol_talent",
|
||||
"specialization",
|
||||
"specialization_focus",
|
||||
"specialization_ability",
|
||||
"specialization_tree_node",
|
||||
"specialization_tree_rank",
|
||||
"weapon_mod",
|
||||
"gear_mod",
|
||||
]
|
||||
|
||||
|
||||
class SourceEngine(typing.TypedDict):
|
||||
@@ -76,14 +109,12 @@ class FieldClaim(typing.TypedDict):
|
||||
|
||||
|
||||
class ScreenshotClassification(typing.TypedDict):
|
||||
family: typing.Literal[
|
||||
"weapon", "gear", "skill_chip", "os_protocol", "specialization", "unknown"
|
||||
]
|
||||
family: ScreenshotFamily
|
||||
view: str
|
||||
gear_slot: (
|
||||
typing.Literal["Mask", "Gloves", "Backpack", "Body Armor", "Holster", "Knee Pads"] | None
|
||||
)
|
||||
record_types: list[str]
|
||||
record_types: list[CanonicalRecordType]
|
||||
confidence: float
|
||||
abstention_reason: str | None
|
||||
|
||||
@@ -99,7 +130,7 @@ class ObservedItemInstance(typing.TypedDict):
|
||||
"""Values shown for one configured item; they are not reusable definitions."""
|
||||
|
||||
instance_id: str
|
||||
family: str
|
||||
family: ScreenshotFamily
|
||||
source_screen_ids: list[str]
|
||||
observed_context: ObservedContext
|
||||
fields: list[FieldClaim]
|
||||
@@ -109,7 +140,7 @@ class CanonicalDefinitionCandidate(typing.TypedDict):
|
||||
"""Evidence-backed reusable claims, kept separate from the observed item."""
|
||||
|
||||
candidate_id: str
|
||||
record_type: str
|
||||
record_type: CanonicalRecordType
|
||||
source_instance_ids: list[str]
|
||||
evidence_refs: list[str]
|
||||
fields: list[FieldClaim]
|
||||
|
||||
@@ -12,6 +12,8 @@ class Family(StrEnum):
|
||||
SKILL_CHIP = "skill_chip"
|
||||
OS_PROTOCOL = "os_protocol"
|
||||
SPECIALIZATION = "specialization"
|
||||
WEAPON_MOD = "weapon_mod"
|
||||
GEAR_MOD = "gear_mod"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@@ -57,6 +59,8 @@ class RecordType(StrEnum):
|
||||
SPECIALIZATION_ABILITY = "specialization_ability"
|
||||
SPECIALIZATION_TREE_NODE = "specialization_tree_node"
|
||||
SPECIALIZATION_TREE_RANK = "specialization_tree_rank"
|
||||
WEAPON_MOD = "weapon_mod"
|
||||
GEAR_MOD = "gear_mod"
|
||||
|
||||
|
||||
SCREEN_VIEWS: Final[Mapping[Family, tuple[str, ...]]] = MappingProxyType(
|
||||
@@ -80,6 +84,8 @@ SCREEN_VIEWS: Final[Mapping[Family, tuple[str, ...]]] = MappingProxyType(
|
||||
"focus_tree",
|
||||
"rank_details",
|
||||
),
|
||||
Family.WEAPON_MOD: ("detail",),
|
||||
Family.GEAR_MOD: ("detail",),
|
||||
Family.UNKNOWN: ("unknown",),
|
||||
}
|
||||
)
|
||||
@@ -117,6 +123,8 @@ RECORD_TYPES_BY_VIEW: Final[Mapping[tuple[Family, str], tuple[RecordType, ...]]]
|
||||
RecordType.SPECIALIZATION_TREE_NODE,
|
||||
),
|
||||
(Family.SPECIALIZATION, "rank_details"): (RecordType.SPECIALIZATION_TREE_RANK,),
|
||||
(Family.WEAPON_MOD, "detail"): (RecordType.WEAPON_MOD,),
|
||||
(Family.GEAR_MOD, "detail"): (RecordType.GEAR_MOD,),
|
||||
(Family.UNKNOWN, "unknown"): (),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -86,7 +86,13 @@ def validate_classification(classification: Mapping[str, object]) -> None:
|
||||
raise ContractValidationError("record_types must contain strings")
|
||||
if len(record_types) != len(set(record_types)):
|
||||
raise ContractValidationError("record_types must be unique")
|
||||
allowed = {record_type.value for record_type in taxonomy.RECORD_TYPES_BY_VIEW[(family, view)]}
|
||||
if len(record_types) > 8:
|
||||
raise ContractValidationError("record_types cannot contain more than 8 values")
|
||||
try:
|
||||
normalized_record_types = {taxonomy.RecordType(record_type) for record_type in record_types}
|
||||
except ValueError as error:
|
||||
raise ContractValidationError("Unknown canonical record type") from error
|
||||
allowed = set(taxonomy.RECORD_TYPES_BY_VIEW[(family, view)])
|
||||
if family is taxonomy.Family.UNKNOWN:
|
||||
if record_types:
|
||||
raise ContractValidationError("Unknown screens cannot claim canonical record types")
|
||||
@@ -94,7 +100,7 @@ def validate_classification(classification: Mapping[str, object]) -> None:
|
||||
if reason not in taxonomy.ABSTENTION_REASONS:
|
||||
raise ContractValidationError("Unknown screens require an abstention reason")
|
||||
else:
|
||||
if not record_types or not set(record_types).issubset(allowed):
|
||||
if not record_types or not normalized_record_types.issubset(allowed):
|
||||
raise ContractValidationError(
|
||||
f"record_types are incompatible with {family.value}/{view}"
|
||||
)
|
||||
|
||||
@@ -40,6 +40,8 @@ from game_data_capture.web.security import new_bootstrap_token
|
||||
_POLICY_VERSION = "capture-v1"
|
||||
_PROCESSING_VERSION = "capture-v1"
|
||||
_POLL_SECONDS = 0.1
|
||||
# This is a bounded latest-history projection, not a corpus total or pagination.
|
||||
_MAX_CAPTURE_PROJECTIONS = 256
|
||||
|
||||
|
||||
class RuntimeFailure(RuntimeError):
|
||||
@@ -462,7 +464,7 @@ class CaptureRuntime:
|
||||
app = create_app(
|
||||
config=WebConfig(
|
||||
server=self.config.server,
|
||||
max_capture_items=min(64, self.config.limits.max_queue_items),
|
||||
max_capture_items=_MAX_CAPTURE_PROJECTIONS,
|
||||
max_image_bytes=self.config.limits.max_image_bytes,
|
||||
),
|
||||
capture_control=control,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body { margin: 0; min-width: 20rem; background: var(--canvas); color: var(--text); }
|
||||
body { margin: 0; background: var(--canvas); color: var(--text); }
|
||||
|
||||
button, input { font: inherit; }
|
||||
|
||||
@@ -67,15 +67,20 @@ h2 { margin-bottom: 0; font-size: 1.15rem; }
|
||||
.control-actions, .form-actions, .dialog-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
|
||||
|
||||
.workspace-grid { display: grid; grid-template-columns: minmax(17rem, 24rem) minmax(0, 1fr); border: 1px solid var(--line); background: var(--surface); }
|
||||
.capture-panel { border-right: 1px solid var(--line); min-height: 42rem; }
|
||||
.capture-panel { border-right: 1px solid var(--line); display: flex; flex-direction: column; height: min(42rem, calc(100vh - 10rem)); min-height: 24rem; min-width: 0; }
|
||||
.capture-panel, .review-panel { padding: 1rem; }
|
||||
.panel-heading { border-bottom: 1px solid var(--line); padding-bottom: .85rem; }
|
||||
.queue-filter { display: flex; gap: .55rem; align-items: center; min-height: 44px; color: var(--muted); font-size: .88rem; }
|
||||
.queue-filter input { inline-size: 1.1rem; block-size: 1.1rem; flex: 0 0 auto; }
|
||||
.capture-scroll { min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; padding-right: .25rem; scrollbar-gutter: stable; }
|
||||
.empty-copy, .review-empty { color: var(--muted); padding: 1rem 0; line-height: 1.5; }
|
||||
.capture-list { margin: 0; padding: 0; list-style: none; }
|
||||
.capture-list button { width: 100%; display: grid; grid-template-columns: 1fr auto; gap: .25rem .6rem; margin-top: .55rem; text-align: left; background: var(--well); }
|
||||
.capture-list button { width: 100%; min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .25rem .6rem; margin-top: .55rem; text-align: left; background: var(--well); }
|
||||
.capture-list button[aria-current="true"] { border-color: var(--orange); color: var(--text); }
|
||||
.capture-list .capture-id { overflow-wrap: anywhere; }
|
||||
.capture-list .capture-meta { color: var(--muted); font-size: .8rem; }
|
||||
.capture-list .capture-id { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.capture-list .capture-meta { display: grid; gap: .15rem; color: var(--muted); font-size: .72rem; line-height: 1.2; text-align: right; white-space: nowrap; }
|
||||
.capture-list .capture-review { color: var(--text); font-size: .75rem; font-weight: 800; letter-spacing: .04em; }
|
||||
.capture-list .capture-time { overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.review-content { padding-top: 1rem; }
|
||||
.evidence-frame { margin: 0 0 1rem; border: 1px solid var(--line); background: var(--well); padding: .65rem; }
|
||||
@@ -103,7 +108,8 @@ dialog p { line-height: 1.5; }
|
||||
.command-header, .control-strip { align-items: flex-start; flex-direction: column; }
|
||||
.header-state { text-align: left; }
|
||||
.workspace-grid { grid-template-columns: 1fr; }
|
||||
.capture-panel { min-height: auto; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.capture-panel { height: auto; min-height: 0; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.capture-scroll { overflow: visible; padding-right: 0; scrollbar-gutter: auto; }
|
||||
.capture-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: .55rem; }
|
||||
.capture-list button { margin-top: 0; }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ const taxonomy = Object.freeze({
|
||||
skill_chip: { views: ["detail", "set_bonus", "talent"], records: { detail: ["skill_chip_set", "skill_chip_roll_pool"], set_bonus: ["skill_chip_set_bonus"], talent: ["skill_chip_talent"] } },
|
||||
os_protocol: { views: ["detail", "secondary_profile", "talent"], records: { detail: ["os_protocol"], secondary_profile: ["os_protocol_secondary_profile"], talent: ["os_protocol_talent"] } },
|
||||
specialization: { views: ["overview", "details", "ability", "focus_details", "focus_tree", "rank_details"], records: { overview: ["specialization"], details: ["specialization"], ability: ["specialization_ability"], focus_details: ["specialization_focus"], focus_tree: ["specialization_focus", "specialization_tree_node"], rank_details: ["specialization_tree_rank"] } },
|
||||
weapon_mod: { views: ["detail"], records: { detail: ["weapon_mod"] } },
|
||||
gear_mod: { views: ["detail"], records: { detail: ["gear_mod"] } },
|
||||
unknown: { views: ["unknown"], records: { unknown: [] } }
|
||||
});
|
||||
const families = Object.keys(taxonomy);
|
||||
@@ -13,7 +15,7 @@ const csrfPattern = /^[A-Za-z0-9_-]{32,128}$/;
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const els = Object.freeze({
|
||||
sessionState: $("session-state"), queueState: $("queue-state"), captureState: $("capture-state"), notice: $("notice"), noticeTitle: $("notice-title"), noticeCopy: $("notice-copy"), start: $("start-button"), stop: $("stop-button"), refresh: $("refresh-button"), list: $("capture-list"), listEmpty: $("capture-empty"), count: $("capture-count"), reviewTitle: $("review-title"), reviewState: $("review-state"), reviewEmpty: $("review-empty"), reviewContent: $("review-content"), media: $("capture-media"), meta: $("capture-meta"), labelForm: $("label-form"), validation: $("label-validation"), save: $("save-label-button"), reject: $("reject-button"), remove: $("delete-button"), family: $("family-options"), view: $("view-options"), gear: $("gear-slot-options"), records: $("record-type-options"), abstention: $("abstention-options"), viewFieldset: $("view-fieldset"), gearFieldset: $("gear-slot-fieldset"), recordFieldset: $("record-types-fieldset"), abstentionFieldset: $("abstention-fieldset"), dialog: $("confirmation-dialog"), dialogKicker: $("confirmation-kicker"), dialogTitle: $("confirmation-title"), dialogCopy: $("confirmation-copy"), dialogSubmit: $("confirmation-submit")
|
||||
sessionState: $("session-state"), queueState: $("queue-state"), captureState: $("capture-state"), notice: $("notice"), noticeTitle: $("notice-title"), noticeCopy: $("notice-copy"), start: $("start-button"), stop: $("stop-button"), refresh: $("refresh-button"), filter: $("hide-reviewed"), list: $("capture-list"), listEmpty: $("capture-empty"), count: $("capture-count"), reviewTitle: $("review-title"), reviewState: $("review-state"), reviewEmpty: $("review-empty"), reviewContent: $("review-content"), media: $("capture-media"), meta: $("capture-meta"), labelForm: $("label-form"), validation: $("label-validation"), save: $("save-label-button"), reject: $("reject-button"), remove: $("delete-button"), family: $("family-options"), view: $("view-options"), gear: $("gear-slot-options"), records: $("record-type-options"), abstention: $("abstention-options"), viewFieldset: $("view-fieldset"), gearFieldset: $("gear-slot-fieldset"), recordFieldset: $("record-types-fieldset"), abstentionFieldset: $("abstention-fieldset"), dialog: $("confirmation-dialog"), dialogKicker: $("confirmation-kicker"), dialogTitle: $("confirmation-title"), dialogCopy: $("confirmation-copy"), dialogSubmit: $("confirmation-submit")
|
||||
});
|
||||
|
||||
let csrfToken = null;
|
||||
@@ -22,6 +24,7 @@ let captures = [];
|
||||
let armed = null;
|
||||
let pending = false;
|
||||
let returnFocus = null;
|
||||
let hideReviewed = false;
|
||||
|
||||
function setText(element, value) { element.textContent = value; }
|
||||
function clearNode(node) { while (node.firstChild) node.removeChild(node.firstChild); }
|
||||
@@ -32,12 +35,30 @@ function selected(name) { return document.querySelector(`input[name="${name}"]:c
|
||||
function selectedAll(name) { return [...document.querySelectorAll(`input[name="${name}"]:checked`)].map((input) => input.value); }
|
||||
function currentFamily() { return selected("family"); }
|
||||
function currentView() { return selected("view"); }
|
||||
function isReviewedCapture(capture) { return capture?.classification !== null && capture?.classification !== undefined; }
|
||||
function visibleCaptures() { return hideReviewed ? captures.filter((capture) => !isReviewedCapture(capture)) : captures; }
|
||||
function abbreviateCaptureId(value) { return value.length <= 22 ? value : `${value.slice(0, 11)}…${value.slice(-8)}`; }
|
||||
function formatCaptureTime(value) {
|
||||
if (typeof value !== "string" || value.length > 64) return "TIME UNAVAILABLE";
|
||||
const timestamp = Date.parse(value);
|
||||
if (!Number.isFinite(timestamp)) return "TIME UNAVAILABLE";
|
||||
const date = new Date(timestamp);
|
||||
const parts = [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes()];
|
||||
if (parts.some((part) => !Number.isInteger(part))) return "TIME UNAVAILABLE";
|
||||
const [year, month, day, hour, minute] = parts;
|
||||
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} ${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} UTC`;
|
||||
}
|
||||
function activeCaptureProjection() {
|
||||
if (!activeCapture) return null;
|
||||
return captures.find((capture) => capture.capture_id === activeCapture.capture_id) || activeCapture;
|
||||
}
|
||||
|
||||
function syncControls() {
|
||||
const locked = !csrfToken;
|
||||
els.start.disabled = pending || locked || armed === true;
|
||||
els.stop.disabled = pending || locked || armed !== true;
|
||||
els.refresh.disabled = pending || locked;
|
||||
els.filter.disabled = pending || locked;
|
||||
els.save.disabled = pending || locked || !activeCapture || !isValidLabel();
|
||||
els.reject.disabled = pending || locked || !activeCapture;
|
||||
els.remove.disabled = pending || locked || !activeCapture;
|
||||
@@ -71,7 +92,7 @@ function lock(message) {
|
||||
els.listEmpty.hidden = false;
|
||||
setText(els.listEmpty, message);
|
||||
clearNode(els.list);
|
||||
els.count.textContent = "0 items";
|
||||
els.count.textContent = "0 shown / 0 loaded";
|
||||
setBusy(false);
|
||||
}
|
||||
function validSession(payload) {
|
||||
@@ -178,24 +199,45 @@ function renderLabelForm(classification) {
|
||||
validateLabel();
|
||||
}
|
||||
function renderCaptures() {
|
||||
clearNode(els.list); els.count.textContent = `${captures.length} ${captures.length === 1 ? "item" : "items"}`; els.listEmpty.hidden = captures.length > 0;
|
||||
if (!captures.length) { setText(els.listEmpty, "EMPTY QUEUE: capture evidence will appear after a manual refresh."); return; }
|
||||
captures.forEach((capture) => {
|
||||
const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button"; button.setAttribute("aria-current", String(activeCapture?.capture_id === capture.capture_id));
|
||||
const id = document.createElement("span"); id.className = "capture-id"; id.textContent = capture.capture_id;
|
||||
const meta = document.createElement("span"); meta.className = "capture-meta"; meta.textContent = `${capture.state} · ${capture.received_at}`;
|
||||
const visible = visibleCaptures();
|
||||
clearNode(els.list); els.count.textContent = `${visible.length} shown / ${captures.length} loaded`; els.listEmpty.hidden = visible.length > 0;
|
||||
if (!visible.length) {
|
||||
setText(els.listEmpty, captures.length ? "ALL LOADED CAPTURES HIDDEN: clear Hide reviewed captures to inspect loaded captures." : "NO LOADED CAPTURES: refresh to check the latest local queue.");
|
||||
return;
|
||||
}
|
||||
visible.forEach((capture) => {
|
||||
const item = document.createElement("li"); const button = document.createElement("button"); button.type = "button";
|
||||
if (activeCapture?.capture_id === capture.capture_id) button.setAttribute("aria-current", "true");
|
||||
const reviewState = isReviewedCapture(capture) ? "REVIEWED" : "UNREVIEWED";
|
||||
const displayedTime = formatCaptureTime(capture.received_at);
|
||||
button.setAttribute("aria-label", `Capture ${capture.capture_id}. ${reviewState}. ${displayedTime}.`);
|
||||
const id = document.createElement("span"); id.className = "capture-id"; id.textContent = abbreviateCaptureId(capture.capture_id);
|
||||
const meta = document.createElement("span"); meta.className = "capture-meta";
|
||||
const review = document.createElement("span"); review.className = "capture-review"; review.textContent = reviewState;
|
||||
const time = document.createElement("span"); time.className = "capture-time"; time.textContent = displayedTime;
|
||||
meta.append(review, time);
|
||||
button.append(id, meta); button.addEventListener("click", () => selectCapture(capture.capture_id)); item.append(button); els.list.append(item);
|
||||
});
|
||||
syncControls();
|
||||
}
|
||||
function focusIfAvailable(control) { if (control && !control.disabled) control.focus(); }
|
||||
function focusVisibleCaptureOrFilter() { focusIfAvailable(els.list.querySelector("button:not(:disabled)") || els.filter); }
|
||||
function applyReviewedFilter() {
|
||||
hideReviewed = els.filter.checked;
|
||||
const activeProjection = activeCaptureProjection();
|
||||
const activeIsHidden = hideReviewed && isReviewedCapture(activeProjection);
|
||||
if (activeIsHidden) clearActiveDetail();
|
||||
renderCaptures();
|
||||
if (activeIsHidden) focusVisibleCaptureOrFilter();
|
||||
}
|
||||
async function refreshCaptures({ announce = true, fallbackFocus = null } = {}) {
|
||||
if (!csrfToken || pending) return false;
|
||||
setBusy(true); if (announce) notice("info", "LOADING", "Refreshing the local capture queue.");
|
||||
const payload = await request("/api/captures");
|
||||
if (!payload || !Array.isArray(payload.captures)) { setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return false; }
|
||||
captures = payload.captures;
|
||||
if (activeCapture && !captures.some((capture) => capture.capture_id === activeCapture.capture_id)) clearActiveDetail();
|
||||
const activeProjection = activeCaptureProjection();
|
||||
if (activeCapture && (!activeProjection || (hideReviewed && isReviewedCapture(activeProjection)))) clearActiveDetail();
|
||||
renderCaptures(); if (announce) notice("success", "QUEUE REFRESHED", "Local capture list updated."); setBusy(false); if (announce) focusIfAvailable(fallbackFocus); return true;
|
||||
}
|
||||
async function selectCapture(captureId, { announce = true } = {}) {
|
||||
@@ -238,6 +280,7 @@ async function confirmed(action, fallbackFocus) {
|
||||
focusIfAvailable(successFocus);
|
||||
}
|
||||
async function initialize() {
|
||||
els.filter.checked = false; hideReviewed = false;
|
||||
renderLabelForm(null);
|
||||
let fragment = window.location.hash; let token = null;
|
||||
if (fragment.length > 1 && fragment.length <= 256) { const params = new URLSearchParams(fragment.slice(1)); const candidate = params.get("bootstrap"); if (candidate && csrfPattern.test(candidate)) token = candidate; }
|
||||
@@ -267,10 +310,15 @@ els.labelForm.addEventListener("submit", async (event) => {
|
||||
if (!payload || !acceptMutationCsrf(payload)) { setBusy(false); return; }
|
||||
setBusy(false);
|
||||
if (!(await refreshCaptures({ announce: false }))) { focusIfAvailable(els.save); return; }
|
||||
if (captures.some((capture) => capture.capture_id === captureId)) await selectCapture(captureId, { announce: false });
|
||||
const savedCapture = captures.find((capture) => capture.capture_id === captureId);
|
||||
const savedCaptureIsHidden = Boolean(savedCapture && hideReviewed && isReviewedCapture(savedCapture));
|
||||
if (savedCapture && !savedCaptureIsHidden) await selectCapture(captureId, { announce: false });
|
||||
else { clearActiveDetail(); renderCaptures(); }
|
||||
notice("success", "LABEL SAVED", "Manual reviewed label saved with confidence 1.0.");
|
||||
focusIfAvailable(els.save);
|
||||
if (savedCaptureIsHidden) focusVisibleCaptureOrFilter();
|
||||
else focusIfAvailable(els.save);
|
||||
});
|
||||
els.filter.addEventListener("change", applyReviewedFilter);
|
||||
els.reject.addEventListener("click", () => showConfirmation("reject", els.reject));
|
||||
els.remove.addEventListener("click", () => showConfirmation("delete", els.remove));
|
||||
els.dialog.addEventListener("close", () => {
|
||||
|
||||
@@ -41,15 +41,21 @@
|
||||
|
||||
<section class="workspace-grid" aria-label="Capture review workspace">
|
||||
<aside class="capture-panel" aria-labelledby="capture-list-title">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<p class="kicker">PRIVATE QUEUE</p>
|
||||
<h2 id="capture-list-title">Captures</h2>
|
||||
</div>
|
||||
<span id="capture-count">0 items</span>
|
||||
</div>
|
||||
<p class="empty-copy" id="capture-empty">Authenticate to inspect the local capture queue.</p>
|
||||
<ol class="capture-list" id="capture-list" aria-label="Available captures"></ol>
|
||||
<span id="capture-count">0 shown / 0 loaded</span>
|
||||
</div>
|
||||
<label class="queue-filter" for="hide-reviewed">
|
||||
<input id="hide-reviewed" type="checkbox" />
|
||||
<span>Hide reviewed captures</span>
|
||||
</label>
|
||||
<div class="capture-scroll" id="capture-scroll">
|
||||
<p class="empty-copy" id="capture-empty">Authenticate to inspect the local capture queue.</p>
|
||||
<ol class="capture-list" id="capture-list" aria-label="Available captures"></ol>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="review-panel" aria-labelledby="review-title">
|
||||
|
||||
Reference in New Issue
Block a user