Initial commit
28
RCV_ADinh_QuizArena_K5-09022026/appmanifest.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "RCV-G5",
|
||||
"short_name": "RCV-G5",
|
||||
"start_url": "index.html",
|
||||
"display": "fullscreen",
|
||||
"orientation": "any",
|
||||
"icons": [{
|
||||
"src": "icon-16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-256.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "icon-256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}]
|
||||
}
|
||||
24110
RCV_ADinh_QuizArena_K5-09022026/c2runtime.js
Normal file
1
RCV_ADinh_QuizArena_K5-09022026/data.js
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/icon-114.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/icon-128.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/icon-16.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/icon-256.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/icon-32.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/answer-sheet0.png
Normal file
|
After Width: | Height: | Size: 606 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/answer-sheet1.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/asset-sheet0.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/asset2-sheet0.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/asset3-sheet0.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/bg-default-000.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/bgm-sheet0.png
Normal file
|
After Width: | Height: | Size: 262 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/bgm-sheet1.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/khungggaudio-sheet0.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/khungggaudio2-sheet0.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/koala_stable-sheet0.png
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/koalaro-sheet0.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/logogame-sheet0.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/logotruong-sheet0.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/question-sheet0.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/sprite-sheet0.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 245 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/images/txt_answers.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
139
RCV_ADinh_QuizArena_K5-09022026/index.html
Normal file
@@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<title>RCV-G5</title>
|
||||
|
||||
<!-- Standardised web app manifest -->
|
||||
<link rel="manifest" href="appmanifest.json" />
|
||||
|
||||
<!-- Allow fullscreen mode on iOS devices. (These are Apple specific meta tags.) -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, minimal-ui" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="apple-touch-icon" sizes="256x256" href="icon-256.png" />
|
||||
<meta name="HandheldFriendly" content="true" />
|
||||
|
||||
<!-- Chrome for Android web app tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="shortcut icon" sizes="256x256" href="icon-256.png" />
|
||||
|
||||
<!-- All margins and padding must be zero for the canvas to fill the screen. -->
|
||||
<style type="text/css">
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html, body {
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
-ms-touch-action: none;
|
||||
}
|
||||
canvas {
|
||||
touch-action-delay: none;
|
||||
touch-action: none;
|
||||
-ms-touch-action: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="fb-root"></div>
|
||||
<div style="width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: -1;">
|
||||
<!--<img src = "./bg.jpg" style="width:100%; height: 100%; object-fit: cover;"/> -->
|
||||
<video autoplay="" loop="" muted="" style="width : 100%; height: 100%;object-fit: cover;" src="bg.mp4">
|
||||
</video>
|
||||
</div>
|
||||
<script>
|
||||
// Issue a warning if trying to preview an exported project on disk.
|
||||
(function(){
|
||||
// Check for running exported on file protocol
|
||||
if (window.location.protocol.substr(0, 4) === "file")
|
||||
{
|
||||
alert("Exported games won't work until you upload them. (When running on the file:/// protocol, browsers block many features from working for security reasons.)");
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- The canvas must be inside a div called c2canvasdiv -->
|
||||
<div id="c2canvasdiv">
|
||||
|
||||
<!-- The canvas the project will render to. If you change its ID, don't forget to change the
|
||||
ID the runtime looks for in the jQuery events above (ready() and cr_sizeCanvas()). -->
|
||||
<canvas id="c2canvas" width="1400" height="900">
|
||||
<!-- This text is displayed if the visitor's browser does not support HTML5.
|
||||
You can change it, but it is a good idea to link to a description of a browser
|
||||
and provide some links to download some popular HTML5-compatible browsers. -->
|
||||
<h1>Your browser does not appear to support HTML5. Try upgrading your browser to the latest version. <a href="http://www.whatbrowser.org">What is a browser?</a>
|
||||
<br/><br/><a href="http://www.microsoft.com/windows/internet-explorer/default.aspx">Microsoft Internet Explorer</a><br/>
|
||||
<a href="http://www.mozilla.com/firefox/">Mozilla Firefox</a><br/>
|
||||
<a href="http://www.google.com/chrome/">Google Chrome</a><br/>
|
||||
<a href="http://www.apple.com/safari/download/">Apple Safari</a></h1>
|
||||
</canvas>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pages load faster with scripts at the bottom -->
|
||||
|
||||
<!-- Construct 2 exported games require jQuery. -->
|
||||
<script src="jquery-3.4.1.min.js"></script>
|
||||
<script src="tdv_sdk.js"></script>
|
||||
|
||||
|
||||
<!-- The runtime script. You can rename it, but don't forget to rename the reference here as well.
|
||||
This file will have been minified and obfuscated if you enabled "Minify script" during export. -->
|
||||
<script src="c2runtime.js"></script>
|
||||
|
||||
<script>
|
||||
// Start the Construct 2 project running on window load.
|
||||
jQuery(document).ready(function ()
|
||||
{
|
||||
// Create new runtime using the c2canvas
|
||||
cr_createRuntime("c2canvas");
|
||||
});
|
||||
|
||||
// Pause and resume on page becoming visible/invisible
|
||||
function onVisibilityChanged() {
|
||||
if (document.hidden || document.mozHidden || document.webkitHidden || document.msHidden)
|
||||
cr_setSuspended(true);
|
||||
else
|
||||
cr_setSuspended(false);
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("mozvisibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("webkitvisibilitychange", onVisibilityChanged, false);
|
||||
document.addEventListener("msvisibilitychange", onVisibilityChanged, false);
|
||||
|
||||
function OnRegisterSWError(e)
|
||||
{
|
||||
console.warn("Failed to register service worker: ", e);
|
||||
};
|
||||
|
||||
// Runtime calls this global method when ready to start caching (i.e. after startup).
|
||||
// This registers the service worker which caches resources for offline support.
|
||||
window.C2_RegisterSW = function C2_RegisterSW()
|
||||
{
|
||||
if (!navigator.serviceWorker)
|
||||
return; // no SW support, ignore call
|
||||
|
||||
try {
|
||||
navigator.serviceWorker.register("sw.js", { scope: "./" })
|
||||
.then(function (reg)
|
||||
{
|
||||
console.log("Registered service worker on " + reg.scope);
|
||||
})
|
||||
.catch(OnRegisterSWError);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
OnRegisterSWError(e);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
2
RCV_ADinh_QuizArena_K5-09022026/jquery-3.4.1.min.js
vendored
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/loading-logo.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
RCV_ADinh_QuizArena_K5-09022026/media/alert-234711.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/bg_music.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/bg_music.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/bg_music_2.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/bg_music_2.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/bubble-pop-389501.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/button-124476.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/click-234708.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/click.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/click.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/collect-5930.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/edugamery-music-13.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/error-010-206498.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/error-08-206492.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/fail-234710.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/interface-2-126517.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/loadquestion.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/loadquestion.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/timeo.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/timeo.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_clap.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_clap.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_correct.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_correct.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_themes.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/win_themes.ogg
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/wrong2.m4a
Normal file
BIN
RCV_ADinh_QuizArena_K5-09022026/media/wrong2.ogg
Normal file
62
RCV_ADinh_QuizArena_K5-09022026/offline.js
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"version": 1770179717,
|
||||
"fileList": [
|
||||
"data.js",
|
||||
"c2runtime.js",
|
||||
"jquery-3.4.1.min.js",
|
||||
"offlineClient.js",
|
||||
"images/bg-default-000.jpg",
|
||||
"images/koalaro-sheet0.png",
|
||||
"images/koala_stable-sheet0.png",
|
||||
"images/answer-sheet0.png",
|
||||
"images/answer-sheet1.png",
|
||||
"images/question-sheet0.png",
|
||||
"images/asset-sheet0.png",
|
||||
"images/asset2-sheet0.png",
|
||||
"images/txt_answers.png",
|
||||
"images/asset3-sheet0.png",
|
||||
"images/sprite-sheet0.png",
|
||||
"images/logogame-sheet0.png",
|
||||
"images/logotruong-sheet0.png",
|
||||
"images/tvdcóviềnknềnstroke-sheet0.png",
|
||||
"images/khungggaudio-sheet0.png",
|
||||
"images/khungggaudio2-sheet0.png",
|
||||
"images/bgm-sheet0.png",
|
||||
"images/bgm-sheet1.png",
|
||||
"media/bg_music.m4a",
|
||||
"media/bg_music.ogg",
|
||||
"media/bg_music_2.m4a",
|
||||
"media/bg_music_2.ogg",
|
||||
"media/click.m4a",
|
||||
"media/click.ogg",
|
||||
"media/loadquestion.m4a",
|
||||
"media/loadquestion.ogg",
|
||||
"media/timeo.m4a",
|
||||
"media/timeo.ogg",
|
||||
"media/win_clap.m4a",
|
||||
"media/win_clap.ogg",
|
||||
"media/win_correct.m4a",
|
||||
"media/win_correct.ogg",
|
||||
"media/win_themes.m4a",
|
||||
"media/win_themes.ogg",
|
||||
"media/wrong2.m4a",
|
||||
"media/wrong2.ogg",
|
||||
"media/alert-234711.ogg",
|
||||
"media/bubble-pop-389501.ogg",
|
||||
"media/button-124476.ogg",
|
||||
"media/click-234708.ogg",
|
||||
"media/collect-5930.ogg",
|
||||
"media/edugamery-music-13.ogg",
|
||||
"media/error-010-206498.ogg",
|
||||
"media/error-08-206492.ogg",
|
||||
"media/fail-234710.ogg",
|
||||
"media/interface-2-126517.ogg",
|
||||
"icon-16.png",
|
||||
"icon-32.png",
|
||||
"icon-114.png",
|
||||
"icon-128.png",
|
||||
"icon-256.png",
|
||||
"loading-logo.png",
|
||||
"tdv_sdk.js"
|
||||
]
|
||||
}
|
||||
53
RCV_ADinh_QuizArena_K5-09022026/offlineClient.js
Normal file
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
class OfflineClient
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
// Create a BroadcastChannel, if supported.
|
||||
this._broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel("offline"));
|
||||
|
||||
// Queue of messages received before a message callback is set.
|
||||
this._queuedMessages = [];
|
||||
|
||||
// The message callback.
|
||||
this._onMessageCallback = null;
|
||||
|
||||
// If BroadcastChannel is supported, listen for messages.
|
||||
if (this._broadcastChannel)
|
||||
this._broadcastChannel.onmessage = (e => this._OnBroadcastChannelMessage(e));
|
||||
}
|
||||
|
||||
_OnBroadcastChannelMessage(e)
|
||||
{
|
||||
// Have a message callback set: just forward the call.
|
||||
if (this._onMessageCallback)
|
||||
{
|
||||
this._onMessageCallback(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise the app hasn't loaded far enough to set a message callback.
|
||||
// Buffer the incoming messages to replay when the app sets a callback.
|
||||
this._queuedMessages.push(e);
|
||||
}
|
||||
|
||||
SetMessageCallback(f)
|
||||
{
|
||||
this._onMessageCallback = f;
|
||||
|
||||
// Replay any queued messages through the handler, then clear the queue.
|
||||
for (let e of this._queuedMessages)
|
||||
this._onMessageCallback(e);
|
||||
|
||||
this._queuedMessages.length = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Create the offline client ASAP so we receive and start queueing any messages the SW broadcasts.
|
||||
window.OfflineClientInfo = new OfflineClient();
|
||||
|
||||
}());
|
||||
|
||||
403
RCV_ADinh_QuizArena_K5-09022026/sw.js
Normal file
@@ -0,0 +1,403 @@
|
||||
"use strict";
|
||||
|
||||
const OFFLINE_DATA_FILE = "offline.js";
|
||||
const CACHE_NAME_PREFIX = "c2offline";
|
||||
const BROADCASTCHANNEL_NAME = "offline";
|
||||
const CONSOLE_PREFIX = "[SW] ";
|
||||
const LAZYLOAD_KEYNAME = "";
|
||||
|
||||
// Create a BroadcastChannel if supported.
|
||||
const broadcastChannel = (typeof BroadcastChannel === "undefined" ? null : new BroadcastChannel(BROADCASTCHANNEL_NAME));
|
||||
|
||||
//////////////////////////////////////
|
||||
// Utility methods
|
||||
function PostBroadcastMessage(o)
|
||||
{
|
||||
if (!broadcastChannel)
|
||||
return; // not supported
|
||||
|
||||
// Impose artificial (and arbitrary!) delay of 3 seconds to make sure client is listening by the time the message is sent.
|
||||
// Note we could remove the delay on some messages, but then we create a race condition where sometimes messages can arrive
|
||||
// in the wrong order (e.g. "update ready" arrives before "started downloading update"). So to keep the consistent ordering,
|
||||
// delay all messages by the same amount.
|
||||
setTimeout(() => broadcastChannel.postMessage(o), 3000);
|
||||
};
|
||||
|
||||
function Broadcast(type)
|
||||
{
|
||||
PostBroadcastMessage({
|
||||
"type": type
|
||||
});
|
||||
};
|
||||
|
||||
function BroadcastDownloadingUpdate(version)
|
||||
{
|
||||
PostBroadcastMessage({
|
||||
"type": "downloading-update",
|
||||
"version": version
|
||||
});
|
||||
}
|
||||
|
||||
function BroadcastUpdateReady(version)
|
||||
{
|
||||
PostBroadcastMessage({
|
||||
"type": "update-ready",
|
||||
"version": version
|
||||
});
|
||||
}
|
||||
|
||||
function IsUrlInLazyLoadList(url, lazyLoadList)
|
||||
{
|
||||
if (!lazyLoadList)
|
||||
return false; // presumably lazy load list failed to load
|
||||
|
||||
try {
|
||||
for (const lazyLoadRegex of lazyLoadList)
|
||||
{
|
||||
if (new RegExp(lazyLoadRegex).test(url))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.error(CONSOLE_PREFIX + "Error matching in lazy-load list: ", err);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
function WriteLazyLoadListToStorage(lazyLoadList)
|
||||
{
|
||||
if (typeof localforage === "undefined")
|
||||
return Promise.resolve(); // bypass if localforage not imported
|
||||
else
|
||||
return localforage.setItem(LAZYLOAD_KEYNAME, lazyLoadList)
|
||||
};
|
||||
|
||||
function ReadLazyLoadListFromStorage()
|
||||
{
|
||||
if (typeof localforage === "undefined")
|
||||
return Promise.resolve([]); // bypass if localforage not imported
|
||||
else
|
||||
return localforage.getItem(LAZYLOAD_KEYNAME);
|
||||
};
|
||||
|
||||
function GetCacheBaseName()
|
||||
{
|
||||
// Include the scope to avoid name collisions with any other SWs on the same origin.
|
||||
// e.g. "c2offline-https://example.com/foo/" (won't collide with anything under bar/)
|
||||
return CACHE_NAME_PREFIX + "-" + self.registration.scope;
|
||||
};
|
||||
|
||||
function GetCacheVersionName(version)
|
||||
{
|
||||
// Append the version number to the cache name.
|
||||
// e.g. "c2offline-https://example.com/foo/-v2"
|
||||
return GetCacheBaseName() + "-v" + version;
|
||||
};
|
||||
|
||||
// Return caches.keys() filtered down to just caches we're interested in (with the right base name).
|
||||
// This filters out caches from unrelated scopes.
|
||||
async function GetAvailableCacheNames()
|
||||
{
|
||||
const cacheNames = await caches.keys();
|
||||
const cacheBaseName = GetCacheBaseName();
|
||||
return cacheNames.filter(n => n.startsWith(cacheBaseName));
|
||||
};
|
||||
|
||||
// Identify if an update is pending, which is the case when we have 2 or more available caches.
|
||||
// One must be an update that is waiting, since the next navigate that does an upgrade will
|
||||
// delete all the old caches leaving just one currently-in-use cache.
|
||||
async function IsUpdatePending()
|
||||
{
|
||||
const availableCacheNames = await GetAvailableCacheNames();
|
||||
return (availableCacheNames.length >= 2);
|
||||
};
|
||||
|
||||
// Automatically deduce the main page URL (e.g. index.html or main.aspx) from the available browser windows.
|
||||
// This prevents having to hard-code an index page in the file list, implicitly caching it like AppCache did.
|
||||
async function GetMainPageUrl()
|
||||
{
|
||||
const allClients = await clients.matchAll({
|
||||
includeUncontrolled: true,
|
||||
type: "window"
|
||||
});
|
||||
|
||||
for (const c of allClients)
|
||||
{
|
||||
// Parse off the scope from the full client URL, e.g. https://example.com/index.html -> index.html
|
||||
let url = c.url;
|
||||
if (url.startsWith(self.registration.scope))
|
||||
url = url.substring(self.registration.scope.length);
|
||||
|
||||
if (url && url !== "/") // ./ is also implicitly cached so don't bother returning that
|
||||
{
|
||||
// If the URL is solely a search string, prefix it with / to ensure it caches correctly.
|
||||
// e.g. https://example.com/?foo=bar needs to cache as /?foo=bar, not just ?foo=bar.
|
||||
if (url.startsWith("?"))
|
||||
url = "/" + url;
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
return ""; // no main page URL could be identified
|
||||
};
|
||||
|
||||
// Hack to fetch optionally bypassing HTTP cache until fetch cache options are supported in Chrome (crbug.com/453190)
|
||||
function fetchWithBypass(request, bypassCache)
|
||||
{
|
||||
if (typeof request === "string")
|
||||
request = new Request(request);
|
||||
|
||||
if (bypassCache)
|
||||
{
|
||||
// bypass enabled: add a random search parameter to avoid getting a stale HTTP cache result
|
||||
const url = new URL(request.url);
|
||||
url.search += Math.floor(Math.random() * 1000000);
|
||||
|
||||
return fetch(url, {
|
||||
headers: request.headers,
|
||||
mode: request.mode,
|
||||
credentials: request.credentials,
|
||||
redirect: request.redirect,
|
||||
cache: "no-store"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// bypass disabled: perform normal fetch which is allowed to return from HTTP cache
|
||||
return fetch(request);
|
||||
}
|
||||
};
|
||||
|
||||
// Effectively a cache.addAll() that only creates the cache on all requests being successful (as a weak attempt at making it atomic)
|
||||
// and can optionally cache-bypass with fetchWithBypass in every request
|
||||
async function CreateCacheFromFileList(cacheName, fileList, bypassCache)
|
||||
{
|
||||
// Kick off all requests and wait for them all to complete
|
||||
const responses = await Promise.all(fileList.map(url => fetchWithBypass(url, bypassCache)));
|
||||
|
||||
// Check if any request failed. If so don't move on to opening the cache.
|
||||
// This makes sure we only open a cache if all requests succeeded.
|
||||
let allOk = true;
|
||||
|
||||
for (const response of responses)
|
||||
{
|
||||
if (!response.ok)
|
||||
{
|
||||
allOk = false;
|
||||
console.error(CONSOLE_PREFIX + "Error fetching '" + response.url + "' (" + response.status + " " + response.statusText + ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (!allOk)
|
||||
throw new Error("not all resources were fetched successfully");
|
||||
|
||||
// Can now assume all responses are OK. Open a cache and write all responses there.
|
||||
// TODO: ideally we can do this transactionally to ensure a complete cache is written as one atomic operation.
|
||||
// This needs either new transactional features in the spec, or at the very least a way to rename a cache
|
||||
// (so we can write to a temporary name that won't be returned by GetAvailableCacheNames() and then rename it when ready).
|
||||
const cache = await caches.open(cacheName);
|
||||
|
||||
try {
|
||||
return await Promise.all(responses.map(
|
||||
(response, i) => cache.put(fileList[i], response)
|
||||
));
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
// Not sure why cache.put() would fail (maybe if storage quota exceeded?) but in case it does,
|
||||
// clean up the cache to try to avoid leaving behind an incomplete cache.
|
||||
console.error(CONSOLE_PREFIX + "Error writing cache entries: ", err);
|
||||
caches.delete(cacheName);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
async function UpdateCheck(isFirst)
|
||||
{
|
||||
try {
|
||||
// Always bypass cache when requesting offline.js to make sure we find out about new versions.
|
||||
const response = await fetchWithBypass(OFFLINE_DATA_FILE, true);
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error(OFFLINE_DATA_FILE + " responded with " + response.status + " " + response.statusText);
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const version = data.version;
|
||||
const fileList = data.fileList;
|
||||
const lazyLoadList = data.lazyLoad;
|
||||
const currentCacheName = GetCacheVersionName(version);
|
||||
|
||||
const cacheExists = await caches.has(currentCacheName);
|
||||
|
||||
// Don't recache if there is already a cache that exists for this version. Assume it is complete.
|
||||
if (cacheExists)
|
||||
{
|
||||
// Log whether we are up-to-date or pending an update.
|
||||
const isUpdatePending = await IsUpdatePending();
|
||||
if (isUpdatePending)
|
||||
{
|
||||
console.log(CONSOLE_PREFIX + "Update pending");
|
||||
Broadcast("update-pending");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(CONSOLE_PREFIX + "Up to date");
|
||||
Broadcast("up-to-date");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Implicitly add the main page URL to the file list, e.g. "index.html", so we don't have to assume a specific name.
|
||||
const mainPageUrl = await GetMainPageUrl();
|
||||
|
||||
// Prepend the main page URL to the file list if we found one and it is not already in the list.
|
||||
// Also make sure we request the base / which should serve the main page.
|
||||
fileList.unshift("./");
|
||||
|
||||
if (mainPageUrl && fileList.indexOf(mainPageUrl) === -1)
|
||||
fileList.unshift(mainPageUrl);
|
||||
|
||||
console.log(CONSOLE_PREFIX + "Caching " + fileList.length + " files for offline use");
|
||||
|
||||
if (isFirst)
|
||||
Broadcast("downloading");
|
||||
else
|
||||
BroadcastDownloadingUpdate(version);
|
||||
|
||||
// Note we don't bypass the cache on the first update check. This is because SW installation and the following
|
||||
// update check caching will race with the normal page load requests. For any normal loading fetches that have already
|
||||
// completed or are in-flight, it is pointless and wasteful to cache-bust the request for offline caching, since that
|
||||
// forces a second network request to be issued when a response from the browser HTTP cache would be fine.
|
||||
if (lazyLoadList)
|
||||
await WriteLazyLoadListToStorage(lazyLoadList); // dump lazy load list to local storage#
|
||||
|
||||
await CreateCacheFromFileList(currentCacheName, fileList, !isFirst);
|
||||
const isUpdatePending = await IsUpdatePending();
|
||||
|
||||
if (isUpdatePending)
|
||||
{
|
||||
console.log(CONSOLE_PREFIX + "All resources saved, update ready");
|
||||
BroadcastUpdateReady(version);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(CONSOLE_PREFIX + "All resources saved, offline support ready");
|
||||
Broadcast("offline-ready");
|
||||
}
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
// Update check fetches fail when we're offline, but in case there's any other kind of problem with it, log a warning.
|
||||
console.warn(CONSOLE_PREFIX + "Update check failed: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener("install", event =>
|
||||
{
|
||||
// On install kick off an update check to cache files on first use.
|
||||
// If it fails we can still complete the install event and leave the SW running, we'll just
|
||||
// retry on the next navigate.
|
||||
event.waitUntil(
|
||||
UpdateCheck(true) // first update
|
||||
.catch(() => null)
|
||||
);
|
||||
});
|
||||
|
||||
async function GetCacheNameToUse(availableCacheNames, doUpdateCheck)
|
||||
{
|
||||
// Prefer the oldest cache available. This avoids mixed-version responses by ensuring that if a new cache
|
||||
// is created and filled due to an update check while the page is running, we keep returning resources
|
||||
// from the original (oldest) cache only.
|
||||
if (availableCacheNames.length === 1 || !doUpdateCheck)
|
||||
return availableCacheNames[0];
|
||||
|
||||
// We are making a navigate request with more than one cache available. Check if we can expire any old ones.
|
||||
const allClients = await clients.matchAll();
|
||||
|
||||
// If there are other clients open, don't expire anything yet. We don't want to delete any caches they
|
||||
// might be using, which could cause mixed-version responses.
|
||||
if (allClients.length > 1)
|
||||
return availableCacheNames[0];
|
||||
|
||||
// Identify newest cache to use. Delete all the others.
|
||||
const latestCacheName = availableCacheNames[availableCacheNames.length - 1];
|
||||
console.log(CONSOLE_PREFIX + "Updating to new version");
|
||||
|
||||
await Promise.all(
|
||||
availableCacheNames.slice(0, -1)
|
||||
.map(c => caches.delete(c))
|
||||
);
|
||||
|
||||
return latestCacheName;
|
||||
};
|
||||
|
||||
async function HandleFetch(event, doUpdateCheck)
|
||||
{
|
||||
const availableCacheNames = await GetAvailableCacheNames();
|
||||
|
||||
// No caches available: go to network
|
||||
if (!availableCacheNames.length)
|
||||
return fetch(event.request);
|
||||
|
||||
const useCacheName = await GetCacheNameToUse(availableCacheNames, doUpdateCheck);
|
||||
const cache = await caches.open(useCacheName);
|
||||
const cachedResponse = await cache.match(event.request);
|
||||
|
||||
if (cachedResponse)
|
||||
return cachedResponse; // use cached response
|
||||
|
||||
// We need to check if this request is to be lazy-cached. Send the request and load the lazy-load list
|
||||
// from storage simultaneously.
|
||||
const result = await Promise.all([fetch(event.request), ReadLazyLoadListFromStorage()]);
|
||||
const fetchResponse = result[0];
|
||||
const lazyLoadList = result[1];
|
||||
|
||||
if (IsUrlInLazyLoadList(event.request.url, lazyLoadList))
|
||||
{
|
||||
// Handle failure writing to the cache. This can happen if the storage quota is exceeded, which is particularly
|
||||
// likely in Safari 11.1, which appears to have very tight storage limits. Make sure even in the event of an error
|
||||
// we continue to return the response from the fetch.
|
||||
try {
|
||||
// Note clone response since we also respond with it
|
||||
await cache.put(event.request, fetchResponse.clone());
|
||||
}
|
||||
catch (err)
|
||||
{
|
||||
console.warn(CONSOLE_PREFIX + "Error caching '" + event.request.url + "': ", err);
|
||||
}
|
||||
}
|
||||
|
||||
return fetchResponse;
|
||||
};
|
||||
|
||||
self.addEventListener("fetch", event =>
|
||||
{
|
||||
/** NOTE (iain)
|
||||
* This check is to prevent a bug with XMLHttpRequest where if its
|
||||
* proxied with "FetchEvent.prototype.respondWith" no upload progress
|
||||
* events are triggered. By returning we allow the default action to
|
||||
* occur instead. Currently all cross-origin requests fall back to default.
|
||||
*/
|
||||
if (new URL(event.request.url).origin !== location.origin)
|
||||
return;
|
||||
|
||||
// Check for an update on navigate requests
|
||||
const doUpdateCheck = (event.request.mode === "navigate");
|
||||
|
||||
const responsePromise = HandleFetch(event, doUpdateCheck);
|
||||
|
||||
if (doUpdateCheck)
|
||||
{
|
||||
// allow the main request to complete, then check for updates
|
||||
event.waitUntil(
|
||||
responsePromise
|
||||
.then(() => UpdateCheck(false)) // not first check
|
||||
);
|
||||
}
|
||||
|
||||
event.respondWith(responsePromise);
|
||||
});
|
||||
531
RCV_ADinh_QuizArena_K5-09022026/tdv_sdk.js
Normal file
@@ -0,0 +1,531 @@
|
||||
var tdv_sdk = {};
|
||||
tdv_sdk.list = [
|
||||
// --- PART 1: MULTIPLE CHOICE (Q1 - Q10) ---
|
||||
{
|
||||
id: "q31",
|
||||
question:
|
||||
"We see many apricot blossoms at Tet in the South of Vietnam. What color are apricot blossoms usually?",
|
||||
options: ["Blue", "Yellow", "Purple", "Black"],
|
||||
answer: "Yellow",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q33",
|
||||
question:
|
||||
"Square glutinous rice cake is a traditional food at Tet. What is its Vietnamese name?",
|
||||
options: ["Bánh mì", "Bánh tét", "Bánh chưng", "Bánh bao"],
|
||||
answer: "Bánh chưng",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q1",
|
||||
question: "Which subject do students learn about numbers?",
|
||||
options: ["English", "Art", "Math", "Vietnamese"],
|
||||
answer: "Math",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q2",
|
||||
question: "Which subject is about countries and maps?",
|
||||
options: ["Biology", "Geography", "Chemistry", "Physics"],
|
||||
answer: "Geography",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q3",
|
||||
question: "Which sentence is correct?",
|
||||
options: [
|
||||
"Today is Monday.",
|
||||
"Today Monday is.",
|
||||
"Today are Monday.",
|
||||
"Monday today.",
|
||||
],
|
||||
answer: "Today is Monday.",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q4",
|
||||
question: "Which activity do you do when you wake up?",
|
||||
options: ["have a snack", "brush my teeth", "do my homework", "watch TV"],
|
||||
answer: "brush my teeth",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q5",
|
||||
question: "Which activity means 'go to school by bus'?",
|
||||
options: ["walk to school", "catch the bus", "ride my bike", "get dressed"],
|
||||
answer: "catch the bus",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q6",
|
||||
question: "Which word shows order?",
|
||||
options: ["always", "then", "never", "often"],
|
||||
answer: "then",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q7",
|
||||
question: "Which sentence is correct?",
|
||||
options: [
|
||||
"I do my homework every day.",
|
||||
"I do every day my homework.",
|
||||
"I every day do my homework.",
|
||||
"I homework do every day.",
|
||||
],
|
||||
answer: "I do my homework every day.",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q8",
|
||||
question: "Which place can you watch a movie?",
|
||||
options: ["swimming pool", "playground", "movie theater", "sports center"],
|
||||
answer: "movie theater",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q9",
|
||||
question: "Which adverb means '0% of the time?'",
|
||||
options: ["always", "usually", "sometimes", "never"],
|
||||
answer: "never",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q10",
|
||||
question: "Which activity uses a ball?",
|
||||
options: ["watch TV", "walk the dog", "play soccer", "listen to music"],
|
||||
answer: "play soccer",
|
||||
audio: "",
|
||||
},
|
||||
|
||||
// --- PART 2: FILL IN THE BLANKS (Q11 - Q20) ---
|
||||
{
|
||||
id: "q11",
|
||||
question: "First, I take a shower. ___, I get dressed.",
|
||||
options: ["Never", "Then", "Always", "Yesterday"],
|
||||
answer: "Then",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q12",
|
||||
question: "He ___ to school by bus every day.",
|
||||
options: ["walk", "walks", "catch", "catches"],
|
||||
answer: "catches",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q13",
|
||||
question: "Which sentence is correct?",
|
||||
options: [
|
||||
"She brush her teeth every day.",
|
||||
"She brushes her teeth every day.",
|
||||
"She brushing her teeth every day.",
|
||||
"She brush teeth every day.",
|
||||
],
|
||||
answer: "She brushes her teeth every day.",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q14",
|
||||
question: "I usually ___ TV in the evening.",
|
||||
options: ["watch", "watches", "watched", "watching"],
|
||||
answer: "watch",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q15",
|
||||
question: "Which place can you buy food and clothes?",
|
||||
options: ["café", "theatre", "shopping mall", "playground"],
|
||||
answer: "shopping mall",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q16",
|
||||
question: "I ___ go to the skatepark. (only on Sunday)",
|
||||
options: ["alway", "usuallys", "sometimes", "nevers"],
|
||||
answer: "sometimes",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q17",
|
||||
question: "Which food is a drink?",
|
||||
options: ["mushrooms", "cucumber", "lemonade", "garlic"],
|
||||
answer: "lemonade",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q18",
|
||||
question: "Choose the correct sentence.",
|
||||
options: [
|
||||
"I like noodles and rice.",
|
||||
"I like noodle and rice.",
|
||||
"I likes noodles and rice.",
|
||||
"I liking noodles and rice.",
|
||||
],
|
||||
answer: "I like noodles and rice.",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q19",
|
||||
question: "Which vehicle travels in the sky?",
|
||||
options: ["bus", "taxi", "train", "plane"],
|
||||
answer: "plane",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q20",
|
||||
question: "Which country is in Europe?",
|
||||
options: ["Viet Nam", "Thailand", "France", "Singapore"],
|
||||
answer: "France",
|
||||
audio: "",
|
||||
},
|
||||
|
||||
// --- PART 3: LISTENING – TRICKY VERSION (Q21 - Q30) ---
|
||||
// Có kèm Text context và Audio link giả định
|
||||
{
|
||||
id: "q21",
|
||||
question: "What subject does the speaker have today?",
|
||||
options: ["English", "Art", "Math", "Vietnamese"],
|
||||
answer: "Math",
|
||||
text: "Today is Wednesday. I have Math, not English. English is on Thursday.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_today_is_wednesday_i_have_math_not_english_english_fc9add41.mp3",
|
||||
},
|
||||
{
|
||||
id: "q22",
|
||||
question: "What does the speaker do last?",
|
||||
options: ["brush teeth", "get dressed", "walk to school", "catch the bus"],
|
||||
answer: "catch the bus",
|
||||
text: "Every morning, I brush my teeth. Then I get dressed. I catch the bus to school.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_every_morning_i_brush_my_teeth_then_i_get_dressed__909e2d52.mp3",
|
||||
},
|
||||
{
|
||||
id: "q23",
|
||||
question: "When does the speaker do homework?",
|
||||
options: ["after school", "in the morning", "in the evening", "at lunch"],
|
||||
answer: "in the evening",
|
||||
text: "After school, I have a snack. I don't do my homework then. I do it in the evening.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_after_school_i_have_a_snack_i_dont_do_my_homework__5ec48cc2.mp3",
|
||||
},
|
||||
{
|
||||
id: "q24",
|
||||
question: "What does the speaker NEVER do?",
|
||||
options: ["play soccer", "watch TV", "walk the dog", "listen to music"],
|
||||
answer: "walk the dog",
|
||||
text: "I usually play soccer. I sometimes watch TV. I never walk the dog.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_i_usually_play_soccer_i_sometimes_watch_tv_i_never_57a91e92.mp3",
|
||||
},
|
||||
{
|
||||
id: "q25",
|
||||
question: "Where do they go?",
|
||||
options: ["market", "café", "shopping mall", "playground"],
|
||||
answer: "shopping mall",
|
||||
text: "On Sunday, we go to the shopping mall. We don't go to the market. We buy clothes.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_on_sunday_we_go_to_the_shopping_mall_we_dont_go_to_5d9dc1fa.mp3",
|
||||
},
|
||||
{
|
||||
id: "q26",
|
||||
question: "What does the speaker drink?",
|
||||
options: ["soda", "ice tea", "lemonade", "water"],
|
||||
answer: "lemonade",
|
||||
text: "For lunch, I eat noodles and meat. I drink lemonade. I don't drink soda.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_for_lunch_i_eat_noodles_and_meat_i_drink_lemonade__48a1174b.mp3",
|
||||
},
|
||||
{
|
||||
id: "q27",
|
||||
question: "How does the speaker go to school?",
|
||||
options: ["by bike", "by bus", "on foot", "by taxi"],
|
||||
answer: "by bus",
|
||||
text: "I go to school by bus. My sister rides her bike. We don't walk to school.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_i_go_to_school_by_bus_my_sister_rides_her_bike_we__9491b621.mp3",
|
||||
},
|
||||
{
|
||||
id: "q28",
|
||||
question: "Where does the speaker live?",
|
||||
options: ["Australia", "the U.S.A", "Viet Nam", "Singapore"],
|
||||
answer: "Viet Nam",
|
||||
text: "I live in Viet Nam. I want to visit Australia. I don't live there.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_i_live_in_viet_nam_i_want_to_visit_australia_i_don_083cbfae.mp3",
|
||||
},
|
||||
{
|
||||
id: "q29",
|
||||
question: "What does the speaker do in free time?",
|
||||
options: ["watch TV", "cook", "play the guitar", "play soccer"],
|
||||
answer: "play the guitar",
|
||||
text: "In my free time, I play the guitar. I don't watch TV. I listen to music.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_in_my_free_time_i_play_the_guitar_i_dont_watch_tv__f91b06b6.mp3",
|
||||
},
|
||||
{
|
||||
id: "q30",
|
||||
question: "When does the game finish?",
|
||||
options: ["at 2 o'clock", "at 3 o'clock", "at 4 o'clock", "at 5 o'clock"],
|
||||
answer: "at 4 o'clock",
|
||||
text: "The game starts at 3 o'clock. It finishes at 4 o'clock. We go home after that.",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_the_game_starts_at_3_oclock_it_finishes_at_4_ocloc_3e321c1b.mp3",
|
||||
},
|
||||
|
||||
// --- PART 4: TET BACK-UP (Q31 - Q40) ---
|
||||
{
|
||||
id: "q31",
|
||||
question:
|
||||
"We see many apricot blossoms at Tet in the South of Vietnam. What color are apricot blossoms usually?",
|
||||
options: ["Blue", "Yellow", "Purple", "Black"],
|
||||
answer: "Yellow",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q32",
|
||||
question: "At Tet, children get lucky money in a ____.",
|
||||
options: ["white box", "school bag", "red envelope", "paper bag"],
|
||||
answer: "red envelope",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q33",
|
||||
question:
|
||||
"Square glutinous rice cake is a traditional food at Tet. What is its Vietnamese name?",
|
||||
options: ["Bánh mì", "Bánh tét", "Bánh chưng", "Bánh bao"],
|
||||
answer: "Bánh chưng",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q34",
|
||||
question: "People go to the flower market at Tet to ____.",
|
||||
options: ["buy books", "buy flowers", "buy clothes", "buy toys"],
|
||||
answer: "buy flowers",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q35",
|
||||
question: "At Tet, families often visit ____.",
|
||||
options: [
|
||||
"teachers",
|
||||
"classmates",
|
||||
"relatives and friends",
|
||||
"neighbors only",
|
||||
],
|
||||
answer: "relatives and friends",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q36",
|
||||
question: "What does fireworks mean?",
|
||||
options: ["flowers", "lights in the sky", "food", "clothes"],
|
||||
answer: "lights in the sky",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q37",
|
||||
question: "People go to the pagoda at Tet to ____.",
|
||||
options: ["play games", "pray for good luck", "buy food", "watch TV"],
|
||||
answer: "pray for good luck",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q38",
|
||||
question: "Which sentence is correct?",
|
||||
options: [
|
||||
"I get lucky money in red envelope.",
|
||||
"I get lucky money on red envelope.",
|
||||
"I get lucky money from red envelope.",
|
||||
"I get lucky money in a red envelope.",
|
||||
],
|
||||
answer: "I get lucky money in a red envelope.",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q39",
|
||||
question: "Which activity is NOT for Tet?",
|
||||
options: [
|
||||
"Getting lucky money",
|
||||
"Watching fireworks",
|
||||
"Visiting relatives",
|
||||
"Taking an English exam",
|
||||
],
|
||||
answer: "Taking an English exam",
|
||||
audio: "",
|
||||
},
|
||||
{
|
||||
id: "q40",
|
||||
question: "What do they do at night?",
|
||||
options: ["Eat cake", "Visit friends", "Watch fireworks", "Go to school"],
|
||||
answer: "Watch fireworks",
|
||||
text: "What do they do at night?",
|
||||
audio:
|
||||
"https://audio.senaai.tech/audio/Sena_Voice1_at_tet_my_family_eats_square_glutinous_rice_cake_w_b8193d8c.mp3",
|
||||
},
|
||||
];
|
||||
tdv_sdk.currentIndex = 0;
|
||||
tdv_sdk.shuffleArray = function (Array) {
|
||||
for (let i = Array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[Array[i], Array[j]] = [Array[j], Array[i]];
|
||||
}
|
||||
};
|
||||
tdv_sdk.start = function () {
|
||||
tdv_sdk.currentIndex = 0;
|
||||
//tdv_sdk.shuffleArray(tdv_sdk.list);
|
||||
tdv_sdk.listquestion = [];
|
||||
tdv_sdk.totalquestion = 40;
|
||||
for (let i = 0; i < tdv_sdk.list.length; i++) {
|
||||
tdv_sdk.listquestion.push(tdv_sdk.list[i]);
|
||||
}
|
||||
};
|
||||
tdv_sdk.loadquestion = function () {
|
||||
tdv_sdk.curQuestion = tdv_sdk.listquestion[tdv_sdk.currentIndex];
|
||||
tdv_sdk.correctAnswer = tdv_sdk.curQuestion.answer;
|
||||
//tdv_sdk.shuffleArray(tdv_sdk.curQuestion.options);
|
||||
};
|
||||
tdv_sdk.getCurQuestion = function () {
|
||||
return tdv_sdk.curQuestion.question;
|
||||
};
|
||||
tdv_sdk.getCurOptions = function (index) {
|
||||
return tdv_sdk.curQuestion.options[index];
|
||||
};
|
||||
tdv_sdk.getCurCorrectOption = function () {
|
||||
return tdv_sdk.correctAnswer;
|
||||
};
|
||||
tdv_sdk.getCorrectCount = function () {
|
||||
return tdv_sdk.answers;
|
||||
};
|
||||
tdv_sdk.nextQuestion = function () {
|
||||
tdv_sdk.currentIndex++;
|
||||
};
|
||||
tdv_sdk.getCurIndex = function () {
|
||||
return tdv_sdk.currentIndex;
|
||||
};
|
||||
tdv_sdk.getTotalQuestion = function () {
|
||||
return tdv_sdk.list.length;
|
||||
};
|
||||
tdv_sdk.getId = function (index) {
|
||||
return tdv_sdk.list[index].id;
|
||||
};
|
||||
tdv_sdk.getAnswers = function (index) {
|
||||
return tdv_sdk.answers[index];
|
||||
};
|
||||
tdv_sdk.goHome = function () {
|
||||
window.location.href = "../";
|
||||
};
|
||||
|
||||
// Biến lưu trữ đối tượng âm thanh hiện tại để có thể dừng/phát lại
|
||||
tdv_sdk.currentAudioObj = null;
|
||||
|
||||
// Hàm phát audio của câu hỏi hiện tại
|
||||
tdv_sdk.playCurrentAudio = function (speed) {
|
||||
// 0. Xử lý tham số tốc độ: Nếu không truyền hoặc truyền null/undefined thì mặc định là 1.0
|
||||
var playbackSpeed = speed !== undefined && speed !== null ? speed : 1.0;
|
||||
|
||||
// 1. Lấy thông tin câu hỏi hiện tại
|
||||
var curQ = tdv_sdk.listquestion[tdv_sdk.currentIndex];
|
||||
|
||||
// Kiểm tra xem có link audio không
|
||||
if (curQ && curQ.audio) {
|
||||
var audioUrl = curQ.audio;
|
||||
|
||||
// 2. Nếu đang có audio nào chạy thì dừng lại trước khi phát cái mới
|
||||
tdv_sdk.stopAudio();
|
||||
|
||||
// 3. Khởi tạo đối tượng Audio HTML5 mới
|
||||
tdv_sdk.currentAudioObj = new Audio(audioUrl);
|
||||
|
||||
// ===> CẬP NHẬT: Thiết lập tốc độ đọc <===
|
||||
// Giá trị: 1.0 là bình thường, < 1.0 là chậm, > 1.0 là nhanh
|
||||
tdv_sdk.currentAudioObj.playbackRate = playbackSpeed;
|
||||
|
||||
// 4. Bắt lỗi nếu trình duyệt chặn Autoplay hoặc link hỏng
|
||||
var playPromise = tdv_sdk.currentAudioObj.play();
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(function () {
|
||||
// Audio bắt đầu phát thành công
|
||||
console.log(
|
||||
"Audio playing: " + audioUrl + " | Speed: " + playbackSpeed,
|
||||
);
|
||||
})
|
||||
.catch(function (error) {
|
||||
// Lỗi (thường do user chưa tương tác với game)
|
||||
console.error("Audio playback failed:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// (Tùy chọn) Lắng nghe sự kiện khi audio kết thúc
|
||||
tdv_sdk.currentAudioObj.onended = function () {
|
||||
console.log("Audio finished.");
|
||||
// Nếu muốn gọi lại Construct 2 khi hết audio, bạn có thể dùng:
|
||||
// c2_callFunction("OnAudioEnded");
|
||||
};
|
||||
} else {
|
||||
console.log("No audio found for this question.");
|
||||
}
|
||||
};
|
||||
|
||||
// Hàm dừng audio (Dùng khi chuyển câu hỏi hoặc tắt tiếng)
|
||||
tdv_sdk.stopAudio = function () {
|
||||
if (tdv_sdk.currentAudioObj) {
|
||||
tdv_sdk.currentAudioObj.pause();
|
||||
tdv_sdk.currentAudioObj.currentTime = 0; // Tua về đầu
|
||||
tdv_sdk.currentAudioObj = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Thêm vào cuối file tdv_sdk.js
|
||||
tdv_sdk.isEssay = function () {
|
||||
// Nếu mảng options rỗng hoặc không tồn tại -> Là câu tự luận
|
||||
if (
|
||||
!tdv_sdk.curQuestion.options ||
|
||||
tdv_sdk.curQuestion.options.length === 0
|
||||
) {
|
||||
return 1; // True (Trong C2 số 1 là true)
|
||||
}
|
||||
return 0; // False
|
||||
};
|
||||
|
||||
// Kiểm tra xem câu hỏi hiện tại có audio hay không
|
||||
tdv_sdk.hasAudio = function () {
|
||||
var q = tdv_sdk.curQuestion;
|
||||
// Kiểm tra nếu thuộc tính audio tồn tại và có độ dài > 0
|
||||
if (q && q.audio && q.audio.length > 0) {
|
||||
return 1; // Có audio -> Là câu nghe
|
||||
}
|
||||
return 0; // Không có -> Trắc nghiệm thường hoặc Tự luận
|
||||
};
|
||||
// Hàm tính toán scale đồng bộ cho cả 4 đáp án
|
||||
tdv_sdk.getUniformAnswerScale = function () {
|
||||
var opts = tdv_sdk.curQuestion.options;
|
||||
|
||||
// Safety check: Nếu không có options (câu tự luận) trả về 1
|
||||
if (!opts || opts.length === 0) return 1.0;
|
||||
|
||||
// 1. Tìm độ dài của đáp án dài nhất (Max Length)
|
||||
var maxLen = 0;
|
||||
for (var i = 0; i < opts.length; i++) {
|
||||
if (opts[i].length > maxLen) {
|
||||
maxLen = opts[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tính toán Scale dựa trên Max Length
|
||||
// Công thức: Constant / maxLen
|
||||
// Giải thích: Với Scale chuẩn 1.1, giả sử hiển thị đẹp cho khoảng 11-12 ký tự.
|
||||
// Ta chọn hằng số khoảng 13.5.
|
||||
|
||||
var calculatedScale = 11 / maxLen;
|
||||
|
||||
// 3. Clamp (Kẹp) giá trị trong khoảng cho phép
|
||||
// Min: 0.55 (Bé quá không đọc được)
|
||||
// Max: 1.1 (Scale mặc định bạn mong muốn)
|
||||
|
||||
if (calculatedScale > 1.2) calculatedScale = 1.2;
|
||||
if (calculatedScale < 0.7) calculatedScale = 0.7;
|
||||
|
||||
return calculatedScale;
|
||||
};
|
||||