Compare commits
16 Commits
v0.1.7
...
v0.2.3-test8
| Author | SHA1 | Date | |
|---|---|---|---|
| b2721c2ec3 | |||
| 1385b3ace0 | |||
| 66a028880c | |||
| 365b12b0eb | |||
| 5ef6747e4d | |||
| 7ce23309f6 | |||
| 6c4b5f4adf | |||
| b8b7b3860b | |||
| 35b19cf140 | |||
| 0b3c8e8a02 | |||
| d890c6e15d | |||
| ccbb3549d1 | |||
| 145c148bbc | |||
| 3402bb1c13 | |||
| 92b66f5b8b | |||
| cd5d92feff |
+101
-11
@@ -15,24 +15,114 @@ jobs:
|
||||
- name: Install Node.js
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y nodejs npm
|
||||
sudo apt-get install -y curl
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Generate Gradle Wrapper
|
||||
run: gradle wrapper
|
||||
|
||||
- name: Grant execute permission
|
||||
run: chmod +x ./gradlew
|
||||
|
||||
- name: Build APK
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-release
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
- name: Locate APK
|
||||
id: locate_apk
|
||||
run: |
|
||||
set -e
|
||||
APK_PATH=$(ls app/build/outputs/apk/release/*.apk | head -n1 || true)
|
||||
if [ -z "$APK_PATH" ]; then
|
||||
echo "No APK found in app/build/outputs/apk/release" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
|
||||
echo "Found $APK_PATH"
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: app-release
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
# Note: uploading artifacts with actions/upload-artifact@v4 is not supported
|
||||
# on some self-hosted/enterprise runners (GHES). We skip storing artifacts
|
||||
# via that action and instead upload the APK directly to the Gitea release
|
||||
# in the steps below.
|
||||
|
||||
- name: Create Gitea release
|
||||
id: create_release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_SERVER: ${{ secrets.GIT_SERVER }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
API_BASE="${GITEA_SERVER}/api/v1"
|
||||
|
||||
# Gitea Actions są kompatybilne z GITHUB_ envs
|
||||
OWNER="${{ github.repository_owner }}"
|
||||
REPO_NAME=$(echo "${{ github.repository }}" | cut -d/ -f2)
|
||||
TAG="${{ github.ref_name }}"
|
||||
|
||||
echo "URL: $API_BASE/repos/$OWNER/$REPO_NAME/releases"
|
||||
echo "OWNER=$OWNER"
|
||||
echo "REPO_NAME=$REPO_NAME"
|
||||
echo "TAG=$TAG"
|
||||
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "Error: TAG (ref_name) is empty. Ensure you pushed a tag."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CREATE_RESPONSE=$(curl -s \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-X POST \
|
||||
-d "{
|
||||
\"tag_name\": \"$TAG\",
|
||||
\"name\": \"$TAG\",
|
||||
\"body\": \"Automated release for $TAG\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}" \
|
||||
"$API_BASE/repos/$OWNER/$REPO_NAME/releases")
|
||||
|
||||
echo "$CREATE_RESPONSE"
|
||||
|
||||
RELEASE_ID=$(echo "$CREATE_RESPONSE" \
|
||||
| grep -o '"id":[0-9]*' \
|
||||
| head -n1 \
|
||||
| cut -d: -f2)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Failed to create release"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "RELEASE_ID=$RELEASE_ID" >> $GITEA_ENV
|
||||
- name: Upload APK to Gitea release
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||
GITEA_SERVER: ${{ secrets.GIT_SERVER }}
|
||||
run: |
|
||||
set -e
|
||||
API_BASE=${GITEA_SERVER:+$GITEA_SERVER/api/v1}
|
||||
API_BASE=${API_BASE:-https://gitea.com/api/v1}
|
||||
REMOTE_URL=$(git config --get remote.origin.url || true)
|
||||
REPO_FULL=$(echo "$REMOTE_URL" | sed -E 's#.*[:/](.+/.+)\.git$#\1#')
|
||||
OWNER=$(echo "$REPO_FULL" | cut -d/ -f1)
|
||||
REPO_NAME=$(echo "$REPO_FULL" | cut -d/ -f2)
|
||||
APK_PATH=${APK_PATH:-$APK_PATH}
|
||||
if [ -z "$APK_PATH" ]; then
|
||||
echo "APK_PATH not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
RELEASE_ID=${RELEASE_ID}
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "RELEASE_ID not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
UPLOAD_URL="$API_BASE/repos/$OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=$(basename $APK_PATH)"
|
||||
echo "Uploading $APK_PATH to $UPLOAD_URL"
|
||||
|
||||
curl --fail -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$APK_PATH" "$UPLOAD_URL"
|
||||
|
||||
+36
-1
@@ -1,3 +1,38 @@
|
||||
.DS_Store
|
||||
./QuizzyTemplate
|
||||
./QuizzyTemplate/*
|
||||
/.QuizzyTemplate
|
||||
|
||||
# Android/Gradle
|
||||
/.gradle/
|
||||
/build/
|
||||
/app/build/
|
||||
/**/build/
|
||||
|
||||
# Gradle wrapper
|
||||
/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Local configuration
|
||||
/local.properties
|
||||
|
||||
# Keystore
|
||||
*.jks
|
||||
|
||||
# Generated APKs/outputs
|
||||
**/outputs/
|
||||
**/apk/**
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# IntelliJ / Android Studio
|
||||
.idea/
|
||||
*.iml
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# NPM / Node
|
||||
node_modules/
|
||||
|
||||
# Misc
|
||||
*.log
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
#Wed May 27 00:12:10 CEST 2026
|
||||
gradle.version=8.5
|
||||
Binary file not shown.
Binary file not shown.
Generated
+65
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="NONE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="51538617-7e5b-4e71-9f47-7bda274cf4cc" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="ClangdSettings">
|
||||
<option name="formatViaClangd" value="false" />
|
||||
</component>
|
||||
<component name="ExternalProjectsManager">
|
||||
<system id="GRADLE">
|
||||
<state>
|
||||
<projects_view>
|
||||
<tree_state>
|
||||
<expand />
|
||||
<select />
|
||||
</tree_state>
|
||||
</projects_view>
|
||||
</state>
|
||||
</system>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 2,
|
||||
"fromUser": false
|
||||
}</component>
|
||||
<component name="ProjectId" id="3EHNE822z12n2x6rUf57MsHEkbE" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||
"android.gradle.sync.needed": "true",
|
||||
"cf.first.check.clang-format": "false",
|
||||
"cidr.known.project.marker": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "/Users/aln/Work/Matma"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="51538617-7e5b-4e71-9f47-7bda274cf4cc" name="Changes" comment="" />
|
||||
<created>1779831422173</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1779831422173</updated>
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
</project>
|
||||
+16
-3
@@ -1,9 +1,12 @@
|
||||
package com.example.app
|
||||
|
||||
import android.os.Bundle
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -13,12 +16,22 @@ class MainActivity : AppCompatActivity() {
|
||||
val webView = WebView(this)
|
||||
|
||||
webView.settings.javaScriptEnabled = true
|
||||
webView.settings.allowFileAccess = true
|
||||
webView.settings.domStorageEnabled = true
|
||||
|
||||
webView.webViewClient = WebViewClient()
|
||||
// Use WebViewAssetLoader to serve files from /assets/ over a secure origin.
|
||||
val assetLoader = WebViewAssetLoader.Builder()
|
||||
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
|
||||
.build()
|
||||
|
||||
webView.loadUrl("file:///android_asset/index.html")
|
||||
webView.webViewClient = object : WebViewClient() {
|
||||
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
|
||||
if (request == null) return null
|
||||
return assetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
}
|
||||
|
||||
// Load the app via the mapped secure origin so fetch() requests are allowed
|
||||
webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
|
||||
|
||||
setContentView(webView)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" packagePrefix="com.example.app" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -13,3 +13,33 @@ Pliki
|
||||
Jak uruchomić
|
||||
|
||||
Otwórz plik `index.html` w przeglądarce (najlepiej na urządzeniu mobilnym lub w trybie responsywnym).
|
||||
|
||||
Signing a release APK/AAB
|
||||
------------------------
|
||||
|
||||
Aby uniknąć ostrzeżeń "nieznany deweloper" lub instalatora na Androidzie, zbuduj podpisany pakiet APK lub AAB i zainstaluj go zamiast debugowego APK.
|
||||
|
||||
1. Utwórz magazyn kluczy (jeśli go nie masz):
|
||||
|
||||
```bash
|
||||
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my_key_alias
|
||||
```
|
||||
|
||||
2. Skopiuj `gradle.properties.example` do swojego osobistego pliku `~/.gradle/gradle.properties` i uzupełnij wartości (nie commituj prawdziwych haseł do kontroli wersji).
|
||||
|
||||
3. Zbuduj podpisany pakiet APK lub AAB:
|
||||
|
||||
```bash
|
||||
./gradlew assembleRelease # podpisany APK (jeśli podpisywanie jest skonfigurowane)
|
||||
./gradlew bundleRelease # AAB do Sklepu Play
|
||||
```
|
||||
|
||||
4. Zainstaluj APK za pomocą adb:
|
||||
|
||||
```bash
|
||||
adb install -r app/build/outputs/apk/release/app-release.apk
|
||||
```
|
||||
|
||||
Lub przesłać AAB do Google Play (testowanie wewnętrzne) — zalecane w celu najłatwiejszej dystrybucji.
|
||||
|
||||
Jeśli chcesz tylko przetestować lokalnie i zobaczyć ostrzeżenie "Nieznane źródła", możesz tymczasowo włączyć instalowanie z nieznanych źródeł na urządzeniu, ale dystrybucja podpisanego wydania jest bezpieczniejszym podejściem.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace 'com.example.app'
|
||||
compileSdk 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.example.app"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// Signing configuration for release builds.
|
||||
// Configure the following properties in either your
|
||||
// - ~/.gradle/gradle.properties (recommended for secrets), or
|
||||
// - <project root>/gradle.properties (do NOT commit passwords)
|
||||
//
|
||||
// Example properties:
|
||||
// MYAPP_STORE_FILE=keystores/my-release-key.jks
|
||||
// MYAPP_STORE_PASSWORD=your_store_password
|
||||
// MYAPP_KEY_ALIAS=my_key_alias
|
||||
// MYAPP_KEY_PASSWORD=your_key_password
|
||||
signingConfigs {
|
||||
release {
|
||||
// Only configure signing when the properties are provided.
|
||||
if (project.hasProperty('MYAPP_STORE_FILE')) {
|
||||
storeFile file(MYAPP_STORE_FILE)
|
||||
storePassword MYAPP_STORE_PASSWORD
|
||||
keyAlias MYAPP_KEY_ALIAS
|
||||
keyPassword MYAPP_KEY_PASSWORD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
// Apply signing config if available. This keeps debug and CI builds
|
||||
// working even when signing properties are absent locally.
|
||||
if (project.hasProperty('MYAPP_STORE_FILE')) {
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
// Ensure release builds are not debuggable.
|
||||
debuggable false
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
|
||||
implementation 'androidx.webkit:webkit:1.8.0'
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,2 @@
|
||||
#- File Locator -
|
||||
listingFile=../../../outputs/apk/debug/output-metadata.json
|
||||
@@ -0,0 +1,2 @@
|
||||
appMetadataVersion=1.1
|
||||
androidGradlePluginVersion=8.2.2
|
||||
@@ -0,0 +1 @@
|
||||
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- Uniwersalny nagłówek aplikacji/substrony -->
|
||||
<header class="subpage-header">
|
||||
<h1 class="subpage-title">{TITLE}</h1>
|
||||
<p class="subpage-subtitle">{SUBTITLE}</p>
|
||||
</header>
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- Uniwersalny pasek postępu -->
|
||||
<div class="progress-bar">
|
||||
<div class="progress-bar-inner" id="{PROGRESS_ID}"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('../../fonts/Inter-ExtraBold.ttf') format('truetype');
|
||||
font-weight: 800;
|
||||
font-style: normal;
|
||||
}
|
||||
body, html {
|
||||
font-family: 'Inter', Arial, sans-serif;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Czytanie</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=202605261" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css?v=202605261">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── LIST SCREEN ── -->
|
||||
<div id="list-wrap" class="app-container">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<main class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Gotowe teksty</h2>
|
||||
<div id="text-list" class="reading-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Własny tekst</h2>
|
||||
<textarea id="custom-input" class="custom-textarea"
|
||||
placeholder="Wklej lub wpisz tutaj swój tekst..."></textarea>
|
||||
<button id="custom-start-btn" class="action-btn">Czytaj ten tekst →</button>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ── READING SCREEN ── -->
|
||||
<div id="read-wrap" class="read-screen-container hidden">
|
||||
<header class="read-header">
|
||||
<button id="read-back-btn" class="read-nav-btn">← Zakończ</button>
|
||||
<span id="read-title" class="read-title-text"></span>
|
||||
<button id="speed-btn" class="read-nav-btn speed-toggle-btn">Pauza</button>
|
||||
</header>
|
||||
<div class="read-progress-bar">
|
||||
<div id="read-progress-bar-inner" class="read-progress-bar-inner"></div>
|
||||
</div>
|
||||
|
||||
<div class="read-viewport" id="read-viewport">
|
||||
<div class="read-text" id="read-text"></div>
|
||||
</div>
|
||||
|
||||
<button id="next-line-btn" class="next-line-btn" aria-label="Następna linia">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/czytanie.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
// Ładowanie nagłówka i stopki
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '📖 Czytanie',
|
||||
'{SUBTITLE}': 'Wybierz tekst do przeczytania lub wklej własny.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Dyktando</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ── LIST SCREEN ── -->
|
||||
<div id="list-wrap" class="app-container">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<main class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Gotowe dyktanda</h2>
|
||||
<div id="text-list" class="reading-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Własny tekst</h2>
|
||||
<textarea id="custom-input" class="custom-textarea"
|
||||
placeholder="Wklej lub wpisz tutaj swój tekst..."></textarea>
|
||||
<button id="custom-start-btn" class="action-btn">Rozpocznij dyktando →</button>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<div id="play-wrap" class="play-screen-container hidden">
|
||||
<header class="play-header">
|
||||
<button id="play-back-btn" class="play-nav-btn">← Zakończ</button>
|
||||
<span id="play-title" class="play-title-text"></span>
|
||||
<span id="blank-progress" class="play-progress"></span>
|
||||
</header>
|
||||
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="play-scroll-area" id="dyk-scroll">
|
||||
<div class="dyk-text-display" id="text-display"></div>
|
||||
|
||||
<div class="summary-view hidden" id="summary">
|
||||
<div id="summary-text" class="summary-text-display"></div>
|
||||
<p id="summary-score" class="summary-score-text"></p>
|
||||
<button id="summary-back-btn" class="action-btn summary-back-btn">
|
||||
Wróć do listy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="choices-bar" id="choices"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/dyktando.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '✏️ Dyktando',
|
||||
'{SUBTITLE}': 'Wybierz dyktando lub wklej własny tekst.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'dyk-progress-bar-inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Nauka Dzielenia</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── SELECT SCREEN ── -->
|
||||
<main id="select-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Wybór dzielnika</h2>
|
||||
<div class="table-grid" id="table-grid">
|
||||
<button class="table-btn" data-val="1">1</button>
|
||||
<button class="table-btn" data-val="2">2</button>
|
||||
<button class="table-btn" data-val="3">3</button>
|
||||
<button class="table-btn" data-val="4">4</button>
|
||||
<button class="table-btn" data-val="5">5</button>
|
||||
<button class="table-btn" data-val="6">6</button>
|
||||
<button class="table-btn" data-val="7">7</button>
|
||||
<button class="table-btn" data-val="8">8</button>
|
||||
<button class="table-btn" data-val="9">9</button>
|
||||
<button class="table-btn" data-val="10">10</button>
|
||||
<button class="table-btn all" data-val="0">Wszystkie</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-panel">
|
||||
<label class="settings-label">
|
||||
Liczba zadań w serii:
|
||||
<input id="total-input" type="number" min="5" max="100" value="20" class="settings-input">
|
||||
</label>
|
||||
</section>
|
||||
<button class="action-btn start-btn" id="start-btn">Rozpocznij ćwiczenie →</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="read-header">
|
||||
<button id="back-btn" class="read-nav-btn">← Zmień</button>
|
||||
<span class="read-title-text">Dzielenie</span>
|
||||
<div class="progress-container">
|
||||
<span id="progress-label" class="progress-text">0/20</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="problem" class="problem-text">?</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit-btn" id="submit">Sprawdź</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<main class="screen-content hidden" id="summary-screen" style="margin-top: 24px;">
|
||||
<div class="summary-view">
|
||||
<div style="font-size: 64px; text-align: center;">🎉</div>
|
||||
<h2 style="text-align: center; font-size: 28px; margin: 16px 0;">Koniec ćwiczenia!</h2>
|
||||
<p id="summary-text" class="summary-score-text"></p>
|
||||
<button class="action-btn summary-back-btn" id="again-btn">Jeszcze raz</button>
|
||||
<button class="action-btn summary-back-btn secondary" id="change-btn">Zmień dzielnik</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/dzielenie.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '÷ Nauka Dzielenia',
|
||||
'{SUBTITLE}': 'Wybierz dzielnik, przez który chcesz dzielić.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress-inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Edu — Główne Menu</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<header class="app-header">
|
||||
<h1 class="app-title">Wybierz kategorię</h1>
|
||||
<p class="app-subtitle">i rozpocznij naukę</p>
|
||||
</header>
|
||||
|
||||
<main class="category-grid">
|
||||
<a href="czytanie.html" class="category-card" style="--grad-start: #3B82F6; --grad-end: #1D4ED8;">
|
||||
<div class="card-icon">📖</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Czytanie</h2>
|
||||
<p class="card-desc">Ćwicz płynność czytania</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="dyktando.html" class="category-card" style="--grad-start: #F59E0B; --grad-end: #D97706;">
|
||||
<div class="card-icon">✏️</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Dyktando</h2>
|
||||
<p class="card-desc">Sprawdź swoją ortografię</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="mnozenie.html" class="category-card" style="--grad-start: #10B981; --grad-end: #059669;">
|
||||
<div class="card-icon">×</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Mnożenie</h2>
|
||||
<p class="card-desc">Tabliczka mnożenia</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="dzielenie.html" class="category-card" style="--grad-start: #8B5CF6; --grad-end: #7C3AED;">
|
||||
<div class="card-icon">÷</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Dzielenie</h2>
|
||||
<p class="card-desc">Powtórka z dzielenia</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="testy.html" class="category-card" style="--grad-start: #EF4444; --grad-end: #DC2626;">
|
||||
<div class="card-icon">📝</div>
|
||||
<div class="card-content">
|
||||
<h2 class="card-title">Testy z matematyki</h2>
|
||||
<p class="card-desc">Sprawdź swoją wiedzę</p>
|
||||
</div>
|
||||
</a>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
|
||||
<script src="js/version.js?v=20260521" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,453 @@
|
||||
// Minimal app logic for math training
|
||||
const state = {
|
||||
ops: new Set(),
|
||||
mode: null, // 'timed' or 'training'
|
||||
score: 0,
|
||||
currentProblem: null,
|
||||
answerBuffer: '',
|
||||
timerId: null,
|
||||
timeLeft: 60,
|
||||
sessionSolved: 0,
|
||||
sessionTarget: 20,
|
||||
settings: {
|
||||
timedSeconds: 60,
|
||||
maxResult: 40,
|
||||
maxOperand: 20,
|
||||
sessionProblems: 20,
|
||||
allowNegative: false,
|
||||
allowFraction: false,
|
||||
}
|
||||
}
|
||||
|
||||
// elements
|
||||
const menuScreen = document.getElementById('menu-screen')
|
||||
const playScreen = document.getElementById('play-screen')
|
||||
const opsContainer = document.getElementById('ops')
|
||||
const modeButtons = document.querySelectorAll('.mode-btn')
|
||||
const opButtons = document.querySelectorAll('.op-btn')
|
||||
const backBtn = document.getElementById('back-btn')
|
||||
const problemEl = document.getElementById('problem')
|
||||
const answerEl = document.getElementById('answer')
|
||||
const feedbackEl = document.getElementById('feedback')
|
||||
const timerEl = document.getElementById('timer')
|
||||
const scoreEl = document.getElementById('score')
|
||||
const keypad = document.querySelectorAll('.key')
|
||||
const submitBtn = document.getElementById('submit')
|
||||
const clearBtn = document.getElementById('clear')
|
||||
const backspaceBtn = document.getElementById('backspace')
|
||||
const historyPanel = document.getElementById('history-panel')
|
||||
const settingTimed = document.getElementById('setting-timed')
|
||||
const settingMaxResult = document.getElementById('setting-max-result')
|
||||
const settingMaxOperand = document.getElementById('setting-max-operand')
|
||||
const settingSessionProblems = document.getElementById('setting-session-problems')
|
||||
const settingAllowNegative = document.getElementById('setting-allow-negative')
|
||||
const settingAllowFraction = document.getElementById('setting-allow-fraction')
|
||||
const saveSettingsBtn = document.getElementById('save-settings')
|
||||
const resetSettingsBtn = document.getElementById('reset-settings')
|
||||
const progressInner = document.getElementById('progress_inner')
|
||||
const dotBtn = document.getElementById('dot')
|
||||
const negateBtn = document.getElementById('negate')
|
||||
const summaryOverlay = document.getElementById('summary-overlay')
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const summaryBack = document.getElementById('summary-back')
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
// load settings from localStorage
|
||||
function loadSettings(){
|
||||
try{
|
||||
const raw = (typeof localStorage !== 'undefined') ? localStorage.getItem('matma:settings') : null
|
||||
if (raw) {
|
||||
const s = JSON.parse(raw)
|
||||
state.settings = Object.assign(state.settings, s)
|
||||
}
|
||||
}catch(e){ console.warn('settings load failed', e) }
|
||||
// reflect to inputs (guard in case some inputs are missing)
|
||||
if (settingTimed) settingTimed.value = state.settings.timedSeconds
|
||||
if (settingMaxResult) settingMaxResult.value = state.settings.maxResult
|
||||
if (settingMaxOperand) settingMaxOperand.value = state.settings.maxOperand
|
||||
if (settingSessionProblems) settingSessionProblems.value = state.settings.sessionProblems
|
||||
if (settingAllowNegative) settingAllowNegative.checked = !!state.settings.allowNegative
|
||||
if (settingAllowFraction) settingAllowFraction.checked = !!state.settings.allowFraction
|
||||
}
|
||||
|
||||
function saveSettings(){
|
||||
state.settings.timedSeconds = Math.max(5, Math.min(600, parseInt(settingTimed.value,10) || 60))
|
||||
state.settings.maxResult = Math.max(1, Math.min(999, parseInt(settingMaxResult.value,10) || 40))
|
||||
state.settings.maxOperand = Math.max(1, Math.min(999, parseInt(settingMaxOperand.value,10) || 20))
|
||||
state.settings.sessionProblems = Math.max(1, Math.min(500, parseInt(settingSessionProblems.value,10) || 20))
|
||||
state.settings.allowNegative = !!settingAllowNegative.checked
|
||||
state.settings.allowFraction = !!settingAllowFraction.checked
|
||||
try{ localStorage.setItem('matma:settings', JSON.stringify(state.settings)) }catch(e){console.warn('save failed',e)}
|
||||
|
||||
// visual feedback
|
||||
const parent = saveSettingsBtn.parentElement;
|
||||
const feedback = document.createElement('span');
|
||||
feedback.textContent = 'Zapisano!';
|
||||
feedback.style.color = '#16a34a';
|
||||
feedback.style.marginLeft = '12px';
|
||||
parent.appendChild(feedback);
|
||||
setTimeout(()=> parent.removeChild(feedback), 2000);
|
||||
}
|
||||
|
||||
function resetSettings(){
|
||||
state.settings = {timedSeconds:60, maxResult:40, maxOperand:20, sessionProblems:20, allowNegative: false, allowFraction: false};
|
||||
saveSettings();
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
saveSettingsBtn.addEventListener('click', saveSettings)
|
||||
resetSettingsBtn.addEventListener('click', resetSettings)
|
||||
|
||||
loadSettings()
|
||||
|
||||
// initial wiring
|
||||
opButtons.forEach(b => {
|
||||
b.addEventListener('click', () => {
|
||||
const op = b.dataset.op
|
||||
if (state.ops.has(op)) { state.ops.delete(op); b.classList.remove('active') }
|
||||
else { state.ops.add(op); b.classList.add('active') }
|
||||
})
|
||||
})
|
||||
|
||||
modeButtons.forEach(b => b.addEventListener('click', () => {
|
||||
if (state.ops.size === 0) {
|
||||
const hint = document.querySelector('.hint-text');
|
||||
hint.style.color = '#dc2626';
|
||||
hint.style.fontWeight = '600';
|
||||
setTimeout(() => {
|
||||
hint.style.color = '';
|
||||
hint.style.fontWeight = '';
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
modeButtons.forEach(x=>x.classList.remove('active'))
|
||||
b.classList.add('active')
|
||||
state.mode = b.dataset.mode
|
||||
startPlay()
|
||||
}))
|
||||
|
||||
backBtn.addEventListener('click', ()=>{
|
||||
stopTimer()
|
||||
playScreen.classList.add('hidden')
|
||||
menuScreen.classList.remove('hidden')
|
||||
feedbackEl.textContent = ''
|
||||
})
|
||||
|
||||
function startPlay(){
|
||||
menuScreen.classList.add('hidden')
|
||||
playScreen.classList.remove('hidden')
|
||||
state.score = 0
|
||||
scoreEl.textContent = state.score
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
feedbackEl.textContent = ''
|
||||
state.currentProblem = generateProblem()
|
||||
renderProblem()
|
||||
|
||||
if (state.mode === 'timed'){
|
||||
if (progressInner) progressInner.style.width = '0%'
|
||||
startTimer(state.settings.timedSeconds)
|
||||
if (statusEl) statusEl.textContent = 'Na czas';
|
||||
if (timerEl) timerEl.classList.remove('hidden');
|
||||
} else {
|
||||
if (progressInner) progressInner.style.width = '0%'
|
||||
state.sessionSolved = 0
|
||||
state.sessionTarget = Math.max(1, state.settings.sessionProblems)
|
||||
updateProgress()
|
||||
if (timerEl) timerEl.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function generateProblem(){
|
||||
const ops = Array.from(state.ops)
|
||||
const op = ops[Math.floor(Math.random()*ops.length)]
|
||||
const maxOp = Math.max(1, state.settings.maxOperand)
|
||||
const maxRes = Math.max(1, state.settings.maxResult)
|
||||
let a = 0, b = 0
|
||||
for (let i=0;i<200;i++){
|
||||
if (op === 'div'){
|
||||
b = randInt(1, maxOp)
|
||||
if (!state.settings.allowFraction){
|
||||
const maxQByOp = Math.floor(maxOp / b)
|
||||
const maxQ = Math.min(maxRes, maxQByOp)
|
||||
if (maxQ < 0) continue
|
||||
const q = randInt(0, maxQ)
|
||||
a = q * b
|
||||
if (state.settings.allowNegative && Math.random() < 0.2) a = -a
|
||||
return {a,b,op}
|
||||
} else {
|
||||
const maxResultNByOp = Math.floor((10 * maxOp) / b)
|
||||
const maxResultN = Math.min(maxRes * 10, maxResultNByOp)
|
||||
if (maxResultN < 0) continue
|
||||
const result_n = randInt(0, maxResultN)
|
||||
if ((result_n * b) % 10 !== 0) continue
|
||||
a = (result_n * b) / 10
|
||||
if (state.settings.allowNegative && Math.random() < 0.2) a = -a
|
||||
return {a,b,op}
|
||||
}
|
||||
} else if (op === 'mul'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (a * b <= maxRes) return {a,b,op}
|
||||
} else if (op === 'add'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (a + b <= maxRes) return {a,b,op}
|
||||
} else if (op === 'sub'){
|
||||
a = randInt(0, maxOp)
|
||||
b = randInt(0, maxOp)
|
||||
if (!state.settings.allowNegative){
|
||||
if (a < b) [a,b] = [b,a]
|
||||
if (a - b <= maxRes) return {a,b,op}
|
||||
} else {
|
||||
if (Math.abs(a - b) <= maxRes) return {a,b,op}
|
||||
}
|
||||
}
|
||||
}
|
||||
a = Math.min(maxOp, Math.floor(maxRes/2))
|
||||
b = Math.min(maxOp, 1)
|
||||
return {a,b,op}
|
||||
}
|
||||
|
||||
function randInt(min,max){
|
||||
return Math.floor(Math.random()*(max-min+1))+min
|
||||
}
|
||||
|
||||
function renderProblem(){
|
||||
const p = state.currentProblem
|
||||
const map = {add: '+', sub: '−', mul: '×', div: '÷'}
|
||||
const symbol = map[p.op] || '?'
|
||||
problemEl.textContent = `${p.a} ${symbol} ${p.b}`
|
||||
}
|
||||
|
||||
function submitAnswer(){
|
||||
const buf = state.answerBuffer.trim()
|
||||
if (buf.length === 0) return
|
||||
const given = parseFloat(buf)
|
||||
const p = state.currentProblem
|
||||
let correct = null
|
||||
if (p.op === 'add') correct = p.a + p.b
|
||||
else if (p.op === 'sub') correct = p.a - p.b
|
||||
else if (p.op === 'mul') correct = p.a * p.b
|
||||
else if (p.op === 'div') correct = (p.b === 0) ? null : (p.a / p.b)
|
||||
const eps = state.settings.allowFraction ? 1e-6 : 1e-9
|
||||
const isCorrect = (correct !== null) && (Math.abs(given - correct) < eps)
|
||||
|
||||
feedbackEl.style.opacity = 1;
|
||||
if (isCorrect) {
|
||||
state.score += 1
|
||||
feedbackEl.textContent = 'Dobrze!'
|
||||
feedbackEl.classList.add('correct');
|
||||
feedbackEl.classList.remove('incorrect');
|
||||
} else {
|
||||
const correctAnswer = Number.isFinite(correct) ? parseFloat(correct.toFixed(2)) : '—';
|
||||
feedbackEl.textContent = `Poprawna odpowiedź: ${correctAnswer}`
|
||||
feedbackEl.classList.add('incorrect');
|
||||
feedbackEl.classList.remove('correct');
|
||||
}
|
||||
scoreEl.textContent = state.score
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
|
||||
if (state.mode === 'training'){
|
||||
state.sessionSolved++
|
||||
updateProgress()
|
||||
if (state.sessionSolved >= state.sessionTarget){
|
||||
saveSessionToHistory()
|
||||
setTimeout(showSummary, 1200);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.opacity = 0;
|
||||
setTimeout(() => {
|
||||
state.currentProblem = generateProblem()
|
||||
renderProblem()
|
||||
}, 200);
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
submitBtn.addEventListener('click', submitAnswer)
|
||||
|
||||
keypad.forEach(k => {
|
||||
k.addEventListener('click', ()=>{
|
||||
const v = k.textContent.trim()
|
||||
if (!/^[0-9]$/.test(v)) return
|
||||
if (state.answerBuffer.length >= 12) return
|
||||
state.answerBuffer += v
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
})
|
||||
|
||||
if (dotBtn){
|
||||
dotBtn.addEventListener('click', ()=>{
|
||||
if (!state.settings.allowFraction) return
|
||||
if (state.answerBuffer.includes('.')) return
|
||||
if (state.answerBuffer.length === 0) state.answerBuffer = '0'
|
||||
state.answerBuffer += '.'
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
}
|
||||
|
||||
if (negateBtn){
|
||||
negateBtn.addEventListener('click', ()=>{
|
||||
if (!state.settings.allowNegative) return
|
||||
if (state.answerBuffer.startsWith('-')) state.answerBuffer = state.answerBuffer.slice(1)
|
||||
else state.answerBuffer = '-' + state.answerBuffer
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
}
|
||||
|
||||
clearBtn.addEventListener('click', ()=>{
|
||||
state.answerBuffer = ''
|
||||
answerEl.textContent = ''
|
||||
})
|
||||
|
||||
backspaceBtn.addEventListener('click', ()=>{
|
||||
state.answerBuffer = state.answerBuffer.slice(0,-1)
|
||||
answerEl.textContent = state.answerBuffer
|
||||
})
|
||||
|
||||
function startTimer(seconds){
|
||||
state.timeLeft = seconds
|
||||
if (timerEl) timerEl.textContent = state.timeLeft
|
||||
|
||||
stopTimer();
|
||||
state.timerId = setInterval(()=>{
|
||||
state.timeLeft -= 1
|
||||
if (timerEl) timerEl.textContent = state.timeLeft
|
||||
const pct = Math.max(0, (state.timeLeft / seconds) * 100)
|
||||
if (progressInner) progressInner.style.width = pct + '%'
|
||||
if (state.timeLeft <= 0) {
|
||||
stopTimer()
|
||||
endSession()
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
|
||||
function stopTimer(){
|
||||
if (state.timerId) clearInterval(state.timerId)
|
||||
state.timerId = null
|
||||
}
|
||||
|
||||
function endSession(){
|
||||
saveSessionToHistory()
|
||||
showSummary();
|
||||
}
|
||||
|
||||
function showSummary(){
|
||||
const total = state.mode === 'timed' ? state.score : state.sessionTarget;
|
||||
const correct = state.score;
|
||||
const pct = total > 0 ? Math.round((correct / total) * 100) : 0;
|
||||
|
||||
if (state.mode === 'timed') {
|
||||
summaryText.textContent = `Koniec czasu! Zdobyłeś ${correct} punktów.`;
|
||||
} else {
|
||||
summaryText.textContent = `Twój wynik: ${correct} / ${total} poprawnie (${pct}%)`;
|
||||
}
|
||||
summaryOverlay.classList.remove('hidden');
|
||||
}
|
||||
|
||||
summaryBack.addEventListener('click', ()=>{
|
||||
summaryOverlay.classList.add('hidden');
|
||||
playScreen.classList.add('hidden');
|
||||
menuScreen.classList.remove('hidden');
|
||||
});
|
||||
|
||||
function updateProgress(){
|
||||
if (state.mode === 'training'){
|
||||
const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100))
|
||||
if (progressInner) progressInner.style.width = pct + '%'
|
||||
if (statusEl) statusEl.textContent = `${state.sessionSolved}/${state.sessionTarget}`
|
||||
}
|
||||
}
|
||||
|
||||
// history persistence
|
||||
function loadHistory(){
|
||||
try{
|
||||
const raw = localStorage.getItem('matma:history')
|
||||
return raw ? JSON.parse(raw) : []
|
||||
}catch(e){ return [] }
|
||||
}
|
||||
|
||||
function saveSessionToHistory(){
|
||||
const h = loadHistory()
|
||||
const sessionData = {
|
||||
ts: Date.now(),
|
||||
mode: state.mode,
|
||||
score: state.score,
|
||||
ops: Array.from(state.ops),
|
||||
duration: state.mode === 'timed' ? state.settings.timedSeconds : null,
|
||||
problems: state.mode === 'training' ? state.sessionTarget : null
|
||||
};
|
||||
h.unshift(sessionData);
|
||||
while(h.length>50) h.pop()
|
||||
try{ localStorage.setItem('matma:history', JSON.stringify(h)) }catch(e){ }
|
||||
}
|
||||
|
||||
function renderHistory(){
|
||||
const h = loadHistory()
|
||||
historyPanel.innerHTML = ''
|
||||
if (h.length === 0) {
|
||||
historyPanel.innerHTML = '<p class="history-empty">Brak zapisanych sesji.</p>'
|
||||
return
|
||||
}
|
||||
h.forEach(item => {
|
||||
const d = new Date(item.ts).toLocaleString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
const modeText = item.mode === 'timed' ? `Na czas (${item.duration}s)` : `Trening (${item.problems} zadań)`;
|
||||
const opsText = item.ops.join(', ').replace('add','+').replace('sub','-').replace('mul','×').replace('div','÷');
|
||||
|
||||
const el = document.createElement('div')
|
||||
el.className = 'history-item'
|
||||
el.innerHTML = `
|
||||
<div class="history-item-main">
|
||||
<span class="history-score">Wynik: ${item.score}</span>
|
||||
<span class="history-ops">${opsText}</span>
|
||||
</div>
|
||||
<div class="history-item-meta">
|
||||
<span>${modeText}</span>
|
||||
<span>${d}</span>
|
||||
</div>`
|
||||
historyPanel.appendChild(el)
|
||||
})
|
||||
}
|
||||
|
||||
// Toggle history panel
|
||||
const historyDetails = historyPanel.closest('details');
|
||||
if (historyDetails) {
|
||||
historyDetails.addEventListener('toggle', () => {
|
||||
if (historyDetails.open) {
|
||||
renderHistory();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// keyboard support
|
||||
window.addEventListener('keydown', (e)=>{
|
||||
if (playScreen.classList.contains('hidden')) return
|
||||
if (/^[0-9]$/.test(e.key)){
|
||||
if (state.answerBuffer.length < 12) {
|
||||
state.answerBuffer += e.key
|
||||
answerEl.textContent = state.answerBuffer
|
||||
}
|
||||
} else if (e.key === 'Backspace'){
|
||||
state.answerBuffer = state.answerBuffer.slice(0,-1)
|
||||
answerEl.textContent = state.answerBuffer
|
||||
} else if (e.key === 'Enter'){
|
||||
submitAnswer()
|
||||
} else if (e.key === '.' || e.key === ',') {
|
||||
dotBtn.click();
|
||||
}
|
||||
})
|
||||
|
||||
// Set default active operations
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
opButtons.forEach(btn => {
|
||||
const op = btn.dataset.op;
|
||||
if (['add', 'sub'].includes(op)) {
|
||||
btn.classList.add('active');
|
||||
state.ops.add(op);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// Komponenty HTML loader
|
||||
function loadComponent(path, replaceMap = {}) {
|
||||
// If path is a full mapped origin used by WebViewAssetLoader, strip it to a relative path.
|
||||
const mappedOrigin = 'https://appassets.androidplatform.net/assets/';
|
||||
if (path.startsWith(mappedOrigin)) {
|
||||
path = path.slice(mappedOrigin.length);
|
||||
}
|
||||
|
||||
// If an absolute path starting with '/' is provided, remove leading slash to make it
|
||||
// relative to the current document (works under local http server and WebViewAssetLoader).
|
||||
if (path.startsWith('/')) path = path.slice(1);
|
||||
|
||||
return fetch(path)
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error('Failed to load component: ' + path);
|
||||
return r.text();
|
||||
})
|
||||
.then(html => {
|
||||
Object.entries(replaceMap).forEach(([key, val]) => {
|
||||
html = html.replaceAll(key, val);
|
||||
});
|
||||
return html;
|
||||
});
|
||||
}
|
||||
|
||||
// Przykład użycia:
|
||||
// loadComponent('components/header.html', {'{TITLE}': 'Tytuł', '{SUBTITLE}': 'Podtytuł'}).then(html => ...)
|
||||
@@ -0,0 +1,142 @@
|
||||
// Nauka Czytania
|
||||
;(function () {
|
||||
// ms between line advances for each speed level (0 = manual)
|
||||
const SPEEDS_MS = [0, 3500, 2000, 1000]
|
||||
const SPEED_LABELS = ['Pauza', 'Wolno', 'Średnio', 'Szybko']
|
||||
|
||||
let yOffset = 0
|
||||
let lineH = 80 // recalculated after render
|
||||
let maxOffset = 0
|
||||
let speedIdx = 0
|
||||
let autoTimer = null
|
||||
|
||||
const listWrap = document.getElementById('list-wrap')
|
||||
const readWrap = document.getElementById('read-wrap')
|
||||
const textList = document.getElementById('text-list')
|
||||
const customInput = document.getElementById('custom-input')
|
||||
const customStartBtn = document.getElementById('custom-start-btn')
|
||||
const readBackBtn = document.getElementById('read-back-btn')
|
||||
const readTitleEl = document.getElementById('read-title')
|
||||
const speedBtn = document.getElementById('speed-btn')
|
||||
const readViewport = document.getElementById('read-viewport')
|
||||
const readTextEl = document.getElementById('read-text')
|
||||
const nextLineBtn = document.getElementById('next-line-btn')
|
||||
let progressBar = null
|
||||
|
||||
// ── Load text list from dyktanda.json ────────────────────────────────────
|
||||
fetch('json/dyktanda.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(item => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'list-item-btn'
|
||||
btn.textContent = item.name
|
||||
btn.addEventListener('click', () => startReading(item.name, item.text))
|
||||
textList.appendChild(btn)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
textList.innerHTML = '<p style="color:var(--muted);padding:8px 0">Nie udało się wczytać tekstów.</p>'
|
||||
})
|
||||
|
||||
customStartBtn.addEventListener('click', () => {
|
||||
const txt = customInput.value.trim()
|
||||
if (!txt) return
|
||||
startReading('Własny', txt)
|
||||
})
|
||||
|
||||
// ── Start reading ─────────────────────────────────────────────────────────
|
||||
function startReading(title, text) {
|
||||
yOffset = 0
|
||||
speedIdx = 0
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
|
||||
if (!readTitleEl || !readTextEl || !speedBtn || !readWrap || !listWrap) {
|
||||
console.warn('czytanie: missing DOM elements', { readTitleEl, readTextEl, speedBtn, readWrap, listWrap })
|
||||
}
|
||||
|
||||
if (readTitleEl) readTitleEl.textContent = title
|
||||
if (readTextEl) readTextEl.textContent = text
|
||||
if (readTextEl) readTextEl.style.transform = 'translateY(0)'
|
||||
if (speedBtn) speedBtn.textContent = SPEED_LABELS[0]
|
||||
if (!progressBar) progressBar = document.getElementById('read-progress-bar-inner')
|
||||
if (progressBar) progressBar.style.width = '0%'
|
||||
|
||||
listWrap.classList.add('hidden')
|
||||
readWrap.classList.remove('hidden')
|
||||
|
||||
// measure real line height after layout
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
const cs = getComputedStyle(readTextEl)
|
||||
const lhVal = cs.lineHeight
|
||||
lineH = (lhVal === 'normal')
|
||||
? parseFloat(cs.fontSize) * 1.35
|
||||
: parseFloat(lhVal)
|
||||
|
||||
maxOffset = Math.max(0, readTextEl.offsetHeight - lineH)
|
||||
updateNextBtn()
|
||||
updateProgressBar()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ── Back to list ──────────────────────────────────────────────────────────
|
||||
// Zakończ button: natychmiast wróć do listy bez potwierdzenia
|
||||
readBackBtn.addEventListener('click', () => {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
readWrap.classList.add('hidden')
|
||||
listWrap.classList.remove('hidden')
|
||||
// Reset speed to manual on exit
|
||||
speedIdx = 0;
|
||||
})
|
||||
|
||||
// ── Speed selector ────────────────────────────────────────────────────────
|
||||
speedBtn.addEventListener('click', () => {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
speedIdx = (speedIdx + 1) % SPEEDS_MS.length
|
||||
speedBtn.textContent = SPEED_LABELS[speedIdx]
|
||||
if (SPEEDS_MS[speedIdx] > 0 && yOffset < maxOffset) {
|
||||
autoTimer = setInterval(advanceLine, SPEEDS_MS[speedIdx])
|
||||
}
|
||||
})
|
||||
|
||||
// ── Manual line advance ───────────────────────────────────────────────────
|
||||
nextLineBtn.addEventListener('click', advanceLine)
|
||||
|
||||
// ── Core scroll logic ─────────────────────────────────────────────────────
|
||||
function advanceLine() {
|
||||
if (yOffset >= maxOffset) {
|
||||
stopAutoAtEnd()
|
||||
return
|
||||
}
|
||||
yOffset = Math.min(yOffset + lineH, maxOffset)
|
||||
readTextEl.style.transform = `translateY(${-yOffset}px)`
|
||||
updateNextBtn()
|
||||
updateProgressBar()
|
||||
if (yOffset >= maxOffset) stopAutoAtEnd()
|
||||
}
|
||||
|
||||
function stopAutoAtEnd() {
|
||||
if (autoTimer !== null) {
|
||||
clearInterval(autoTimer)
|
||||
autoTimer = null
|
||||
speedIdx = 0
|
||||
speedBtn.textContent = SPEED_LABELS[0]
|
||||
}
|
||||
updateNextBtn()
|
||||
}
|
||||
|
||||
function updateNextBtn() {
|
||||
nextLineBtn.disabled = yOffset >= maxOffset
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const progress = maxOffset > 0 ? (yOffset / maxOffset) * 100 : 100
|
||||
if (!progressBar) progressBar = document.getElementById('read-progress-bar-inner')
|
||||
if (progressBar) progressBar.style.width = `${progress}%`
|
||||
}
|
||||
})()
|
||||
@@ -0,0 +1,319 @@
|
||||
// Dyktando — ortografia u/ó, rz/ż, h/ch
|
||||
;(function () {
|
||||
|
||||
// Pairs: key = lowercase match → [choice1, choice2] (shown to user)
|
||||
const PAIRS = {
|
||||
ch: ['ch', 'h'],
|
||||
rz: ['rz', 'ż'],
|
||||
ż: ['rz', 'ż'],
|
||||
ó: ['u', 'ó'],
|
||||
u: ['u', 'ó'],
|
||||
h: ['h', 'ch'],
|
||||
}
|
||||
|
||||
// Order matters: digraphs (ch, rz) must be tried before single chars (h)
|
||||
const BLANK_RE = /ch|rz|[uóżh]/gi
|
||||
|
||||
let tokens = [] // {type:'text',content} | {type:'blank',...}
|
||||
let blanks = [] // subset where type==='blank'
|
||||
let current = 0 // index of active blank
|
||||
let origText = ''
|
||||
|
||||
// ── DOM refs ─────────────────────────────────────────────────────────────
|
||||
const listWrap = document.getElementById('list-wrap')
|
||||
const playWrap = document.getElementById('play-wrap')
|
||||
const textList = document.getElementById('text-list')
|
||||
const customInput = document.getElementById('custom-input')
|
||||
const customStartBtn = document.getElementById('custom-start-btn')
|
||||
const playBackBtn = document.getElementById('play-back-btn')
|
||||
const playTitle = document.getElementById('play-title')
|
||||
const textDisplay = document.getElementById('text-display')
|
||||
const choicesEl = document.getElementById('choices')
|
||||
const progressEl = document.getElementById('blank-progress')
|
||||
const summaryWrap = document.getElementById('summary')
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const summaryScore = document.getElementById('summary-score')
|
||||
const summaryBackBtn = document.getElementById('summary-back-btn')
|
||||
const dykScroll = document.getElementById('dyk-scroll')
|
||||
let progressBar = null
|
||||
|
||||
// ── Load texts ────────────────────────────────────────────────────────────
|
||||
fetch('json/dyktanda.json')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(item => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'list-item-btn'
|
||||
btn.textContent = item.name
|
||||
btn.addEventListener('click', () => startGame(item.name, item.text))
|
||||
textList.appendChild(btn)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
textList.innerHTML = '<p style="color:var(--muted)">Nie udało się wczytać tekstów.</p>'
|
||||
})
|
||||
|
||||
// Poczekaj na dynamiczne załadowanie progress bara
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// progress bar jest ładowany przez loadComponent w dyktando.html
|
||||
const checkProgressBar = () => {
|
||||
const el = document.getElementById('dyk-progress-bar-inner')
|
||||
if (el) {
|
||||
progressBar = el
|
||||
} else {
|
||||
setTimeout(checkProgressBar, 50)
|
||||
}
|
||||
}
|
||||
checkProgressBar()
|
||||
})
|
||||
|
||||
customStartBtn.addEventListener('click', () => {
|
||||
const txt = customInput.value.trim()
|
||||
if (!txt) return
|
||||
startGame('Własny', txt)
|
||||
})
|
||||
|
||||
// ── Start game ────────────────────────────────────────────────────────────
|
||||
function startGame(title, text) {
|
||||
origText = text
|
||||
tokens = []
|
||||
blanks = []
|
||||
current = 0
|
||||
|
||||
// Tokenize: split text into plain segments and blanks
|
||||
BLANK_RE.lastIndex = 0
|
||||
let lastIdx = 0
|
||||
let m
|
||||
while ((m = BLANK_RE.exec(text)) !== null) {
|
||||
if (m.index > lastIdx) {
|
||||
tokens.push({ type: 'text', content: text.slice(lastIdx, m.index) })
|
||||
}
|
||||
const blank = {
|
||||
type: 'blank',
|
||||
id: blanks.length,
|
||||
answer: m[0], // original (preserves case)
|
||||
choices: PAIRS[m[0].toLowerCase()], // pair to show
|
||||
start: m.index, // position in origText
|
||||
end: m.index + m[0].length,
|
||||
userAnswer: null,
|
||||
correct: null,
|
||||
}
|
||||
tokens.push(blank)
|
||||
blanks.push(blank)
|
||||
lastIdx = m.index + m[0].length
|
||||
}
|
||||
if (lastIdx < text.length) {
|
||||
tokens.push({ type: 'text', content: text.slice(lastIdx) })
|
||||
}
|
||||
|
||||
// Reset UI
|
||||
playTitle.textContent = title
|
||||
summaryWrap.classList.add('hidden')
|
||||
textDisplay.classList.remove('hidden')
|
||||
choicesEl.classList.remove('hidden')
|
||||
progressEl.textContent = ''
|
||||
if (progressBar) progressBar.style.width = '0%'
|
||||
|
||||
listWrap.classList.add('hidden')
|
||||
playWrap.classList.remove('hidden')
|
||||
|
||||
if (blanks.length === 0) {
|
||||
renderText()
|
||||
showSummary()
|
||||
} else {
|
||||
renderText()
|
||||
activateBlank(0)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render the text with blanks ───────────────────────────────────────────
|
||||
function renderText() {
|
||||
textDisplay.innerHTML = ''
|
||||
tokens.forEach(tok => {
|
||||
if (tok.type === 'text') {
|
||||
textDisplay.appendChild(document.createTextNode(tok.content))
|
||||
} else {
|
||||
const span = document.createElement('span')
|
||||
span.id = `blank-${tok.id}`
|
||||
|
||||
if (tok.correct !== null) {
|
||||
// Already answered
|
||||
if (tok.correct) {
|
||||
span.className = 'dyk-blank dyk-blank--ok'
|
||||
span.textContent = tok.answer
|
||||
} else {
|
||||
span.className = 'dyk-blank dyk-blank--err'
|
||||
span.innerHTML = `<s>${esc(tok.userAnswer)}</s><sup class="dyk-correction">${esc(tok.answer)}</sup>`
|
||||
}
|
||||
} else if (tok.id === current) {
|
||||
span.className = 'dyk-blank dyk-blank--active'
|
||||
span.textContent = '__'
|
||||
} else {
|
||||
span.className = 'dyk-blank dyk-blank--pending'
|
||||
span.textContent = '__'
|
||||
}
|
||||
|
||||
textDisplay.appendChild(span)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Activate a blank ──────────────────────────────────────────────────────
|
||||
function activateBlank(idx) {
|
||||
current = idx
|
||||
renderText()
|
||||
|
||||
const blank = blanks[idx]
|
||||
|
||||
// Shuffle choices so correct answer isn't always in same position
|
||||
const choices = Math.random() < 0.5
|
||||
? [...blank.choices]
|
||||
: [...blank.choices].reverse()
|
||||
|
||||
choicesEl.innerHTML = ''
|
||||
choices.forEach(choice => {
|
||||
const btn = document.createElement('button')
|
||||
btn.className = 'choice-btn'
|
||||
btn.textContent = choice
|
||||
btn.addEventListener('click', () => handleAnswer(blank, choice))
|
||||
choicesEl.appendChild(btn)
|
||||
})
|
||||
|
||||
progressEl.textContent = `${idx + 1} / ${blanks.length}`
|
||||
|
||||
// Scroll active blank into view
|
||||
requestAnimationFrame(() => {
|
||||
const el = document.getElementById(`blank-${idx}`)
|
||||
if (el) el.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
})
|
||||
|
||||
updateProgressBar()
|
||||
}
|
||||
|
||||
// ── Handle answer ─────────────────────────────────────────────────────────
|
||||
function handleAnswer(blank, choice) {
|
||||
blank.correct = choice.toLowerCase() === blank.answer.toLowerCase()
|
||||
blank.userAnswer = choice
|
||||
|
||||
const nextIdx = blank.id + 1
|
||||
if (nextIdx >= blanks.length) {
|
||||
// Last blank answered: show final state briefly, then summary
|
||||
current = blanks.length
|
||||
choicesEl.innerHTML = ''
|
||||
progressEl.textContent = ''
|
||||
renderText()
|
||||
setTimeout(showSummary, 500)
|
||||
} else {
|
||||
activateBlank(nextIdx)
|
||||
}
|
||||
updateProgressBar()
|
||||
}
|
||||
|
||||
// ── Summary ───────────────────────────────────────────────────────────────
|
||||
function showSummary() {
|
||||
choicesEl.classList.add('hidden')
|
||||
textDisplay.classList.add('hidden')
|
||||
summaryWrap.classList.remove('hidden')
|
||||
|
||||
const total = blanks.length
|
||||
const correct = blanks.filter(b => b.correct).length
|
||||
|
||||
if (total === 0) {
|
||||
summaryScore.textContent = 'Brak liter do uzupełnienia w tym tekście.'
|
||||
summaryText.innerHTML = esc(origText)
|
||||
updateProgressBar()
|
||||
} else {
|
||||
summaryScore.textContent = `Poprawnie: ${correct} z ${total}`
|
||||
summaryText.innerHTML = buildSummaryHTML()
|
||||
}
|
||||
|
||||
dykScroll.scrollTop = 0
|
||||
}
|
||||
|
||||
// Build word-coloured summary HTML
|
||||
function buildSummaryHTML() {
|
||||
// Find word spans (non-whitespace sequences) in original text
|
||||
const wordRe = /\S+/g
|
||||
const words = []
|
||||
let wm
|
||||
while ((wm = wordRe.exec(origText)) !== null) {
|
||||
words.push({ start: wm.index, end: wm.index + wm[0].length })
|
||||
}
|
||||
|
||||
// Assign each word its blanks and overall status
|
||||
words.forEach(w => {
|
||||
w.blanks = blanks.filter(b => b.start >= w.start && b.end <= w.end)
|
||||
w.status = w.blanks.length === 0 ? 'none'
|
||||
: w.blanks.every(b => b.correct) ? 'ok' : 'err'
|
||||
})
|
||||
|
||||
let html = ''
|
||||
let pos = 0
|
||||
|
||||
words.forEach(word => {
|
||||
// Whitespace / punctuation before this word
|
||||
if (word.start > pos) html += esc(origText.slice(pos, word.start))
|
||||
|
||||
// Build inner word HTML (blanks replaced by correct answers, coloured)
|
||||
let wPos = word.start
|
||||
let inner = ''
|
||||
word.blanks.forEach(blank => {
|
||||
if (blank.start > wPos) inner += esc(origText.slice(wPos, blank.start))
|
||||
if (blank.correct) {
|
||||
inner += `<span class="dyk-word--ok">${esc(blank.answer)}</span>`
|
||||
} else {
|
||||
inner += `<span class="dyk-word--err"><s>${esc(blank.userAnswer)}</s><sup class="dyk-correction">${esc(blank.answer)}</sup></span>`
|
||||
}
|
||||
wPos = blank.end
|
||||
})
|
||||
inner += esc(origText.slice(wPos, word.end))
|
||||
|
||||
html += word.status !== 'none'
|
||||
? `<span class="dyk-word--${word.status}">${inner}</span>`
|
||||
: inner
|
||||
|
||||
pos = word.end
|
||||
})
|
||||
|
||||
if (pos < origText.length) html += esc(origText.slice(pos))
|
||||
return html
|
||||
}
|
||||
|
||||
function updateProgressBar() {
|
||||
const total = blanks.length
|
||||
if (total === 0) {
|
||||
progressBar.style.width = '100%'
|
||||
return
|
||||
}
|
||||
// Use `current` which is the index of the *next* blank to be filled.
|
||||
// When the game ends, `current` becomes `blanks.length`.
|
||||
const progress = (current / total) * 100
|
||||
progressBar.style.width = `${progress}%`
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
return (s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────
|
||||
// Zakończ button: natychmiast wracamy do listy bez potwierdzenia
|
||||
playBackBtn.addEventListener('click', () => {
|
||||
goToList()
|
||||
})
|
||||
|
||||
summaryBackBtn.addEventListener('click', goToList)
|
||||
|
||||
function goToList() {
|
||||
tokens = []
|
||||
blanks = []
|
||||
current = 0
|
||||
textDisplay.innerHTML = ''
|
||||
textDisplay.classList.remove('hidden')
|
||||
summaryWrap.classList.add('hidden')
|
||||
choicesEl.innerHTML = ''
|
||||
choicesEl.classList.remove('hidden')
|
||||
playWrap.classList.add('hidden')
|
||||
listWrap.classList.remove('hidden')
|
||||
}
|
||||
|
||||
})()
|
||||
@@ -0,0 +1,150 @@
|
||||
// Nauka Dzielenia
|
||||
;(function () {
|
||||
const st = { divisor: null, total: 20, solved: 0, score: 0, current: null, buf: '' }
|
||||
|
||||
const selectScreen = document.getElementById('select-screen')
|
||||
const playScreen = document.getElementById('play-screen')
|
||||
const summaryScreen = document.getElementById('summary-screen')
|
||||
const problemEl = document.getElementById('problem')
|
||||
const answerEl = document.getElementById('answer')
|
||||
const feedbackEl = document.getElementById('feedback')
|
||||
let progressInner = null
|
||||
const progressLabel = document.getElementById('progress-label')
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const totalInput = document.getElementById('total-input')
|
||||
|
||||
// divisor selection
|
||||
document.getElementById('table-grid').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.table-btn')
|
||||
if (!btn) return
|
||||
document.querySelectorAll('.table-btn').forEach(b => b.classList.remove('active'))
|
||||
btn.classList.add('active')
|
||||
st.divisor = parseInt(btn.dataset.val, 10) // 0 = all
|
||||
})
|
||||
|
||||
document.getElementById('start-btn').addEventListener('click', () => {
|
||||
st.total = Math.max(5, Math.min(100, parseInt(totalInput.value, 10) || 20))
|
||||
st.solved = 0
|
||||
st.score = 0
|
||||
show(playScreen)
|
||||
nextProblem()
|
||||
})
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => show(selectScreen))
|
||||
document.getElementById('again-btn').addEventListener('click', () => {
|
||||
st.solved = 0; st.score = 0; show(playScreen); nextProblem()
|
||||
})
|
||||
document.getElementById('change-btn').addEventListener('click', () => show(selectScreen))
|
||||
|
||||
// keypad
|
||||
document.querySelectorAll('.key').forEach(k => {
|
||||
k.addEventListener('click', () => {
|
||||
const v = k.textContent.trim()
|
||||
if (!/^[0-9]$/.test(v)) return
|
||||
if (st.buf.length >= 6) return
|
||||
st.buf += v; answerEl.textContent = st.buf
|
||||
})
|
||||
})
|
||||
document.getElementById('clear').addEventListener('click', () => { st.buf = ''; answerEl.textContent = '' })
|
||||
document.getElementById('backspace').addEventListener('click', () => { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' })
|
||||
document.getElementById('submit').addEventListener('click', submit)
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (!playScreen.classList.contains('hidden')) {
|
||||
if (/^[0-9]$/.test(e.key) && st.buf.length < 6) { st.buf += e.key; answerEl.textContent = st.buf }
|
||||
else if (e.key === 'Backspace') { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' }
|
||||
else if (e.key === 'Enter') submit()
|
||||
}
|
||||
})
|
||||
|
||||
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min }
|
||||
|
||||
function nextProblem() {
|
||||
const b = st.divisor || randInt(1, 10) // divisor
|
||||
const answer = randInt(1, 10) // quotient (always integer)
|
||||
const a = b * answer // dividend
|
||||
st.current = { a, b, answer }
|
||||
problemEl.textContent = `${a} ÷ ${b}`
|
||||
feedbackEl.textContent = ''
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!st.buf.trim()) return
|
||||
const given = parseInt(st.buf, 10)
|
||||
st.solved++
|
||||
if (given === st.current.answer) {
|
||||
st.score++
|
||||
feedbackEl.textContent = 'Dobrze!'
|
||||
feedbackEl.classList.add('correct')
|
||||
feedbackEl.classList.remove('incorrect')
|
||||
} else {
|
||||
feedbackEl.textContent = `Poprawna odpowiedź: ${st.current.answer}`
|
||||
feedbackEl.classList.add('incorrect')
|
||||
feedbackEl.classList.remove('correct')
|
||||
}
|
||||
feedbackEl.style.opacity = 1;
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
if (st.solved >= st.total) {
|
||||
setTimeout(showSummary, 1200)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.opacity = 0;
|
||||
setTimeout(nextProblem, 200);
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function updateBar() {
|
||||
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
|
||||
if (!progressInner) {
|
||||
progressInner = document.getElementById('progress-inner')
|
||||
}
|
||||
if (progressInner) progressInner.style.width = pct + '%'
|
||||
progressLabel.textContent = `${st.solved}/${st.total}`
|
||||
}
|
||||
|
||||
function showSummary() {
|
||||
const pct = st.total > 0 ? Math.round((st.score / st.total) * 100) : 0
|
||||
summaryText.textContent = `Twój wynik: ${st.score} / ${st.total} poprawnie (${pct}%)`
|
||||
show(summaryScreen)
|
||||
}
|
||||
|
||||
function show(screen) {
|
||||
[selectScreen, playScreen, summaryScreen].forEach(s => s.classList.add('hidden'))
|
||||
screen.classList.remove('hidden')
|
||||
if (screen === summaryScreen) {
|
||||
selectScreen.classList.add('hidden');
|
||||
playScreen.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// commit SHA and default selection
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const el = document.getElementById('commit-sha')
|
||||
if (!el) return
|
||||
|
||||
// Set default divisor selection
|
||||
const defaultDivisorBtn = document.querySelector('.table-btn[data-val="0"]');
|
||||
if(defaultDivisorBtn) {
|
||||
defaultDivisorBtn.classList.add('active');
|
||||
st.divisor = 0;
|
||||
}
|
||||
|
||||
let sha = (window.COMMIT_SHA || '').toString().trim()
|
||||
if (!sha) {
|
||||
try {
|
||||
// use relative path so the request works under WebViewAssetLoader or a local server
|
||||
const res = await fetch('version.sha', { cache: 'no-cache' })
|
||||
if (res.ok) {
|
||||
const txt = await res.text()
|
||||
const first = txt.split(/\r?\n/).find(l => l.trim().length > 0)
|
||||
if (first) sha = first.trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (sha) el.textContent = sha.slice(0, 8)
|
||||
})
|
||||
})()
|
||||
@@ -0,0 +1,150 @@
|
||||
// Nauka Mnożenia
|
||||
;(function () {
|
||||
const st = { table: null, total: 20, solved: 0, score: 0, current: null, buf: '' }
|
||||
|
||||
const selectScreen = document.getElementById('select-screen')
|
||||
const playScreen = document.getElementById('play-screen')
|
||||
const summaryScreen = document.getElementById('summary-screen')
|
||||
const problemEl = document.getElementById('problem')
|
||||
const answerEl = document.getElementById('answer')
|
||||
const feedbackEl = document.getElementById('feedback')
|
||||
let progressInner = null
|
||||
const progressLabel = document.getElementById('progress-label')
|
||||
const scoreLabel = document.getElementById('score-label') // This element is removed, but we might re-purpose the logic
|
||||
const summaryText = document.getElementById('summary-text')
|
||||
const totalInput = document.getElementById('total-input')
|
||||
|
||||
// table selection
|
||||
document.getElementById('table-grid').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.table-btn')
|
||||
if (!btn) return
|
||||
document.querySelectorAll('.table-btn').forEach(b => b.classList.remove('active'))
|
||||
btn.classList.add('active')
|
||||
st.table = parseInt(btn.dataset.val, 10) // 0 = all
|
||||
})
|
||||
|
||||
document.getElementById('start-btn').addEventListener('click', () => {
|
||||
st.total = Math.max(5, Math.min(100, parseInt(totalInput.value, 10) || 20))
|
||||
st.solved = 0
|
||||
st.score = 0
|
||||
show(playScreen)
|
||||
nextProblem()
|
||||
})
|
||||
|
||||
document.getElementById('back-btn').addEventListener('click', () => show(selectScreen))
|
||||
document.getElementById('again-btn').addEventListener('click', () => {
|
||||
st.solved = 0; st.score = 0; show(playScreen); nextProblem()
|
||||
})
|
||||
document.getElementById('change-btn').addEventListener('click', () => show(selectScreen))
|
||||
|
||||
// keypad
|
||||
document.querySelectorAll('.key').forEach(k => {
|
||||
k.addEventListener('click', () => {
|
||||
const v = k.textContent.trim()
|
||||
if (!/^[0-9]$/.test(v)) return
|
||||
if (st.buf.length >= 6) return
|
||||
st.buf += v; answerEl.textContent = st.buf
|
||||
})
|
||||
})
|
||||
document.getElementById('clear').addEventListener('click', () => { st.buf = ''; answerEl.textContent = '' })
|
||||
document.getElementById('backspace').addEventListener('click', () => { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' })
|
||||
document.getElementById('submit').addEventListener('click', submit)
|
||||
|
||||
window.addEventListener('keydown', e => {
|
||||
if (!playScreen.classList.contains('hidden')) {
|
||||
if (/^[0-9]$/.test(e.key) && st.buf.length < 6) { st.buf += e.key; answerEl.textContent = st.buf }
|
||||
else if (e.key === 'Backspace') { st.buf = st.buf.slice(0,-1); answerEl.textContent = st.buf || '' }
|
||||
else if (e.key === 'Enter') submit()
|
||||
}
|
||||
})
|
||||
|
||||
function randInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min }
|
||||
|
||||
function nextProblem() {
|
||||
const a = st.table || randInt(1, 10)
|
||||
const b = randInt(1, 10)
|
||||
st.current = { a, b, answer: a * b }
|
||||
problemEl.textContent = `${a} × ${b}`
|
||||
feedbackEl.textContent = ''
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (!st.buf.trim()) return
|
||||
const given = parseInt(st.buf, 10)
|
||||
st.solved++
|
||||
if (given === st.current.answer) {
|
||||
st.score++
|
||||
feedbackEl.textContent = 'Dobrze!'
|
||||
feedbackEl.classList.add('correct')
|
||||
feedbackEl.classList.remove('incorrect')
|
||||
} else {
|
||||
feedbackEl.textContent = `Poprawna odpowiedź: ${st.current.answer}`
|
||||
feedbackEl.classList.add('incorrect')
|
||||
feedbackEl.classList.remove('correct')
|
||||
}
|
||||
feedbackEl.style.opacity = 1;
|
||||
st.buf = ''; answerEl.textContent = ''
|
||||
updateBar()
|
||||
if (st.solved >= st.total) {
|
||||
setTimeout(showSummary, 1200)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
feedbackEl.style.opacity = 0;
|
||||
setTimeout(nextProblem, 200);
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
function updateBar() {
|
||||
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
|
||||
if (!progressInner) progressInner = document.getElementById('progress-inner')
|
||||
if (progressInner) progressInner.style.width = pct + '%'
|
||||
progressLabel.textContent = `${st.solved}/${st.total}`
|
||||
// scoreLabel is removed, so we comment this out
|
||||
// scoreLabel.textContent = `✔ ${st.score}`
|
||||
}
|
||||
|
||||
function showSummary() {
|
||||
const pct = st.total > 0 ? Math.round((st.score / st.total) * 100) : 0
|
||||
summaryText.textContent = `Twój wynik: ${st.score} / ${st.total} poprawnie (${pct}%)`
|
||||
show(summaryScreen)
|
||||
}
|
||||
|
||||
function show(screen) {
|
||||
console.log(`Showing screen: ${screen.id}`);
|
||||
[selectScreen, playScreen, summaryScreen].forEach(s => s.classList.add('hidden'))
|
||||
screen.classList.remove('hidden')
|
||||
if (screen === summaryScreen) {
|
||||
selectScreen.classList.add('hidden');
|
||||
playScreen.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// commit SHA
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const el = document.getElementById('commit-sha')
|
||||
if (!el) return
|
||||
// Set default table selection
|
||||
const defaultTableBtn = document.querySelector('.table-btn[data-val="0"]');
|
||||
if(defaultTableBtn) {
|
||||
defaultTableBtn.classList.add('active');
|
||||
st.table = 0;
|
||||
}
|
||||
|
||||
let sha = (window.COMMIT_SHA || '').toString().trim()
|
||||
if (!sha) {
|
||||
try {
|
||||
// use relative path so the request works under WebViewAssetLoader or a local server
|
||||
const res = await fetch('version.sha', { cache: 'no-cache' })
|
||||
if (res.ok) {
|
||||
const txt = await res.text()
|
||||
const first = txt.split(/\r?\n/).find(l => l.trim().length > 0)
|
||||
if (first) sha = first.trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (sha) el.textContent = sha.slice(0, 8)
|
||||
})
|
||||
})()
|
||||
@@ -0,0 +1,13 @@
|
||||
// Back-to-hub button with active-task confirmation
|
||||
(function () {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const backBtn = document.getElementById('back-to-hub')
|
||||
if (!backBtn) return
|
||||
|
||||
// Immediately navigate back to hub/menu without confirmation
|
||||
backBtn.addEventListener('click', (e) => {
|
||||
const href = backBtn.getAttribute('href')
|
||||
if (href) window.location.href = href
|
||||
})
|
||||
})
|
||||
})()
|
||||
@@ -0,0 +1 @@
|
||||
const COMMIT_SHA = '-';
|
||||
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"name": "Wiosenna wycieczka",
|
||||
"text": "W sobotę Krzyś i Jurek pojechali z mamą do lasu. Wśród dużych drzew rosły żółte kwiaty i czerwone grzyby. Chłopcy słuchali śpiewu ptaków i szumu strumyka. Później zjedli bułki, jabłka i gorącą herbatę."
|
||||
},
|
||||
{
|
||||
"name": "Na podwórku",
|
||||
"text": "Hania huśtała się na huśtawce obok wysokiego płotu. Jej koleżanka Róża jeździła na różowym rowerze. Na chodniku bawiły się trzy małe kotki. Wieczorem dziewczynki wróciły do domu bardzo wesołe."
|
||||
},
|
||||
{
|
||||
"name": "W kuchni babci",
|
||||
"text": "Babcia upiekła pyszne drożdżowe bułeczki z różą i jabłkami. W dużym garnku gotowała się zupa jarzynowa. Henio pomagał kroić marchewkę i ogórki. Po obiedzie wszyscy chrupali kruche ciasteczka."
|
||||
},
|
||||
{
|
||||
"name": "Zimowy poranek",
|
||||
"text": "W grudniu spadł puszysty śnieg i zrobiło się chłodno. Przed szkołą dzieci lepiły dużego bałwana. Grześ rzucał śnieżkami, a Hela ciągnęła sanki. Po zabawie wszyscy pili gorącą czekoladę."
|
||||
},
|
||||
{
|
||||
"name": "W zoo",
|
||||
"text": "W warszawskim zoo Zuzia zobaczyła żyrafę i małego żubra. Obok wybiegów chodziły pawie i hałaśliwe papugi. Pan przewodnik opowiadał ciekawostki o zwierzętach. Na końcu wycieczki dzieci kupiły pamiątkowe magnesy."
|
||||
},
|
||||
{
|
||||
"name": "Na plaży",
|
||||
"text": "Latem Ania i Kuba spędzali czas na plaży nad morzem. Budowali zamki z piasku i zbierali kolorowe muszelki. Woda była ciepła, więc często pływali i pluskali się. Wieczorem oglądali zachód słońca i jedli lody."
|
||||
},
|
||||
{
|
||||
"name": "* Wycieczka do gospodarstwa",
|
||||
"text": "W środę klasa trzecia pojechała na wycieczkę do wiejskiego gospodarstwa. Dzieci jechały autobusem przez zielone pola i wzgórza. Na miejscu przywitał je gospodarz w słomkowym kapeluszu. Pokazał uczniom dużą stodołę, kurnik oraz ogród pełen warzyw. Później dzieci pomagały zbierać dojrzałe ogórki i marchewki. W sadzie rosły grusze, jabłonie i śliwy. Na podwórku chodziły kury, gęsi i małe żółte kurczątka. Największą atrakcją była przejażdżka starym wozem ciągniętym przez spokojnego konia. Po południu wszyscy usiedli przy długim stole i jedli chrupiące bułki z twarożkiem. Zmęczone, ale szczęśliwe dzieci wróciły do domu z dobrymi humorami."
|
||||
},
|
||||
{
|
||||
"name": "* Wizyta w bibliotece",
|
||||
"text": "W poniedziałek klasa trzecia wybrała się na wycieczkę do miejskiej biblioteki. Dzieci jechały autobusem przez centrum miasta, mijając wysokie budynki i kolorowe sklepy. Na miejscu przywitała ich pani bibliotekarka, która zaprosiła ich do środka. Biblioteka była ogromna, z wieloma regałami pełnymi książek. Dzieci mogły wybierać spośród różnych gatunków literackich, od bajek po powieści przygodowe. Pani bibliotekarka opowiedziała im o historii biblioteki i pokazała, jak korzystać z katalogu. Po krótkiej prezentacji dzieci miały czas na samodzielne przeglądanie książek i czytanie w specjalnej strefie dla dzieci. Na koniec wszyscy wypożyczyli swoje ulubione książki i wrócili do domu z uśmiechami na twarzach."
|
||||
},
|
||||
{
|
||||
"name": "* Deszczowy dzień",
|
||||
"text": "Od rana na niebie krążyły ciemne chmury. Chwilę później zaczął padać ulewny deszcz. Krople głośno uderzały o dach i szyby. Hania usiadła przy oknie i obserwowała mokre podwórko. Jej młodszy brat układał w pokoju puzzle z obrazkiem górskiego schroniska. Mama przygotowała gorącą herbatę z miodem i cytryną oraz kruche ciasteczka. W kuchni pachniało cynamonem i świeżym ciastem drożdżowym. Po południu burza ucichła i zza chmur wyszło słońce. Na niebie pojawiła się kolorowa tęcza. Dzieci szybko wybiegły na dwór, aby poskakać po kałużach i odetchnąć świeżym powietrzem."
|
||||
},
|
||||
{
|
||||
"name": "* W bibliotece",
|
||||
"text": "W piątek uczniowie odwiedzili szkolną bibliotekę. Miła bibliotekarka opowiadała o książkach podróżniczych i historycznych. Na wysokich półkach stały grube słowniki, atlasy oraz kolorowe baśnie. Krzyś długo szukał książki o dżungli i egzotycznych zwierzętach. Zuzia wybrała historię o królewnie, rycerzu i smoku. W kącie biblioteki siedział chłopiec, który z ogromnym skupieniem czytał album o żaglowcach. Na zakończenie spotkania dzieci dostały zakładki do książek i obiecały, że będą częściej odwiedzać bibliotekę. Wychodziły ze szkoły w świetnych humorach, trzymając wypożyczone książki pod pachą."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Nauka Mnożenia</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── SELECT SCREEN ── -->
|
||||
<main id="select-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">Wybór tabliczki</h2>
|
||||
<div class="table-grid" id="table-grid">
|
||||
<button class="table-btn" data-val="1">1</button>
|
||||
<button class="table-btn" data-val="2">2</button>
|
||||
<button class="table-btn" data-val="3">3</button>
|
||||
<button class="table-btn" data-val="4">4</button>
|
||||
<button class="table-btn" data-val="5">5</button>
|
||||
<button class="table-btn" data-val="6">6</button>
|
||||
<button class="table-btn" data-val="7">7</button>
|
||||
<button class="table-btn" data-val="8">8</button>
|
||||
<button class="table-btn" data-val="9">9</button>
|
||||
<button class="table-btn" data-val="10">10</button>
|
||||
<button class="table-btn all" data-val="0">Wszystkie</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="content-panel">
|
||||
<label class="settings-label">
|
||||
Liczba zadań w serii:
|
||||
<input id="total-input" type="number" min="5" max="100" value="20" class="settings-input">
|
||||
</label>
|
||||
</section>
|
||||
<button class="action-btn start-btn" id="start-btn">Rozpocznij ćwiczenie →</button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="read-header">
|
||||
<button id="back-btn" class="read-nav-btn">← Zmień</button>
|
||||
<span class="read-title-text">Mnożenie</span>
|
||||
<div class="progress-container">
|
||||
<span id="progress-label" class="progress-text">0/20</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="problem" class="problem-text">?</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="submit-btn" id="submit">Sprawdź</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<main class="screen-content hidden" id="summary-screen" style="margin-top: 24px;">
|
||||
<div class="summary-view">
|
||||
<div style="font-size: 64px; text-align: center;">🎉</div>
|
||||
<h2 style="text-align: center; font-size: 28px; margin: 16px 0;">Koniec ćwiczenia!</h2>
|
||||
<p id="summary-text" class="summary-score-text"></p>
|
||||
<button class="action-btn summary-back-btn" id="again-btn">Jeszcze raz</button>
|
||||
<button class="action-btn summary-back-btn secondary" id="change-btn">Zmień tabliczkę</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/mnozenie.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '× Nauka Mnożenia',
|
||||
'{SUBTITLE}': 'Wybierz tabliczkę, którą chcesz poćwiczyć.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress-inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,170 @@
|
||||
<!doctype html>
|
||||
<html lang="pl">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||||
<title>Testy Matematyczne</title>
|
||||
<link rel="stylesheet" href="css/styles.css?v=20260524" />
|
||||
<link rel="stylesheet" href="css/fonts/inter.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-container">
|
||||
<!-- ── MENU SCREEN ── -->
|
||||
<main id="menu-screen">
|
||||
<nav class="subpage-nav">
|
||||
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
|
||||
</nav>
|
||||
<div class="screen-content">
|
||||
<div id="header-placeholder"></div>
|
||||
|
||||
<div class="test-config-grid">
|
||||
<!-- Operations -->
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">1. Wybierz działania</h2>
|
||||
<div class="ops-grid" id="ops">
|
||||
<button class="op-btn" data-op="add">+ Dodawanie</button>
|
||||
<button class="op-btn" data-op="sub">− Odejmowanie</button>
|
||||
<button class="op-btn" data-op="mul">× Mnożenie</button>
|
||||
<button class="op-btn" data-op="div">÷ Dzielenie</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Game Mode -->
|
||||
<section class="content-panel">
|
||||
<h2 class="panel-title">2. Wybierz tryb gry</h2>
|
||||
<div class="modes-grid">
|
||||
<button class="mode-btn" data-mode="timed" id="mode-timed">
|
||||
<span class="mode-icon">⧗</span>
|
||||
<span class="mode-label">Na czas</span>
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="training" id="mode-training">
|
||||
<span class="mode-icon">∞</span>
|
||||
<span class="mode-label">Trening</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="hint-text">Wybierz co najmniej jedno działanie, a następnie tryb, aby rozpocząć.</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="content-panel">
|
||||
<details class="settings-details">
|
||||
<summary class="settings-summary">
|
||||
<h2 class="panel-title">Ustawienia zaawansowane</h2>
|
||||
</summary>
|
||||
<div class="settings-content" id="settings-panel">
|
||||
<label class="settings-label-row"> Czas (sek) — tryb na czas
|
||||
<input id="setting-timed" type="number" min="5" max="600" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Maksymalny wynik
|
||||
<input id="setting-max-result" type="number" min="1" max="999" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Maksymalna składowa (operand)
|
||||
<input id="setting-max-operand" type="number" min="1" max="999" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row"> Liczba zadań (tryb Trening)
|
||||
<input id="setting-session-problems" type="number" min="1" max="500" class="settings-input-row" />
|
||||
</label>
|
||||
<label class="settings-label-row checkbox"> Wynik może być ujemny
|
||||
<input id="setting-allow-negative" type="checkbox" />
|
||||
</label>
|
||||
<label class="settings-label-row checkbox"> Wynik może być ułamkiem
|
||||
<input id="setting-allow-fraction" type="checkbox" />
|
||||
</label>
|
||||
<div class="settings-actions">
|
||||
<button id="save-settings" class="action-btn">Zapisz</button>
|
||||
<button id="reset-settings" class="action-btn secondary">Resetuj</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<!-- History -->
|
||||
<section class="content-panel">
|
||||
<details class="settings-details">
|
||||
<summary class="settings-summary">
|
||||
<h2 class="panel-title">Historia wyników</h2>
|
||||
</summary>
|
||||
<div class="history-list" id="history-panel">
|
||||
<!-- History items will be injected here -->
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── PLAY SCREEN ── -->
|
||||
<main class="play-screen-container hidden" id="play-screen">
|
||||
<header class="play-header">
|
||||
<button id="back-btn" class="play-nav-btn">← Menu</button>
|
||||
<div id="status" class="play-status">Trening</div>
|
||||
<div id="score" class="play-score">0</div>
|
||||
</header>
|
||||
<div id="progressbar-placeholder"></div>
|
||||
|
||||
<div class="play-body">
|
||||
<div class="problem-display">
|
||||
<div id="timer" class="timer-display hidden">60</div>
|
||||
<div id="problem" class="problem-text">—</div>
|
||||
<div id="feedback" class="feedback-text"></div>
|
||||
</div>
|
||||
|
||||
<div class="keypad-container">
|
||||
<div id="answer" class="answer-display"></div>
|
||||
<div class="keypad">
|
||||
<div class="key-row">
|
||||
<button class="key">1</button><button class="key">2</button><button class="key">3</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">4</button><button class="key">5</button><button class="key">6</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key">7</button><button class="key">8</button><button class="key">9</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="negate">±</button>
|
||||
<button class="key">0</button>
|
||||
<button class="key key-special" id="dot">.</button>
|
||||
</div>
|
||||
<div class="key-row">
|
||||
<button class="key key-special" id="clear">C</button>
|
||||
<button class="key key-special" id="backspace">⌫</button>
|
||||
<button class="submit-btn" id="submit">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ── SUMMARY SCREEN ── -->
|
||||
<div id="summary-overlay" class="summary-overlay hidden">
|
||||
<div class="summary-modal">
|
||||
<div style="font-size: 48px;">🎉</div>
|
||||
<h2>Koniec gry!</h2>
|
||||
<p id="summary-text" class="summary-modal-text"></p>
|
||||
<button id="summary-back" class="action-btn">Wróć do menu</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer-placeholder"></div>
|
||||
<script src="js/version.js?v=20260521" defer></script>
|
||||
<script src="js/nav.js?v=20260521"></script>
|
||||
<script src="js/app.js?v=20260524"></script>
|
||||
<script src="js/components.js"></script>
|
||||
<script>
|
||||
loadComponent('components/header.html', {
|
||||
'{TITLE}': '📝 Testy Matematyczne',
|
||||
'{SUBTITLE}': 'Skonfiguruj swój test i sprawdź wiedzę.'
|
||||
}).then(html => {
|
||||
document.getElementById('header-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/progress-bar.html', {'{PROGRESS_ID}': 'progress_inner'}).then(html => {
|
||||
document.getElementById('progressbar-placeholder').outerHTML = html;
|
||||
});
|
||||
loadComponent('components/footer.html').then(html => {
|
||||
document.getElementById('footer-placeholder').outerHTML = html;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.example.app",
|
||||
"variantName": "debug",
|
||||
"elements": []
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user