MediaWiki:Common.js: Difference between revisions
YouTube click-to-play and watch-next |
Document OCR review UI and docs |
||
| Line 1,472: | Line 1,472: | ||
if (!path) { | if (!path) { | ||
return ''; | return ''; | ||
} | |||
var jobMatch = String(path).match(/\/api\/jobs\/([a-fA-F0-9]+)\/preview(?:\?|$)/); | |||
var pageMatch = String(path).match(/\/api\/jobs\/([a-fA-F0-9]+)\/page\/(\d+)\/preview/); | |||
var params; | |||
if (pageMatch) { | |||
params = { | |||
ritchieocr_preview: '1', | |||
job_id: pageMatch[1], | |||
page: pageMatch[2] | |||
}; | |||
return mw.util.wikiScript() + '?' + $.param(params); | |||
} | |||
if (jobMatch) { | |||
params = { | |||
ritchieocr_preview: '1', | |||
job_id: jobMatch[1] | |||
}; | |||
return mw.util.wikiScript() + '?' + $.param(params); | |||
} | |||
/* Legacy fallback if a full absolute URL was stored */ | |||
if (/^https?:\/\//i.test(path)) { | |||
return path; | |||
} | } | ||
var base = (window.__ocrPublicUrl || '').replace(/\/$/, ''); | var base = (window.__ocrPublicUrl || '').replace(/\/$/, ''); | ||
| Line 1,482: | Line 1,504: | ||
function publicPreviewUrl(job) { | function publicPreviewUrl(job) { | ||
return publicAssetUrl(job && job.preview_url); | return publicAssetUrl(job && job.preview_url); | ||
} | |||
function timeAgo(ts) { | |||
var sec = Math.max(0, Math.floor(Date.now() / 1000 - Number(ts || 0))); | |||
if (!ts || sec < 60) { | |||
return 'just now'; | |||
} | |||
if (sec < 3600) { | |||
return Math.floor(sec / 60) + 'm ago'; | |||
} | |||
if (sec < 86400) { | |||
return Math.floor(sec / 3600) + 'h ago'; | |||
} | |||
return Math.floor(sec / 86400) + 'd ago'; | |||
} | } | ||
| Line 1,570: | Line 1,606: | ||
} | } | ||
if (fact.kind === 'note') { | if (fact.kind === 'note') { | ||
return (fact.label || ' | return 'Add research note — ' + (fact.label || 'OCR note') + ': ' + | ||
(val.length > 90 ? val.slice(0, 90) + '…' : val); | |||
} | } | ||
if (label.indexOf('team') !== -1 || label.indexOf('ipw') !== -1) { | if (label.indexOf('team') !== -1 || label.indexOf('ipw') !== -1) { | ||
| Line 1,619: | Line 1,656: | ||
} | } | ||
function isWritableFact(fact) { | function isWritableFact(fact, applyNotes) { | ||
return fact.kind === 'field' || fact.kind === 'bio'; | if (!fact) { | ||
return false; | |||
} | |||
if (fact.kind === 'field' || fact.kind === 'bio') { | |||
return true; | |||
} | |||
return !!(applyNotes && fact.kind === 'note'); | |||
} | } | ||
| Line 2,169: | Line 2,212: | ||
$id.append(document.createTextNode(job.identity_warning)); | $id.append(document.createTextNode(job.identity_warning)); | ||
$panel.append($id); | $panel.append($id); | ||
} | |||
if (mwApiBool(job.pdf_truncated)) { | |||
$panel.append($('<div>', { 'class': 'ocr-identity-banner ocr-identity-warn' }).append( | |||
$('<strong>').text('PDF truncated: '), | |||
document.createTextNode( | |||
'Only the first ' + (job.page_count || 8) + ' of ' + | |||
(job.pdf_total_pages || '?') + | |||
' pages were OCR’d. Split the PDF or re-upload the remaining pages.' | |||
) | |||
)); | |||
} | |||
if (job.error) { | |||
$panel.append($('<div>', { 'class': 'ocr-identity-banner ocr-identity-warn' }).text(job.error)); | |||
} | } | ||
| Line 2,214: | Line 2,272: | ||
function refreshPreview() { | function refreshPreview() { | ||
var applyNotesOn = !!$panel.find('.ocr-apply-notes input').prop('checked'); | |||
var props = collectProposed(); | var props = collectProposed(); | ||
var byPage = {}; | var byPage = {}; | ||
props.forEach(function (p) { | props.forEach(function (p) { | ||
if (!p.selected || !isWritableFact(p) || factAlreadyOnPage(p)) { | if (!p.selected || !isWritableFact(p, applyNotesOn) || factAlreadyOnPage(p)) { | ||
return; | return; | ||
} | } | ||
| Line 2,316: | Line 2,375: | ||
' Add document to Gallery on Apply' + (isMultiPersonJob(job) ? ' (off by default for multi-person)' : '') | ' Add document to Gallery on Apply' + (isMultiPersonJob(job) ? ' (off by default for multi-person)' : '') | ||
); | ); | ||
var $applyNotes = $('<label>', { 'class': 'ocr-apply-notes' }).append( | |||
$('<input>', { type: 'checkbox', checked: mwApiBool(job.apply_notes) }), | |||
' Also write selected notes into Research notes' | |||
); | |||
var $forceMulti = $('<label>', { 'class': 'ocr-force-multi' }).append( | |||
$('<input>', { type: 'checkbox', checked: mwApiBool(job.force_multi) }), | |||
' Treat as multi-person roster (rebuild required)' | |||
); | |||
if (isMultiPersonJob(job) && !job.bound_title) { | |||
$forceMulti.hide(); | |||
} | |||
var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | var $forceId = $('<label>', { 'class': 'ocr-force-identity' }).append( | ||
$('<input>', { type: 'checkbox', checked: false }), | $('<input>', { type: 'checkbox', checked: false }), | ||
| Line 2,323: | Line 2,393: | ||
$forceId.hide(); | $forceId.hide(); | ||
} | } | ||
$advBody.append($overwrite, $('<br>'), $gallery, $('<br>'), $forceId); | $applyNotes.find('input').on('change', refreshPreview); | ||
$advBody.append( | |||
$overwrite, $('<br>'), | |||
$gallery, $('<br>'), | |||
$applyNotes, $('<br>'), | |||
$forceMulti, $('<br>'), | |||
$forceId | |||
); | |||
var $save = $('<button>', { type: 'button', text: 'Save draft' }); | var $save = $('<button>', { type: 'button', text: 'Save draft' }); | ||
var $rebuild = $('<button>', { type: 'button', text: 'Rebuild from pages' }); | var $rebuild = $('<button>', { type: 'button', text: 'Rebuild from pages / roster mode' }); | ||
if (pages.length < 2) { | if (pages.length < 2 && !job.bound_title) { | ||
$rebuild.hide(); | $rebuild.hide(); | ||
} | } | ||
| Line 2,355: | Line 2,432: | ||
function collectProposed() { | function collectProposed() { | ||
var applyNotesOn = !!$applyNotes.find('input').prop('checked'); | |||
var out = []; | var out = []; | ||
$list.find('.ocr-fact-row').each(function () { | $list.find('.ocr-fact-row').each(function () { | ||
| Line 2,370: | Line 2,448: | ||
var $cb = $row.find('input[type="checkbox"]').first(); | var $cb = $row.find('input[type="checkbox"]').first(); | ||
base.selected = $cb.length ? !!$cb.prop('checked') : false; | base.selected = $cb.length ? !!$cb.prop('checked') : false; | ||
if ( | if (base.kind === 'note' && !applyNotesOn) { | ||
base.selected = false; | base.selected = false; | ||
} | } | ||
| Line 2,408: | Line 2,486: | ||
doc_type: $docSelect.val(), | doc_type: $docSelect.val(), | ||
gallery_caption: $.trim($caption.val() || ''), | gallery_caption: $.trim($caption.val() || ''), | ||
force_identity: $forceId.find('input').prop('checked') | force_identity: $forceId.find('input').prop('checked'), | ||
force_multi: $forceMulti.find('input').prop('checked'), | |||
apply_notes: $applyNotes.find('input').prop('checked') | |||
}; | }; | ||
} | } | ||
| Line 2,469: | Line 2,549: | ||
} | } | ||
var selected = collectProposed().filter(function (p) { | var selected = collectProposed().filter(function (p) { | ||
return p.selected && isWritableFact(p) && (p.target_title || '').trim(); | return p.selected && isWritableFact(p, $applyNotes.find('input').prop('checked')) && (p.target_title || '').trim(); | ||
}); | }); | ||
if (!selected.length) { | if (!selected.length) { | ||
$status.text('Select at least one change with a matched soldier.'); | $status.text('Select at least one change with a matched soldier.'); | ||
return; | |||
} | |||
var nPages = {}; | |||
selected.forEach(function (p) { nPages[p.target_title] = 1; }); | |||
var pageCount = Object.keys(nPages).length; | |||
if (pageCount > 5 && !window.confirm('Apply changes to ' + pageCount + ' soldier pages?')) { | |||
return; | return; | ||
} | } | ||
| Line 2,549: | Line 2,635: | ||
} | } | ||
function startUpload($fileInput, boundTitle, $host, cfg) { | function startUpload($fileInput, boundTitle, $host, cfg, opts) { | ||
opts = opts || {}; | |||
var file = $fileInput[0].files[0]; | var file = $fileInput[0].files[0]; | ||
if (!file) { | if (!file) { | ||
| Line 2,560: | Line 2,647: | ||
op: 'upload', | op: 'upload', | ||
bound_title: boundTitle || '', | bound_title: boundTitle || '', | ||
force_multi: opts.forceMulti ? true : false, | |||
file: file, | file: file, | ||
format: 'json' | format: 'json' | ||
| Line 2,616: | Line 2,704: | ||
'class': 'soldier-upload-file' | 'class': 'soldier-upload-file' | ||
}); | }); | ||
var $rosterMode = $('<label>', { 'class': 'ocr-force-multi-upload' }).append( | |||
$('<input>', { type: 'checkbox' }), | |||
' This is a multi-person roster (not only this soldier)' | |||
); | |||
var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' }); | var $btn = $('<button>', { type: 'button', text: 'Upload & OCR' }); | ||
var $host = $('<div>', { 'class': 'ocr-review-host' }); | var $host = $('<div>', { 'class': 'ocr-review-host' }); | ||
$section.append($file, $btn, $host); | $section.append($file, $rosterMode, $btn, $host); | ||
$btn.on('click', function () { | $btn.on('click', function () { | ||
startUpload($file, pageTitle, $host, cfg); | startUpload($file, pageTitle, $host, cfg, { | ||
forceMulti: $rosterMode.find('input').prop('checked') | |||
}); | |||
}); | }); | ||
var $slot = $('.soldier-ocr-slot').first(); | var $slot = $('.soldier-ocr-slot').first(); | ||
| Line 2,658: | Line 2,752: | ||
'Resume ready jobs, or reopen a profile upload that finished after you left the page.' | 'Resume ready jobs, or reopen a profile upload that finished after you left the page.' | ||
)); | )); | ||
var me = mw.config.get('wgUserName') || ''; | |||
var $filters = $('<div>', { 'class': 'ocr-queue-filters' }); | |||
var $filterMine = $('<label>').append( | |||
$('<input>', { type: 'checkbox', checked: false, 'class': 'ocr-queue-mine' }), | |||
' Only mine' | |||
); | |||
var $filterReady = $('<label>').append( | |||
$('<input>', { type: 'checkbox', checked: true, 'class': 'ocr-queue-ready' }), | |||
' Ready' | |||
); | |||
var $filterProc = $('<label>').append( | |||
$('<input>', { type: 'checkbox', checked: true, 'class': 'ocr-queue-processing' }), | |||
' In progress' | |||
); | |||
$filters.append($filterMine, ' ', $filterReady, ' ', $filterProc); | |||
var $ul = $('<ul>', { 'class': 'ocr-queue-list' }); | var $ul = $('<ul>', { 'class': 'ocr-queue-list' }); | ||
$queue.append($ul); | $queue.append($filters, $ul); | ||
$slot.append($queue); | $slot.append($queue); | ||
| Line 2,672: | Line 2,781: | ||
}).then(function (bags) { | }).then(function (bags) { | ||
$ul.empty(); | $ul.empty(); | ||
var jobs = [].concat(bags.processing || [] | var showMine = $filterMine.find('input').prop('checked'); | ||
var showReady = $filterReady.find('input').prop('checked'); | |||
var showProc = $filterProc.find('input').prop('checked'); | |||
var jobs = []; | |||
if (showProc) { | |||
jobs = jobs.concat(bags.processing || []); | |||
} | |||
if (showReady) { | |||
jobs = jobs.concat(bags.ready || []); | |||
} | |||
if (showMine) { | |||
jobs = jobs.filter(function (j) { | |||
return (j.uploader || '') === me; | |||
}); | |||
} | |||
if (!jobs.length) { | if (!jobs.length) { | ||
$ul.append($('<li>').text('No | $ul.append($('<li>').text('No jobs match these filters.')); | ||
return; | return; | ||
} | } | ||
| Line 2,693: | Line 2,816: | ||
? ((ps.detected || 0) + ' people · ' + (ps.matched || 0) + ' matched · ' + (ps.unresolved || 0) + ' unmatched') | ? ((ps.detected || 0) + ' people · ' + (ps.matched || 0) + ' matched · ' + (ps.unresolved || 0) + ' unmatched') | ||
: (job.bound_title ? job.bound_title : 'single document'); | : (job.bound_title ? job.bound_title : 'single document'); | ||
var mineBit = (job.uploader || '') === me ? ' · you' : ''; | |||
$meta.append($('<div>').append( | $meta.append($('<div>').append( | ||
$('<strong>').text(job.filename || job.id), | $('<strong>').text(job.filename || job.id), | ||
| Line 2,700: | Line 2,824: | ||
peopleBit + ' · ' + (job.status || '?') + | peopleBit + ' · ' + (job.status || '?') + | ||
(job.status === 'processing' ? (' ' + (job.progress || 0) + '%') : '') + | (job.status === 'processing' ? (' ' + (job.progress || 0) + '%') : '') + | ||
' · by ' + (job.uploader || '?') | ' · by ' + (job.uploader || '?') + mineBit + | ||
' · ' + timeAgo(job.created_at) | |||
)); | )); | ||
if (job.apply_progress && job.apply_progress.length) { | |||
$meta.append($('<div>', { 'class': 'ocr-fact-meta' }).text( | |||
'Partial apply: ' + job.apply_progress.length + ' page(s) already saved — Open and Apply to resume' | |||
)); | |||
} | |||
var $open = $('<button>', { type: 'button', text: 'Open' }); | var $open = $('<button>', { type: 'button', text: 'Open' }); | ||
$meta.append($open); | $meta.append($open); | ||
| Line 2,721: | Line 2,851: | ||
}); | }); | ||
} | } | ||
$filters.find('input').on('change', loadQueue); | |||
loadQueue(); | loadQueue(); | ||
} | } | ||