動画ライブラリ | エマナス
VIDEO LIBRARY
動画ライブラリ
find what you need、すぐに。
救急看護の基本から指導者向けコンテンツまで。STEP・テーマ・キーワードで絞り込んで、いま必要な一本にたどり着きます。
STEP
コピペで一括追加。 1行1件、区切りは |(パイプ) または タブ。
最低限タイトルだけでOK。STEPは 0-3 (0=参照資料)、テーマは , で複数指定。
URLを入れるとカードがクリックで動画ページに飛びます。
JSONファイルとして書き出し/読み込みができます。
',
'' + (ICONS[v.icon] || ICONS.heart) + '
',
'' + v.num + '',
'' + badges + '
',
duration,
' ',
'
',
'
',
'' + stepLabel(v.step) + '',
'' + esc(v.themes[0] || '') + '',
'
',
'
' + esc(v.title) + '
',
'
' + tags + '
',
'',
'
',
'' + tag + '>'
].join('');
}
function applyFilters() {
var list = videos.slice();
if (state.step !== 'all') {
var s = parseInt(state.step, 10);
list = list.filter(function(v) { return v.step === s; });
}
var activeThemes = Object.keys(state.themes);
if (activeThemes.length > 0) {
list = list.filter(function(v) {
return v.themes.some(function(t) { return state.themes[t]; });
});
}
var q = state.search.trim().toLowerCase();
if (q) {
list = list.filter(function(v) {
return v.title.toLowerCase().indexOf(q) !== -1 ||
(v.instructor || '').toLowerCase().indexOf(q) !== -1 ||
v.themes.some(function(t) { return t.toLowerCase().indexOf(q) !== -1; });
});
}
if (state.sort === 'new') list.sort(function(a, b) { return new Date(b.date) - new Date(a.date); });
else if (state.sort === 'step') list.sort(function(a, b) { return a.step - b.step || new Date(b.date) - new Date(a.date); });
else if (state.sort === 'popular') list.sort(function(a, b) { return b.popularity - a.popularity; });
else if (state.sort === 'duration') list.sort(function(a, b) { return (a.duration || 9999) - (b.duration || 9999); });
return list;
}
function renderGrid() {
var list = applyFilters();
var grid = document.getElementById('grid');
document.getElementById('resultCount').textContent = list.length;
var parts = [];
if (state.step !== 'all') parts.push('STEP' + state.step);
var activeThemes = Object.keys(state.themes);
if (activeThemes.length) parts.push(activeThemes.join('・'));
if (state.search.trim()) parts.push('「' + state.search.trim() + '」');
var sortLabels = { new: '新着順', step: 'STEP順', popular: '人気順', duration: '短い順' };
document.getElementById('activeFilters').textContent = (parts.length ? parts.join(' / ') + ' ・ ' : '') + sortLabels[state.sort];
if (list.length === 0) {
grid.innerHTML = '
該当するコンテンツが見つかりません
フィルターや検索条件を変更してみてください
';
return;
}
grid.innerHTML = list.map(cardHTML).join('');
}
function refresh() {
updateStats();
renderChips();
Object.keys(state.themes).forEach(function(t) {
if (!uniqueThemes().indexOf(t) === -1) delete state.themes[t];
});
renderGrid();
}
// ========== Parse ==========
function parseInput(text) {
var lines = text.split(/\r?\n/);
var rows = [];
var errors = [];
lines.forEach(function(rawLine, i) {
var line = rawLine.trim();
if (!line) return;
if (i === 0 && /タイトル|title/i.test(line) && /STEP|step|テーマ|theme/i.test(line)) return;
if (line.charAt(0) === '#' || line.indexOf('//') === 0) return;
var sep = '\t';
if (line.indexOf('\t') !== -1) sep = '\t';
else if (line.indexOf('|') !== -1) sep = '|';
else if (line.indexOf(',') !== -1) sep = ',';
var parts = line.split(sep).map(function(p) { return p.trim(); });
if (!parts[0]) {
errors.push({ line: i + 1, text: line, reason: 'タイトルが空' });
return;
}
var step = 2;
if (parts[1]) {
var n = parseInt(String(parts[1]).replace(/[^0-9]/g, ''), 10);
if (!isNaN(n) && n >= 0 && n
' + errors.length + '件読み込めない行: ';
errHTML += errors.slice(0, 3).map(function(e) { return '行' + e.line; }).join(', ') + '
';
}
var items = rows.map(function(r) {
return '
' +
'' + stepLabel(r.step) + '' +
'' + esc(r.title) + '' +
'' + esc(r.themes.join('・') || '—') + '' +
'' + (r.duration ? r.duration + '分' : '—') + '' +
'' + esc(r.date) + '' +
'
';
}).join('');
box.innerHTML =
'
' +
'
プレビュー' + rows.length + ' 件
' +
'
' + items + '
' +
errHTML +
'
' +
'
';
var applyBtn = document.getElementById('applyBtn');
if (applyBtn) {
applyBtn.addEventListener('click', function() {
var mode = document.querySelector('input[name="mode"]:checked').value;
if (mode === 'replace') {
videos = parsedRows.map(function(r, i) { return enrich(r, i); });
} else {
var start = videos.length;
videos = videos.concat(parsedRows.map(function(r, i) { return enrich(r, start + i); }));
}
save();
refresh();
toast(parsedRows.length + '件を反映しました');
closeModal();
document.getElementById('pasteArea').value = '';
box.innerHTML = '';
});
}
}
function renderEditTable() {
var tbody = document.getElementById('editTableBody');
tbody.innerHTML = videos.map(function(v, i) {
return '
' +
' | ' +
' | ' +
' | ' +
' | ' +
' | ' +
' | ' +
' | ' +
'
';
}).join('');
Array.prototype.forEach.call(tbody.querySelectorAll('.row-del'), function(btn) {
btn.addEventListener('click', function(e) {
var tr = e.currentTarget.closest('tr');
var idx = parseInt(tr.getAttribute('data-idx'), 10);
if (confirm('この動画を削除しますか?')) {
videos.splice(idx, 1);
save();
refresh();
renderEditTable();
}
});
});
}
// ========== Format guide render ==========
function renderFormatGuide() {
var el = document.getElementById('formatGuide');
el.innerHTML = FORMAT_GUIDE_LINES.map(function(pair) {
var line = pair[0], cls = pair[1];
if (cls) return '
' + esc(line) + '';
// Highlight separators
return esc(line).replace(/ \| /g, '
| ').replace(/\| $/g, '
|');
}).join('\n');
}
// ========== Init and bind ==========
function bind() {
document.getElementById('stepTabs').addEventListener('click', function(e) {
var btn = e.target.closest('.step-tab');
if (!btn) return;
Array.prototype.forEach.call(document.querySelectorAll('.step-tab'), function(b) { b.classList.remove('active'); });
btn.classList.add('active');
state.step = btn.getAttribute('data-step');
renderGrid();
});
document.getElementById('themeChips').addEventListener('click', function(e) {
var chip = e.target.closest('.chip');
if (!chip) return;
var theme = chip.getAttribute('data-theme');
if (state.themes[theme]) { delete state.themes[theme]; chip.classList.remove('active'); }
else { state.themes[theme] = true; chip.classList.add('active'); }
renderGrid();
});
var searchTimer;
document.getElementById('searchInput').addEventListener('input', function(e) {
clearTimeout(searchTimer);
var val = e.target.value;
searchTimer = setTimeout(function() { state.search = val; renderGrid(); }, 200);
});
document.getElementById('sortSelect').addEventListener('change', function(e) {
state.sort = e.target.value;
renderGrid();
});
document.getElementById('resetBtn').addEventListener('click', function() {
state.step = 'all'; state.themes = {}; state.search = ''; state.sort = 'new';
Array.prototype.forEach.call(document.querySelectorAll('.step-tab'), function(b) {
b.classList.toggle('active', b.getAttribute('data-step') === 'all');
});
Array.prototype.forEach.call(document.querySelectorAll('.chip'), function(c) { c.classList.remove('active'); });
document.getElementById('searchInput').value = '';
document.getElementById('sortSelect').value = 'new';
renderGrid();
});
// Modal
document.getElementById('openDataBtn').addEventListener('click', openModal);
document.getElementById('modalClose').addEventListener('click', closeModal);
document.getElementById('modalBackdrop').addEventListener('click', function(e) {
if (e.target === e.currentTarget) closeModal();
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeModal();
});
Array.prototype.forEach.call(document.querySelectorAll('.modal-tab'), function(t) {
t.addEventListener('click', function() {
Array.prototype.forEach.call(document.querySelectorAll('.modal-tab'), function(x) { x.classList.remove('active'); });
Array.prototype.forEach.call(document.querySelectorAll('.tab-panel'), function(x) { x.classList.remove('active'); });
t.classList.add('active');
document.querySelector('[data-panel="' + t.getAttribute('data-tab') + '"]').classList.add('active');
if (t.getAttribute('data-tab') === 'edit') renderEditTable();
});
});
document.getElementById('loadExampleBtn').addEventListener('click', function() {
document.getElementById('pasteArea').value = EXAMPLE_PASTE_LINES.join('\n');
});
document.getElementById('parseBtn').addEventListener('click', function() {
var text = document.getElementById('pasteArea').value;
if (!text.trim()) { toast('貼り付けてから解析してください'); return; }
var result = parseInput(text);
parsedRows = result.rows;
renderPreview(result.rows, result.errors);
});
document.getElementById('addRowBtn').addEventListener('click', function() {
videos.push(enrich({ title: '新規動画', step: 2, themes: [], duration: null, date: new Date().toISOString().slice(0, 10), url: '' }, videos.length));
renderEditTable();
});
document.getElementById('saveEditBtn').addEventListener('click', function() {
var rows = document.querySelectorAll('#editTableBody tr');
var updated = [];
Array.prototype.forEach.call(rows, function(tr, i) {
function g(k) { return tr.querySelector('[data-field="' + k + '"]').value.trim(); }
updated.push({
title: g('title') || '(無題)',
step: parseInt(g('step'), 10) || 2,
themes: g('themes').split(/[,、]/).map(function(t) { return t.trim(); }).filter(Boolean),
duration: g('duration') ? parseInt(g('duration'), 10) : null,
date: g('date') || new Date().toISOString().slice(0, 10),
url: g('url') || '',
isNew: videos[i] ? videos[i].isNew : false
});
});
videos = updated.map(function(v, i) { return enrich(v, i); });
save();
refresh();
toast('保存しました');
});
document.getElementById('exportBtn').addEventListener('click', function() {
var blob = new Blob([JSON.stringify(videos, null, 2)], { type: 'application/json' });
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'emanurse-videos-' + new Date().toISOString().slice(0, 10) + '.json';
document.body.appendChild(a); a.click(); a.remove();
toast('JSONを書き出しました');
});
document.getElementById('importBtn').addEventListener('click', function() {
document.getElementById('importInput').click();
});
document.getElementById('importInput').addEventListener('change', function(e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function(ev) {
try {
var data = JSON.parse(ev.target.result);
if (!Array.isArray(data)) throw new Error('配列形式ではありません');
videos = data.map(function(v, i) { return enrich(v, i); });
save();
refresh();
renderEditTable();
toast(videos.length + '件を読み込みました');
} catch (err) {
toast('読み込み失敗: ' + err.message);
}
};
reader.readAsText(file);
e.target.value = '';
});
document.getElementById('resetDataBtn').addEventListener('click', function() {
if (!confirm('サンプルデータに戻します。よろしいですか?')) return;
videos = SAMPLE.map(function(v, i) { return enrich(v, i); });
save();
refresh();
renderEditTable();
toast('サンプルに戻しました');
});
}
// ========== Start ==========
function start() {
try {
initData();
renderFormatGuide();
bind();
refresh();
} catch (err) {
document.getElementById('grid').innerHTML =
'
初期化エラー
' + esc(err.message) + '
';
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
})();