Files

584 lines
16 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$is_player = $selected_deck !== null;
$caption_colors = array('#e86a5c', '#2f9e99', '#f0a04b');
include('template/' . $OJ_TEMPLATE . '/header.php');
?>
<style>
body#MainBg-C.cw-catalog-page {
background: #f3eadc !important;
background-image:
repeating-linear-gradient(
to bottom,
rgba(120, 90, 50, 0.05) 0,
rgba(120, 90, 50, 0.05) 1px,
transparent 1px,
transparent 32px
) !important;
}
body#MainBg-C.cw-player-page {
background: #1b2430 !important;
}
#main.ui.main.container.cw-main {
width: 100% !important;
max-width: none !important;
margin: 0 !important;
padding: 0 24px 48px !important;
}
.cw-player-page #main.ui.main.container.cw-main {
padding: 0 !important;
}
.cw-player-page .footer {
display: none;
}
.cw-hero {
text-align: center;
padding: 28px 0 8px;
}
.cw-hero h1 {
margin: 0;
font-size: 3.4rem;
font-weight: 800;
color: #e86a5c;
letter-spacing: 0.04em;
line-height: 1.1;
}
.cw-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 28px;
max-width: 1180px;
margin: 28px auto 0;
}
.cw-card {
display: block;
background: #fff;
border-radius: 18px;
overflow: hidden;
text-decoration: none !important;
color: inherit;
box-shadow: 0 14px 32px rgba(80, 50, 20, 0.12);
transform: translateY(0);
transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.cw-card:hover,
.cw-card:focus {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(80, 50, 20, 0.16);
}
.cw-card-cover {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
aspect-ratio: 16 / 9;
background:
radial-gradient(circle at 20% 20%, rgba(255,255,255,0.35), transparent 42%),
linear-gradient(160deg, #f7d3c8, #f6efe4 55%, #d8efe9);
}
.cw-card-meta {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 16px;
color: #fff;
}
.cw-card-title {
font-size: 1.05rem;
font-weight: 700;
line-height: 1.35;
}
.cw-empty {
max-width: 640px;
margin: 40px auto 0;
padding: 28px 24px;
text-align: center;
background: rgba(255, 255, 255, 0.72);
border-radius: 18px;
color: #4b3b2a;
font-size: 1.05rem;
line-height: 1.7;
}
.cw-theater {
min-height: calc(100vh - 77px);
display: flex;
flex-direction: column;
padding: 18px 20px 24px;
color: #f4f1ea;
}
.cw-back {
align-self: flex-start;
color: #f4f1ea !important;
font-size: 1.05rem;
font-weight: 600;
text-decoration: underline;
text-underline-offset: 4px;
margin-bottom: 8px;
}
.cw-stage {
flex: 1;
display: grid;
grid-template-columns: 72px minmax(0, 1fr) 72px;
align-items: center;
gap: 12px;
min-height: 0;
}
.cw-nav-btn {
width: 64px;
height: 64px;
border: 0;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 2rem;
cursor: pointer;
color: #fff;
background: #5b6570;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
}
.cw-nav-btn.next {
background: #e86a5c;
}
.cw-nav-btn.is-disabled,
.cw-nav-btn:disabled {
opacity: 0.28;
pointer-events: none;
}
.cw-frame {
position: relative;
width: 100%;
max-height: calc(100vh - 220px);
aspect-ratio: 16 / 9;
margin: 0 auto;
background: #fff;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.35);
cursor: pointer;
}
.cw-frame canvas {
display: block;
width: 100%;
height: 100%;
}
.cw-status {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
text-align: center;
color: #3b3228;
font-size: 1.15rem;
font-weight: 600;
background: #fff8ee;
}
.cw-status.is-error {
color: #9b2c2c;
}
.cw-status[hidden] {
display: none;
}
.cw-pager {
text-align: center;
margin-top: 16px;
}
.cw-pager-num {
font-size: 1.8rem;
font-weight: 800;
letter-spacing: 0.04em;
}
.cw-pager-num em {
font-style: normal;
color: #e86a5c;
border-bottom: 4px solid #e86a5c;
padding: 0 2px;
}
.cw-dots {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.cw-dots button {
width: 10px;
height: 10px;
padding: 0;
border: 0;
border-radius: 50%;
background: #7b8490;
cursor: pointer;
}
.cw-dots button.is-current {
background: #e86a5c;
}
.cw-dots button:disabled {
cursor: default;
}
.cw-mobile-nav {
display: none;
justify-content: space-between;
padding: 8px 8px 0;
}
@media (max-width: 991px) {
.cw-hero h1 {
font-size: 2.4rem;
}
.cw-stage {
grid-template-columns: 1fr;
}
.cw-frame {
max-height: none;
}
.cw-mobile-nav {
display: flex;
}
}
</style>
<script>
document.body.classList.add('<?php echo $is_player ? 'cw-player-page' : 'cw-catalog-page'; ?>');
var main = document.getElementById('main');
if (main) {
main.classList.add('cw-main');
}
</script>
<?php if ($is_player) { ?>
<div class="cw-theater">
<a class="cw-back" href="courseware.php"><?php echo htmlspecialchars($MSG_COURSEWARE_BACK, ENT_QUOTES, 'UTF-8'); ?></a>
<div class="cw-stage">
<button type="button" class="cw-nav-btn desktop-only" id="cw-prev" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_PREV, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true"></button>
<div class="cw-frame" id="cw-frame">
<canvas id="cw-canvas"></canvas>
<div class="cw-status" id="cw-status" role="status" aria-live="polite"><?php echo htmlspecialchars($MSG_COURSEWARE_OPENING, ENT_QUOTES, 'UTF-8'); ?></div>
</div>
<button type="button" class="cw-nav-btn next desktop-only" id="cw-next" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_NEXT, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true"></button>
</div>
<div class="cw-mobile-nav">
<button type="button" class="cw-nav-btn" id="cw-prev-m" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_PREV, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true"></button>
<button type="button" class="cw-nav-btn next" id="cw-next-m" aria-label="<?php echo htmlspecialchars($MSG_COURSEWARE_NEXT, ENT_QUOTES, 'UTF-8'); ?>" disabled aria-disabled="true"></button>
</div>
<div class="cw-pager">
<div class="cw-pager-num" aria-live="polite"><em id="cw-page-now"><?php echo intval($current_page); ?></em> / <span id="cw-page-total"><?php echo htmlspecialchars($MSG_COURSEWARE_PAGE_UNKNOWN, ENT_QUOTES, 'UTF-8'); ?></span></div>
<div class="cw-dots" id="cw-dots" role="tablist"></div>
</div>
</div>
<script src="<?php echo $path_fix; ?>include/pptxviewjs/jszip.min.js"></script>
<script src="<?php echo $path_fix; ?>include/pptxviewjs/PptxViewJS.min.js"></script>
<script>
(function () {
var slug = <?php echo json_encode($selected_deck['slug'], JSON_UNESCAPED_UNICODE); ?>;
var page = <?php echo intval($current_page); ?>;
var desiredPage = page;
var total = 0;
var totalKnown = false;
var ready = false;
var failed = false;
var viewer = null;
var navQueue = Promise.resolve();
var navBusy = false;
var canvas = document.getElementById('cw-canvas');
var statusEl = document.getElementById('cw-status');
var buttons = {
prev: [document.getElementById('cw-prev'), document.getElementById('cw-prev-m')],
next: [document.getElementById('cw-next'), document.getElementById('cw-next-m')]
};
var msg = {
unknown: <?php echo json_encode($MSG_COURSEWARE_PAGE_UNKNOWN, JSON_UNESCAPED_UNICODE); ?>,
pageN: <?php echo json_encode($MSG_COURSEWARE_PAGE_N, JSON_UNESCAPED_UNICODE); ?>,
login: <?php echo json_encode($MSG_COURSEWARE_ERR_LOGIN, JSON_UNESCAPED_UNICODE); ?>,
missing: <?php echo json_encode($MSG_COURSEWARE_ERR_NOT_FOUND, JSON_UNESCAPED_UNICODE); ?>,
broken: <?php echo json_encode($MSG_COURSEWARE_ERR_BROKEN, JSON_UNESCAPED_UNICODE); ?>,
player: <?php echo json_encode($MSG_COURSEWARE_ERR_PLAYER, JSON_UNESCAPED_UNICODE); ?>
};
function setStatus(text, isError) {
if (!text) {
statusEl.hidden = true;
statusEl.removeAttribute('role');
statusEl.setAttribute('role', 'status');
statusEl.setAttribute('aria-live', 'polite');
return;
}
statusEl.hidden = false;
statusEl.textContent = text;
statusEl.classList.toggle('is-error', !!isError);
statusEl.setAttribute('role', isError ? 'alert' : 'status');
statusEl.setAttribute('aria-live', isError ? 'assertive' : 'polite');
}
function setBtnState(list, disabled) {
list.forEach(function (btn) {
if (!btn) {
return;
}
btn.disabled = !!disabled;
btn.setAttribute('aria-disabled', disabled ? 'true' : 'false');
btn.classList.toggle('is-disabled', !!disabled);
});
}
function clampPage(value) {
var next = parseInt(value, 10);
if (isNaN(next) || next < 1) {
next = 1;
}
if (totalKnown && total >= 1 && next > total) {
next = total;
}
return next;
}
function shownPage() {
if (viewer && ready && !failed) {
return viewer.getCurrentSlideIndex() + 1;
}
return page;
}
function updateChrome() {
if (viewer && ready) {
page = viewer.getCurrentSlideIndex() + 1;
total = viewer.getSlideCount() || total;
totalKnown = total >= 1;
}
document.getElementById('cw-page-now').textContent = String(page);
document.getElementById('cw-page-total').textContent = totalKnown ? String(total) : msg.unknown;
var disableNav = !ready || failed;
setBtnState(buttons.prev, disableNav || desiredPage <= 1);
setBtnState(buttons.next, disableNav || (totalKnown && desiredPage >= total));
var dots = document.getElementById('cw-dots');
if (totalKnown && dots.childNodes.length !== total) {
dots.innerHTML = '';
for (var i = 1; i <= total; i++) {
var dot = document.createElement('button');
dot.type = 'button';
dot.setAttribute('aria-label', msg.pageN.replace('%s', String(i)));
(function (index) {
dot.addEventListener('click', function (event) {
event.stopPropagation();
go(index);
});
})(i);
dots.appendChild(dot);
}
}
Array.prototype.forEach.call(dots.children, function (dot, idx) {
dot.classList.toggle('is-current', idx + 1 === page);
dot.disabled = !ready || failed;
dot.setAttribute('aria-disabled', (!ready || failed) ? 'true' : 'false');
});
if (window.history && window.history.replaceState) {
window.history.replaceState(null, '', 'courseware.php?deck=' + encodeURIComponent(slug) + '&page=' + page);
}
}
function go(nextPage) {
if (!viewer || !ready || failed) {
return;
}
desiredPage = clampPage(nextPage);
if (navBusy) {
return;
}
navBusy = true;
navQueue = navQueue.then(function pump() {
if (!viewer || !ready || failed) {
navBusy = false;
return;
}
var target = clampPage(desiredPage);
var current = shownPage();
if (target === current) {
page = current;
navBusy = false;
updateChrome();
return;
}
return viewer.goToSlide(target - 1).then(function () {
page = shownPage();
updateChrome();
if (clampPage(desiredPage) !== page) {
return pump();
}
navBusy = false;
});
}).catch(function () {
navBusy = false;
setStatus(msg.broken, true);
});
}
function sizeCanvas() {
var frame = document.getElementById('cw-frame');
var rect = frame.getBoundingClientRect();
var ratio = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(rect.width * ratio));
canvas.height = Math.max(1, Math.floor(rect.height * ratio));
}
function fail(kind) {
failed = true;
ready = false;
if (kind === 'login') {
setStatus(msg.login, true);
} else if (kind === 'missing') {
setStatus(msg.missing, true);
} else if (kind === 'player') {
setStatus(msg.player, true);
} else {
setStatus(msg.broken, true);
}
updateChrome();
}
try {
if (!window.Chart || !window.JSZip || !window.PptxViewJS || !window.PptxViewJS.PPTXViewer) {
fail('player');
return;
}
sizeCanvas();
viewer = new PptxViewJS.PPTXViewer({
canvas: canvas,
slideSizeMode: 'fit',
backgroundColor: '#ffffff'
});
viewer.on('slideChanged', updateChrome);
viewer.on('renderComplete', function () {
if (!failed) {
setStatus('');
updateChrome();
}
});
viewer.loadFromArrayBuffer = function (buffer) {
return this.loadFile(buffer);
};
fetch('courseware-file.php?deck=' + encodeURIComponent(slug), { credentials: 'same-origin' }).then(function (res) {
if (res.status === 401) {
throw { kind: 'login' };
}
if (res.status === 404 || !res.ok) {
throw { kind: 'missing' };
}
return res.arrayBuffer();
}).then(function (buf) {
return viewer.loadFromArrayBuffer(buf);
}).then(function () {
total = viewer.getSlideCount() || 0;
totalKnown = total >= 1;
if (!totalKnown) {
throw { kind: 'broken' };
}
ready = true;
page = clampPage(page);
desiredPage = page;
return viewer.goToSlide(page - 1);
}).then(function () {
setStatus('');
updateChrome();
}).catch(function (err) {
fail(err && err.kind ? err.kind : 'broken');
});
} catch (err) {
fail('player');
}
buttons.prev.forEach(function (btn) {
if (btn) btn.addEventListener('click', function () { go(desiredPage - 1); });
});
buttons.next.forEach(function (btn) {
if (btn) btn.addEventListener('click', function () { go(desiredPage + 1); });
});
document.addEventListener('keydown', function (event) {
if (event.key === 'ArrowLeft') {
event.preventDefault();
go(desiredPage - 1);
}
if (event.key === 'ArrowRight' || event.key === ' ') {
event.preventDefault();
go(desiredPage + 1);
}
if (event.key === 'Escape') window.location.href = 'courseware.php';
});
document.getElementById('cw-frame').addEventListener('click', function (event) {
var rect = this.getBoundingClientRect();
var x = event.clientX - rect.left;
if (x < rect.width * 0.4) go(desiredPage - 1);
if (x > rect.width * 0.6) go(desiredPage + 1);
});
window.addEventListener('resize', function () {
sizeCanvas();
if (viewer && ready && !failed) viewer.render();
});
updateChrome();
})();
</script>
<?php } else { ?>
<div class="cw-hero">
<h1><?php echo htmlspecialchars($MSG_COURSEWARE, ENT_QUOTES, 'UTF-8'); ?></h1>
</div>
<?php if (empty($decks)) { ?>
<div class="cw-empty"><?php echo htmlspecialchars($MSG_COURSEWARE_EMPTY, ENT_QUOTES, 'UTF-8'); ?></div>
<?php } else { ?>
<div class="cw-grid">
<?php foreach ($decks as $index => $deck) {
$bar = $caption_colors[$index % count($caption_colors)];
?>
<a class="cw-card" href="courseware.php?deck=<?php echo rawurlencode($deck['slug']); ?>">
<div class="cw-card-cover" aria-hidden="true"></div>
<div class="cw-card-meta" style="background:<?php echo $bar; ?>">
<div class="cw-card-title"><?php echo htmlspecialchars($deck['title'], ENT_QUOTES, 'UTF-8'); ?></div>
</div>
</a>
<?php } ?>
</div>
<?php } ?>
<?php } ?>
<?php include('template/' . $OJ_TEMPLATE . '/footer.php'); ?>