feat(courseware): add protected PPTX viewer
This commit is contained in:
@@ -0,0 +1,583 @@
|
||||
<?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'); ?>
|
||||
Reference in New Issue
Block a user