<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>GTKTP – Zinnenplayer Mobile</title>
<style>
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
padding: 0;
font-family: Arial, Helvetica, sans-serif;
background: #f5f7fa;
color: #172033;
}
.app {
width: 100%;
max-width: 760px;
margin: 0 auto;
padding: 10px 12px 30px;
}
header {
padding: 8px 4px 12px;
}
.brand {
font-size: 25px;
font-weight: 800;
line-height: 1.1;
}
.subtitle {
margin-top: 4px;
font-size: 13px;
color: #64748b;
}
.status {
margin-top: 8px;
padding: 8px 10px;
border-radius: 10px;
background: #e9eef5;
font-size: 13px;
}
.player {
background: white;
border-radius: 18px;
padding: 18px;
box-shadow: 0 4px 18px rgba(0,0,0,.08);
scroll-margin-top: 8px;
}
.counter {
text-align: center;
color: #64748b;
font-size: 13px;
margin-bottom: 12px;
}
.sentence {
min-height: 145px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-size: clamp(25px, 7vw, 42px);
line-height: 1.2;
font-weight: 700;
padding: 15px 5px 25px;
}
.favorite {
width: 100%;
border: 0;
background: transparent;
font-size: 30px;
padding: 5px;
cursor: pointer;
}
.favorite.active {
transform: scale(1.08);
}
.startButton {
width: 100%;
min-height: 64px;
border: 0;
border-radius: 15px;
font-size: 21px;
font-weight: 800;
cursor: pointer;
margin: 8px 0;
background: #172033;
color: white;
}
.startButton.pause {
background: #b42318;
}
.controls {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 9px;
margin-top: 12px;
}
button.control {
min-height: 48px;
border: 0;
border-radius: 12px;
background: #e8edf3;
color: #172033;
font-size: 15px;
font-weight: 700;
cursor: pointer;
padding: 8px;
}
button.control:active,
.startButton:active {
transform: scale(.98);
}
.settings {
margin-top: 14px;
background: white;
border-radius: 16px;
padding: 15px;
box-shadow: 0 3px 14px rgba(0,0,0,.06);
}
.settings h2 {
margin: 0 0 12px;
font-size: 18px;
}
.setting {
margin-bottom: 13px;
}
label {
display: block;
font-size: 13px;
font-weight: 700;
margin-bottom: 5px;
color: #475569;
}
select,
input[type="number"] {
width: 100%;
min-height: 45px;
border: 1px solid #cbd5e1;
border-radius: 10px;
padding: 8px 10px;
font-size: 16px;
background: white;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 14px;
}
.stat {
background: white;
border-radius: 12px;
padding: 11px 6px;
text-align: center;
box-shadow: 0 2px 9px rgba(0,0,0,.05);
}
.statValue {
font-size: 21px;
font-weight: 800;
}
.statLabel {
font-size: 11px;
color: #64748b;
margin-top: 2px;
}
.favoritesList {
margin-top: 14px;
background: white;
border-radius: 16px;
padding: 15px;
}
.favoritesList h2 {
margin: 0 0 10px;
font-size: 18px;
}
.favoriteItem {
border-bottom: 1px solid #e2e8f0;
padding: 10px 0;
cursor: pointer;
}
.favoriteItem:last-child {
border-bottom: 0;
}
.favoriteId {
font-size: 11px;
color: #64748b;
margin-bottom: 3px;
}
.favoriteText {
font-size: 14px;
line-height: 1.35;
}
.empty {
color: #64748b;
font-size: 14px;
padding: 5px 0;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 12px;
color: #94a3b8;
}
@media (min-width: 600px) {
.app {
padding-top: 18px;
}
.sentence {
min-height: 190px;
}
}
</style>
</head>
<body>
<div class="app">
<header>
<div class="brand">GTKTP</div>
<div class="subtitle">
Together we make life easier · Zinnenplayer
</div>
<div id="status" class="status">
Laden...
</div>
</header>
<!-- START / PAUZE BOVEN -->
<button
id="startTop"
class="startButton"
onclick="togglePlayer()">
▶ Start
</button>
<!-- PLAYER -->
<section id="player" class="player">
<div id="counter" class="counter">
0 / 0
</div>
<div id="sentence" class="sentence">
Zinnen worden geladen...
</div>
<button
id="favoriteButton"
class="favorite"
onclick="toggleFavorite()"
aria-label="Favoriet">
☆
</button>
</section>
<!-- START / PAUZE ONDER -->
<button
id="startBottom"
class="startButton"
onclick="togglePlayer()">
▶ Start
</button>
<!-- BEDIENING -->
<div class="controls">
<button class="control" onclick="previousSentence()">
◀ Vorige
</button>
<button class="control" onclick="nextSentence()">
Volgende ▶
</button>
<button class="control" onclick="showRandomSentence()">
🔀 Random
</button>
<button class="control" onclick="showFavorites()">
★ Favorieten
</button>
<button class="control" onclick="exportJSON()">
📤 Export JSON
</button>
<button
class="control"
onclick="document.getElementById('jsonFile').click()">
📥 Import JSON
</button>
<button
class="control"
onclick="exportCurrentSentenceWhatsApp()">
</button>
<button
class="control"
onclick="copyCurrentSentenceWhatsApp()">
📋 Kopieer WhatsApp
</button>
<input
id="jsonFile"
type="file"
accept=".json,application/json"
style="display:none"
onchange="importJSON(event)">
</div>
<!-- INSTELLINGEN -->
<section class="settings">
<h2>Instellingen</h2>
<div class="setting">
<label for="playMode">
Afspelen
</label>
<select
id="playMode"
onchange="changeMode()">
<option value="all">
Alle zinnen
</option>
<option value="random">
Random
</option>
<option value="favorites">
Alleen favorieten
</option>
</select>
</div>
<div class="setting">
<label for="repeatCount">
Herhalen per zin
</label>
<input
id="repeatCount"
type="number"
min="1"
max="100"
value="1"
onchange="changeRepeat">
</div>
</section>
<!-- STATISTIEKEN -->
<section class="stats">
<div class="stat">
<div
id="totalCount"
class="statValue">
0
</div>
<div class="statLabel">
Zinnen
</div>
</div>
<div class="stat">
<div
id="favoriteCount"
class="statValue">
0
</div>
<div class="statLabel">
Favorieten
</div>
</div>
<div class="stat">
<div
id="playedCount"
class="statValue">
0
</div>
<div class="statLabel">
Gespeeld
</div>
</div>
</section>
<!-- FAVORIETEN -->
<section class="favoritesList">
<h2>
Mijn favorieten
</h2>
<div id="favoritesContainer">
<div class="empty">
Nog geen favorieten.
</div>
</div>
</section>
<div class="footer">
GTKTP · We keep it simple.
</div>
</div>
<script>
let sentences = [];
let currentIndex = 0;
let playing = false;
let timer = null;
let repeatCount = 1;
let currentRepeat = 0;
let mode = "all";
let favorites = new Set();
let playedCount = 0;
/*
* ---------------------------------------------------------
* CREDITS VOOR WHATSAPP
* ---------------------------------------------------------
*/
const WHATSAPP_TOP_CREDIT =
"https://sites.google.com/view/lifecoachstijn";
const WHATSAPP_BOTTOM_CREDIT =
"https://sites.google.com/view/lifecoachstijn/stijn-gabeler";
/*
* ---------------------------------------------------------
* START
* ---------------------------------------------------------
*/
window.addEventListener(
"load",
function() {
loadLocalSettings();
loadSentences();
}
);
/*
* ---------------------------------------------------------
* ZINNEN LADEN
* ---------------------------------------------------------
*/
function loadSentences() {
setStatus(
"Zinnen laden..."
);
if (
typeof google !== "undefined" &&
google.script &&
google.script.run
) {
google.script.run
.withSuccessHandler(
function(data) {
if (!data) {
setStatus(
"Geen zinnen ontvangen."
);
return;
}
if (
typeof data === "string"
) {
try {
data =
JSON.parse(data);
} catch (e) {
console.error(e);
}
}
if (
Array.isArray(data)
) {
sentences =
normalizeSentences(
data
);
} else if (
data.sentences
) {
sentences =
normalizeSentences(
data.sentences
);
}
finishLoading();
}
)
.withFailureHandler(
function(error) {
console.error(error);
setStatus(
"Zinnen konden niet worden geladen."
);
}
)
.getSentences();
} else {
setStatus(
"Deze HTML is niet gekoppeld aan Apps Script."
);
}
}
/*
* ---------------------------------------------------------
* NORMALISEREN
* ---------------------------------------------------------
*/
function normalizeSentences(data) {
return data
.map(
function(item, index) {
if (
typeof item === "string"
) {
return {
id: index + 1,
text: item
};
}
return {
id:
item.id ??
item.ID ??
index + 1,
text:
item.text ??
item.zin ??
item.Zin ??
item.sentence ??
""
};
}
)
.filter(
function(item) {
return (
String(
item.text
).trim() !== ""
);
}
);
}
/*
* ---------------------------------------------------------
* KLAAR
* ---------------------------------------------------------
*/
function finishLoading() {
if (
!sentences.length
) {
setStatus(
"Geen zinnen gevonden."
);
return;
}
document.getElementById(
"totalCount"
).textContent =
sentences.length;
updateFavoriteCount();
updateFavoriteList();
showCurrentSentence();
setStatus(
sentences.length +
" zinnen geladen."
);
}
/*
* ---------------------------------------------------------
* DISPLAY
* ---------------------------------------------------------
*/
function showCurrentSentence() {
if (
!sentences.length
) {
return;
}
const item =
sentences[currentIndex];
document.getElementById(
"sentence"
).textContent =
item.text;
document.getElementById(
"counter"
).textContent =
(
currentIndex + 1
) +
" / " +
sentences.length;
updateFavoriteButton();
}
/*
* ---------------------------------------------------------
* FAVORIET
* ---------------------------------------------------------
*/
function toggleFavorite() {
if (
!sentences.length
) {
return;
}
const id =
String(
sentences[currentIndex].id
);
if (
favorites.has(id)
) {
favorites.delete(id);
} else {
favorites.add(id);
}
saveLocalSettings();
updateFavoriteButton();
updateFavoriteCount();
updateFavoriteList();
}
function updateFavoriteButton() {
if (
!sentences.length
) {
return;
}
const id =
String(
sentences[currentIndex].id
);
const button =
document.getElementById(
"favoriteButton"
);
if (
favorites.has(id)
) {
button.textContent =
"★";
button.classList.add(
"active"
);
} else {
button.textContent =
"☆";
button.classList.remove(
"active"
);
}
}
function updateFavoriteCount() {
document.getElementById(
"favoriteCount"
).textContent =
favorites.size;
}
/*
* ---------------------------------------------------------
* FAVORIETEN LIJST
* ---------------------------------------------------------
*/
function updateFavoriteList() {
const container =
document.getElementById(
"favoritesContainer"
);
if (
!favorites.size
) {
container.innerHTML =
'<div class="empty">' +
'Nog geen favorieten.' +
'</div>';
return;
}
let html = "";
sentences.forEach(
function(item, index) {
if (
!favorites.has(
String(item.id)
)
) {
return;
}
html +=
'<div class="favoriteItem" ' +
'onclick="openFavorite(' +
index +
')">' +
'<div class="favoriteId">' +
"ID " +
item.id +
"</div>" +
'<div class="favoriteText">' +
escapeHTML(
item.text
) +
"</div>" +
"</div>";
}
);
container.innerHTML =
html;
}
function openFavorite(index) {
currentIndex =
index;
showCurrentSentence();
scrollToPlayer();
}
/*
* ---------------------------------------------------------
* VOLGENDE / VORIGE
* ---------------------------------------------------------
*/
function nextSentence() {
if (
!sentences.length
) {
return;
}
const list =
getPlaybackList();
if (
!list.length
) {
setStatus(
"Er zijn geen zinnen beschikbaar in deze selectie."
);
pausePlayer();
return;
}
let position =
list.indexOf(
currentIndex
);
if (
position === -1
) {
position = 0;
} else {
position++;
}
if (
position >= list.length
) {
position = 0;
}
currentIndex =
list[position];
currentRepeat = 0;
playedCount++;
document.getElementById(
"playedCount"
).textContent =
playedCount;
showCurrentSentence();
scrollToPlayer();
}
function previousSentence() {
if (
!sentences.length
) {
return;
}
const list =
getPlaybackList();
if (
!list.length
) {
return;
}
let position =
list.indexOf(
currentIndex
);
if (
position <= 0
) {
position =
list.length - 1;
} else {
position--;
}
currentIndex =
list[position];
currentRepeat = 0;
showCurrentSentence();
scrollToPlayer();
}
/*
* ---------------------------------------------------------
* PLAYBACK
* ---------------------------------------------------------
*/
function getPlaybackList() {
if (
mode === "favorites"
) {
return sentences
.map(
function(item, index) {
return favorites.has(
String(item.id)
)
? index
: null;
}
)
.filter(
function(index) {
return index !== null;
}
);
}
return sentences.map(
function(item, index) {
return index;
}
);
}
function togglePlayer() {
if (
playing
) {
pausePlayer();
} else {
startPlayer();
}
}
function startPlayer() {
if (
!sentences.length
) {
setStatus(
"Er zijn nog geen zinnen geladen."
);
return;
}
if (
mode === "favorites" &&
favorites.size === 0
) {
setStatus(
"Voeg eerst favorieten toe."
);
return;
}
playing = true;
updateStartButtons();
setStatus(
"Afspelen actief."
);
playCurrent();
}
function pausePlayer() {
playing = false;
if (
timer
) {
clearTimeout(timer);
timer = null;
}
if (
"speechSynthesis" in window
) {
window.speechSynthesis.cancel();
}
updateStartButtons();
setStatus(
"Gepauzeerd. Instellingen kunnen nu worden gewijzigd."
);
}
function playCurrent() {
if (
!playing
) {
return;
}
showCurrentSentence();
speakCurrent();
}
/*
* ---------------------------------------------------------
* SPRAAK
* ---------------------------------------------------------
*/
function speakCurrent() {
if (
!("speechSynthesis" in window)
) {
setStatus(
"Tekst-naar-spraak wordt niet ondersteund."
);
return;
}
window.speechSynthesis.cancel();
const item =
sentences[currentIndex];
const utterance =
new SpeechSynthesisUtterance(
item.text
);
utterance.lang =
"nl-NL";
utterance.rate =
0.9;
utterance.onend =
function() {
if (
!playing
) {
return;
}
currentRepeat++;
if (
currentRepeat <
repeatCount
) {
timer =
setTimeout(
playCurrent,
500
);
} else {
currentRepeat = 0;
timer =
setTimeout(
nextSentenceAndPlay,
700
);
}
};
window.speechSynthesis.speak(
utterance
);
}
function nextSentenceAndPlay() {
if (
!playing
) {
return;
}
nextSentence();
playCurrent();
}
/*
* ---------------------------------------------------------
* RANDOM
* ---------------------------------------------------------
*/
function showRandomSentence() {
if (
!sentences.length
) {
return;
}
const list =
getPlaybackList();
if (
!list.length
) {
setStatus(
"Geen zinnen in deze selectie."
);
return;
}
const randomPosition =
Math.floor(
Math.random() *
list.length
);
currentIndex =
list[randomPosition];
currentRepeat = 0;
showCurrentSentence();
scrollToPlayer();
}
/*
* ---------------------------------------------------------
* FAVORIETEN MODE
* ---------------------------------------------------------
*/
function showFavorites() {
mode =
"favorites";
document.getElementById(
"playMode"
).value =
"favorites";
saveLocalSettings();
if (
!favorites.size
) {
setStatus(
"Nog geen favorieten."
);
return;
}
const list =
getPlaybackList();
currentIndex =
list[0];
showCurrentSentence();
setStatus(
favorites.size +
" favorieten geselecteerd."
);
scrollToPlayer();
}
/*
* ---------------------------------------------------------
* INSTELLINGEN
* ---------------------------------------------------------
*/
function changeMode() {
mode =
document.getElementById(
"playMode"
).value;
saveLocalSettings();
if (
mode === "favorites" &&
favorites.size === 0
) {
setStatus(
"Favorietenmodus gekozen, maar er zijn nog geen favorieten."
);
return;
}
setStatus(
"Modus: " +
document.getElementById(
"playMode"
).selectedOptions[0].text
);
}
function changeRepeat() {
let value =
parseInt(
document.getElementById(
"repeatCount"
).value,
10
);
if (
!Number.isFinite(value)
) {
value = 1;
}
value =
Math.max(
1,
Math.min(
100,
value
)
);
repeatCount =
value;
document.getElementById(
"repeatCount"
).value =
value;
saveLocalSettings();
}
/*
* ---------------------------------------------------------
* START BUTTONS
* ---------------------------------------------------------
*/
function updateStartButtons() {
const buttons = [
document.getElementById(
"startTop"
),
document.getElementById(
"startBottom"
)
];
buttons.forEach(
function(button) {
if (
playing
) {
button.textContent =
"⏸ Pauze";
button.classList.add(
"pause"
);
} else {
button.textContent =
"▶ Start";
button.classList.remove(
"pause"
);
}
}
);
}
/*
* ---------------------------------------------------------
* LOCAL OPSLAG
* ---------------------------------------------------------
*/
function saveLocalSettings() {
const data = {
version:
"GTKTP-MOBILE-1.0",
favorites:
Array.from(
favorites
),
mode:
mode,
repeatCount:
repeatCount
};
localStorage.setItem(
"GTKTP_MOBILE_SETTINGS",
JSON.stringify(data)
);
}
function loadLocalSettings() {
const raw =
localStorage.getItem(
"GTKTP_MOBILE_SETTINGS"
);
if (
!raw
) {
return;
}
try {
const data =
JSON.parse(raw);
if (
Array.isArray(
data.favorites
)
) {
favorites =
new Set(
data.favorites.map(
String
)
);
}
if (
data.mode
) {
mode =
data.mode;
const select =
document.getElementById(
"playMode"
);
if (
select
) {
select.value =
mode;
}
}
if (
data.repeatCount &&
Number.isFinite(
Number(
data.repeatCount
)
)
) {
repeatCount =
Math.max(
1,
Math.min(
100,
Number(
data.repeatCount
)
)
);
document.getElementById(
"repeatCount"
).value =
repeatCount;
}
} catch (error) {
console.error(
"Instellingen konden niet worden geladen.",
error
);
}
}
/*
* ---------------------------------------------------------
* JSON EXPORT
* ---------------------------------------------------------
*/
function exportJSON() {
const data = {
gtkp:
"GTKTP",
version:
"1.0",
exported:
new Date().toISOString(),
settings: {
mode:
mode,
repeatCount:
repeatCount
},
favorites:
Array.from(
favorites
),
favoriteSentences:
sentences
.filter(
function(item) {
return favorites.has(
String(item.id)
);
}
)
.map(
function(item) {
return {
id:
item.id,
zin:
item.text
};
}
)
};
const blob =
new Blob(
[
JSON.stringify(
data,
null,
2
)
],
{
type:
"application/json"
}
);
const url =
URL.createObjectURL(
blob
);
const a =
document.createElement(
"a"
);
a.href =
url;
a.download =
"GTKTP-favorieten-" +
dateForFilename() +
".json";
document.body.appendChild(
a
);
a.click();
a.remove();
URL.revokeObjectURL(
url
);
setStatus(
favorites.size +
" favorieten geëxporteerd."
);
}
/*
* ---------------------------------------------------------
* WHATSAPP TEKST MAKEN
* ---------------------------------------------------------
*/
function getCurrentWhatsAppText() {
if (
!sentences.length
) {
return "";
}
const item =
sentences[currentIndex];
const sentence =
String(
item.text || ""
).trim();
if (
!sentence
) {
return "";
}
return (
WHATSAPP_TOP_CREDIT +
"\n\n" +
sentence +
"\n\n" +
WHATSAPP_BOTTOM_CREDIT
);
}
/*
* ---------------------------------------------------------
* WHATSAPP EXPORT
* ---------------------------------------------------------
*/
function exportCurrentSentenceWhatsApp() {
const message =
getCurrentWhatsAppText();
if (
!message
) {
setStatus(
"Er is geen huidige zin om te delen."
);
return;
}
const whatsappURL =
"https://wa.me/?text=" +
encodeURIComponent(
message
);
/*
* Op telefoon/tablet:
* native deelvenster gebruiken.
*/
if (
navigator.share &&
/Android|iPhone|iPad|iPod/i.test(
navigator.userAgent
)
) {
navigator.share({
title:
"GTKTP",
text:
message
})
.then(
function() {
setStatus(
"Zin gedeeld."
);
}
)
.catch(
function(error) {
if (
error &&
error.name !==
"AbortError"
) {
window.open(
whatsappURL,
"_blank"
);
}
}
);
return;
}
/*
* Chromebook / desktop:
* WhatsApp Web openen.
*/
window.open(
whatsappURL,
"_blank"
);
setStatus(
"WhatsApp geopend met de huidige zin."
);
}
/*
* ---------------------------------------------------------
* KOPIEER WHATSAPP
* ---------------------------------------------------------
*/
function copyCurrentSentenceWhatsApp() {
const message =
getCurrentWhatsAppText();
if (
!message
) {
setStatus(
"Er is geen huidige zin om te kopiëren."
);
return;
}
/*
* Moderne Clipboard API
*/
if (
navigator.clipboard &&
window.isSecureContext
) {
navigator.clipboard
.writeText(message)
.then(
function() {
setStatus(
"WhatsApp-tekst gekopieerd."
);
}
)
.catch(
function() {
copyTextFallback(
message
);
}
);
return;
}
/*
* Fallback voor oudere browsers.
*/
copyTextFallback(
message
);
}
function copyTextFallback(text) {
const textarea =
document.createElement(
"textarea"
);
textarea.value =
text;
textarea.style.position =
"fixed";
textarea.style.left =
"-9999px";
textarea.style.top =
"0";
document.body.appendChild(
textarea
);
textarea.focus();
textarea.select();
try {
const successful =
document.execCommand(
"copy"
);
if (
successful
) {
setStatus(
"WhatsApp-tekst gekopieerd."
);
} else {
setStatus(
"Kopiëren is niet gelukt."
);
}
} catch (error) {
console.error(
error
);
setStatus(
"Kopiëren is niet gelukt."
);
}
textarea.remove();
}
/*
* ---------------------------------------------------------
* JSON IMPORT
* ---------------------------------------------------------
*/
function importJSON(event) {
const file =
event.target.files[0];
if (
!file
) {
return;
}
const reader =
new FileReader();
reader.onload =
function(e) {
try {
const data =
JSON.parse(
e.target.result
);
if (
data.favorites &&
Array.isArray(
data.favorites
)
) {
favorites =
new Set(
data.favorites.map(
String
)
);
}
if (
data.settings &&
data.settings.mode
) {
mode =
data.settings.mode;
document.getElementById(
"playMode"
).value =
mode;
}
if (
data.settings &&
data.settings.repeatCount
) {
repeatCount =
Number(
data.settings.repeatCount
);
document.getElementById(
"repeatCount"
).value =
repeatCount;
}
saveLocalSettings();
updateFavoriteCount();
updateFavoriteList();
updateFavoriteButton();
setStatus(
favorites.size +
" favorieten geïmporteerd."
);
} catch (error) {
console.error(error);
setStatus(
"Ongeldig JSON-bestand."
);
}
};
reader.readAsText(
file
);
event.target.value =
"";
}
/*
* ---------------------------------------------------------
* HULPFUNCTIES
* ---------------------------------------------------------
*/
function scrollToPlayer() {
document
.getElementById(
"player"
)
.scrollIntoView({
behavior:
"smooth",
block:
"start"
});
}
function setStatus(text) {
document.getElementById(
"status"
).textContent =
text;
}
function dateForFilename() {
const d =
new Date();
return d
.toISOString()
.slice(
0,
10
);
}
function escapeHTML(text) {
return String(text)
.replace(
/&/g,
"&"
)
.replace(
/</g,
"<"
)
.replace(
/>/g,
">"
)
.replace(
/"/g,
"""
)
.replace(
/'/g,
"'"
);
}
</script>
</body>
</html>
```