const CONFIG = {
SPREADSHEET_ID: '1ZSoVnh2Eix8lyqWppqq-fRdKeNd5O457MOJnEH076xo',
SENTENCE_SHEET: 'Zinnen',
SETTINGS_SHEET: 'Settings',
FAVORITES_SHEET: 'Favorieten'
};
function doGet() {
return HtmlService
.createTemplateFromFile('Index')
.evaluate()
.setTitle('1000 Zinnen · Presentator')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
function getSpreadsheet_() {
return SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
}
function setupSheets() {
const ss = getSpreadsheet_();
let sentenceSheet = ss.getSheetByName(CONFIG.SENTENCE_SHEET);
if (!sentenceSheet) {
sentenceSheet = ss.insertSheet(CONFIG.SENTENCE_SHEET);
}
if (sentenceSheet.getLastRow() === 0) {
sentenceSheet.getRange(1, 1, 1, 7).setValues([[
'ID',
'Zin',
'Actief',
'Favoriet',
'Notitie',
'Aangemaakt',
'Gewijzigd'
]]);
}
let settingsSheet = ss.getSheetByName(CONFIG.SETTINGS_SHEET);
if (!settingsSheet) {
settingsSheet = ss.insertSheet(CONFIG.SETTINGS_SHEET);
settingsSheet.getRange(1, 1, 1, 3).setValues([[
'Key',
'Value',
'Gewijzigd'
]]);
}
let favoritesSheet = ss.getSheetByName(CONFIG.FAVORITES_SHEET);
if (!favoritesSheet) {
favoritesSheet = ss.insertSheet(CONFIG.FAVORITES_SHEET);
favoritesSheet.getRange(1, 1, 1, 6).setValues([[
'ID',
'Zin',
'Notitie',
'Toegevoegd',
'Naam',
'Telefoon'
]]);
}
return {
ok: true,
message: 'Sheets gecontroleerd en klaargezet.'
};
}
function getSentences() {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return [];
}
const values = sheet.getRange(2, 1, lastRow - 1, 7).getValues();
return values
.map(function(row, index) {
return {
id: Number(row[0]) || index + 1,
text: String(row[1] || '').trim(),
active: row[2] !== false,
favorite: row[3] === true,
note: String(row[4] || ''),
created: row[5] || '',
modified: row[6] || ''
};
})
.filter(function(item) {
return item.text;
});
}
function saveSentence(sentence) {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
const lastRow = sheet.getLastRow();
if (!sentence.id) {
sentence.id = lastRow;
}
const data = sheet.getRange(2, 1, Math.max(1, lastRow - 1), 7).getValues();
let targetRow = -1;
for (let i = 0; i < data.length; i++) {
if (Number(data[i][0]) === Number(sentence.id)) {
targetRow = i + 2;
break;
}
}
const now = new Date();
if (targetRow === -1) {
targetRow = sheet.getLastRow() + 1;
sheet.getRange(targetRow, 1, 1, 7).setValues([[
Number(sentence.id),
sentence.text,
sentence.active !== false,
sentence.favorite === true,
sentence.note || '',
now,
now
]]);
} else {
const existing = sheet.getRange(targetRow, 1, 1, 7).getValues()[0];
sheet.getRange(targetRow, 1, 1, 7).setValues([[
Number(sentence.id),
sentence.text,
sentence.active !== false,
sentence.favorite === true,
sentence.note || '',
existing[5] || now,
now
]]);
}
return {
ok: true,
sentence: sentence
};
}
function updateSentenceText(id, text) {
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
if (!sheet) {
setupSheets();
return updateSentenceText(id, text);
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return false;
}
const ids = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (let i = 0; i < ids.length; i++) {
if (Number(ids[i][0]) === Number(id)) {
sheet.getRange(i + 2, 2).setValue(text);
sheet.getRange(i + 2, 7).setValue(new Date());
return true;
}
}
return false;
}
function setFavorite(id, favorite) {
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
if (!sheet) {
setupSheets();
return setFavorite(id, favorite);
}
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return false;
}
const ids = sheet.getRange(2, 1, lastRow - 1, 1).getValues();
for (let i = 0; i < ids.length; i++) {
if (Number(ids[i][0]) === Number(id)) {
sheet.getRange(i + 2, 4).setValue(Boolean(favorite));
sheet.getRange(i + 2, 7).setValue(new Date());
syncFavorite_(id, favorite);
return true;
}
}
return false;
}
function syncFavorite_(id, favorite) {
const ss = getSpreadsheet_();
const source = ss.getSheetByName(CONFIG.SENTENCE_SHEET);
const target = ss.getSheetByName(CONFIG.FAVORITES_SHEET);
if (!target) {
setupSheets();
return syncFavorite_(id, favorite);
}
const sourceLast = source.getLastRow();
if (sourceLast < 2) {
return;
}
const values = source.getRange(2, 1, sourceLast - 1, 7).getValues();
let sentence = null;
for (let i = 0; i < values.length; i++) {
if (Number(values[i][0]) === Number(id)) {
sentence = values[i];
break;
}
}
if (!sentence) {
return;
}
const targetLast = target.getLastRow();
if (targetLast >= 2) {
const favorites = target.getRange(2, 1, targetLast - 1, 6).getValues();
for (let i = 0; i < favorites.length; i++) {
if (Number(favorites[i][0]) === Number(id)) {
if (!favorite) {
target.deleteRow(i + 2);
}
return;
}
}
}
if (favorite) {
target.appendRow([
sentence[0],
sentence[1],
sentence[4],
new Date(),
'',
''
]);
}
}
function getSettings() {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SETTINGS_SHEET);
const lastRow = sheet.getLastRow();
const defaults = {
name: '',
phone: '',
site: '',
slogan: 'Accepteer nu wat is, verander op jouw manier wat kan.',
note: '',
speed: 650,
repeats: 3,
fontSize: 58,
wordColor: '#ffffff',
background: '#111827',
animation: 'fade',
fullscreen: false
};
if (lastRow < 2) {
return defaults;
}
const values = sheet.getRange(2, 1, lastRow - 1, 2).getValues();
values.forEach(function(row) {
const key = String(row[0] || '');
if (key) {
defaults[key] = row[1];
}
});
return defaults;
}
function saveSettings(settings) {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SETTINGS_SHEET);
const keys = Object.keys(settings);
if (sheet.getLastRow() > 1) {
sheet.getRange(
2,
1,
sheet.getLastRow() - 1,
3
).clearContent();
}
const rows = keys.map(function(key) {
return [
key,
settings[key],
new Date()
];
});
if (rows.length) {
sheet.getRange(2, 1, rows.length, 3).setValues(rows);
}
return true;
}
function seedStructure() {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return {
ok: false,
count: 0,
message: 'De sheet Zinnen is nog leeg. Plak de 1000 zinnen in kolom B en voer daarna normalizeSentences() uit.'
};
}
return normalizeSentences();
}
function normalizeSentences() {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
const lastRow = sheet.getLastRow();
if (lastRow < 2) {
return {
ok: false,
count: 0,
message: 'Geen zinnen gevonden.'
};
}
const values = sheet.getRange(2, 1, lastRow - 1, 7).getValues();
const output = [];
values.forEach(function(row, index) {
const text = String(row[1] || '').trim();
if (!text) {
return;
}
output.push([
index + 1,
text,
row[2] === false ? false : true,
row[3] === true,
row[4] || '',
row[5] || new Date(),
new Date()
]);
});
if (output.length) {
sheet.getRange(2, 1, output.length, 7).setValues(output);
}
if (output.length >= 1000) {
sheet.getRange(1001, 2).setValue(
'Accepteer nu wat is, verander op jouw manier wat kan.'
);
}
return {
ok: true,
count: output.length,
message: output.length + ' zinnen verwerkt.'
};
}
function importSentencesFromText(text) {
setupSheets();
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SENTENCE_SHEET);
const lines = String(text || '')
.split(/\r?\n/)
.map(function(line) {
return line.trim();
})
.filter(function(line) {
return line;
});
if (!lines.length) {
return {
ok: false,
count: 0
};
}
const rows = lines.map(function(line, index) {
return [
index + 1,
line.replace(/^\d+[\.\)]\s*/, ''),
true,
false,
'',
new Date(),
new Date()
];
});
sheet.clearContents();
sheet.getRange(1, 1, 1, 7).setValues([[
'ID',
'Zin',
'Actief',
'Favoriet',
'Notitie',
'Aangemaakt',
'Gewijzigd'
]]);
sheet.getRange(2, 1, rows.length, 7).setValues(rows);
if (rows.length >= 1000) {
sheet.getRange(1001, 2).setValue(
'Accepteer nu wat is, verander op jouw manier wat kan.'
);
}
return {
ok: true,
count: rows.length
};
}
function getFavorites() {
const sheet = getSpreadsheet_().getSheetByName(CONFIG.FAVORITES_SHEET);
if (!sheet || sheet.getLastRow() < 2) {
return [];
}
return sheet
.getRange(2, 1, sheet.getLastRow() - 1, 6)
.getValues()
.map(function(row) {
return {
id: Number(row[0]),
text: String(row[1] || ''),
note: String(row[2] || ''),
added: row[3] || '',
name: String(row[4] || ''),
phone: String(row[5] || '')
};
});
}
```html
<!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/coachingkaarten/nl-to-en";
const WHATSAPP_BOTTOM_CREDIT =
"https://sites.google.com/view/gtktp";
/*
* ---------------------------------------------------------
* 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>
```