25 Commits

Author SHA1 Message Date
Sebastian Molenda 7ce23309f6 0.2.3-t2
Deploy to FTP / deploy (push) Successful in 5s
Build APK / build (push) Successful in 2m1s
Release APK on Tag (Gitea Actions) / build-and-release (push) Failing after 2s
2026-05-27 15:04:28 +02:00
Sebastian Molenda 6c4b5f4adf 0.2.3
Deploy to FTP / deploy (push) Successful in 5s
Build APK / build (push) Successful in 1m58s
2026-05-27 14:57:44 +02:00
Sebastian Molenda b8b7b3860b 0.2.2
Deploy to FTP / deploy (push) Successful in 16s
Build APK / build (push) Successful in 2m8s
2026-05-27 14:21:02 +02:00
Sebastian Molenda 35b19cf140 0.2.1
Deploy to FTP / deploy (push) Successful in 1m4s
Build APK / build (push) Successful in 2m47s
2026-05-27 14:06:15 +02:00
Sebastian Molenda 0b3c8e8a02 prawie dziala
Deploy to FTP / deploy (push) Successful in 1m47s
Build APK / build (push) Successful in 3m50s
2026-05-27 13:51:51 +02:00
Sebastian Molenda d890c6e15d fix
Deploy to FTP / deploy (push) Successful in 5s
2026-05-27 11:18:37 +02:00
Sebastian Molenda ccbb3549d1 fix indexa
Deploy to FTP / deploy (push) Successful in 5s
2026-05-27 11:05:48 +02:00
Sebastian Molenda 145c148bbc lokalnie gradle dziala
Deploy to FTP / deploy (push) Successful in 5s
Build APK / build (push) Successful in 1m51s
2026-05-27 00:13:35 +02:00
Sebastian Molenda 3402bb1c13 noszgradleurwaaa2a
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 1m11s
2026-05-26 23:56:06 +02:00
Sebastian Molenda 92b66f5b8b noszgradleurwaaaa
Deploy to FTP / deploy (push) Successful in 3s
Build APK / build (push) Failing after 39s
2026-05-26 23:25:13 +02:00
Sebastian Molenda cd5d92feff noszkurwaaaa
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 36s
2026-05-26 23:22:24 +02:00
Sebastian Molenda d3ef35f9ce znowhbrddsrgu zjebal
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 46s
2026-05-26 23:18:53 +02:00
Sebastian Molenda 367402e777 znowu zjebal
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 4s
2026-05-26 23:10:03 +02:00
Sebastian Molenda f59f0f19c8 se
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 23:07:32 +02:00
Sebastian Molenda 44338d1502 poprawka builda
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 2s
2026-05-26 23:02:09 +02:00
Sebastian Molenda d9885a2d17 poprawka builda
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 1s
2026-05-26 23:00:11 +02:00
Sebastian Molenda 8dd8ccd5f7 bild
Deploy to FTP / deploy (push) Successful in 3s
Build APK / build (push) Failing after 3s
2026-05-26 22:52:40 +02:00
Sebastian Molenda d3407057f2 prawie
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 22:28:49 +02:00
Sebastian Molenda 0386b615b1 poprawil?
Deploy to FTP / deploy (push) Successful in 7s
2026-05-26 22:24:49 +02:00
Sebastian Molenda ea84777b97 zjebal
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 22:22:53 +02:00
Sebastian Molenda 3bcf995c10 Merge branch 'androidapka'
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 22:17:26 +02:00
Sebastian Molenda 202562878a hello apk 2026-05-26 22:16:51 +02:00
Sebastian Molenda 7bab05fb24 wu tang clan
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 22:02:05 +02:00
Sebastian Molenda c26165bfb7 all local
Deploy to FTP / deploy (push) Successful in 6s
2026-05-26 21:58:38 +02:00
Sebastian Molenda 3cf616ad11 no cdn
Deploy to FTP / deploy (push) Successful in 4s
2026-05-26 21:31:43 +02:00
685 changed files with 28953 additions and 942 deletions
+37
View File
@@ -0,0 +1,37 @@
name: Build APK
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
container:
image: cimg/android:2024.01
steps:
- name: Install Node.js
run: |
sudo apt-get update
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
+1 -1
View File
@@ -25,6 +25,6 @@ jobs:
server: ${{ secrets.FTP_HOST }}
username: ${{ secrets.FTP_USER }}
password: ${{ secrets.FTP_PASS }}
local-dir: ./
local-dir: app/src/main/assets/
server-dir: /
+117
View File
@@ -0,0 +1,117 @@
name: Release APK on Tag (Gitea Actions)
on:
push:
tags:
- '*'
# Note: This workflow requires a repository secret named GITEA_TOKEN with a personal access token
# that has `repo` (or appropriate) scope for creating releases and uploading assets.
# Optionally set GITEA_SERVER to your Gitea server base URL (e.g. https://gitea.example.com).
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Ensure JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
- name: Build release APK
run: |
set -e
./gradlew :app:assembleRelease --no-daemon --stacktrace
- 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"
exit 1
fi
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
echo "Found $APK_PATH"
- name: Create Gitea release
id: create_release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER: ${{ secrets.GITEA_SERVER }}
run: |
set -e
# Determine API base URL
if [ -n "$GITEA_SERVER" ]; then
API_BASE="$GITEA_SERVER/api/v1"
else
API_BASE="https://gitea.com/api/v1"
fi
# Derive owner/repo from git remote
REMOTE_URL=$(git config --get remote.origin.url || true)
if [ -z "$REMOTE_URL" ]; then
echo "Cannot determine remote origin URL" >&2
exit 1
fi
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)
TAG=${GITHUB_REF_NAME:-$(echo $GITHUB_REF | sed 's#refs/tags/##')}
if [ -z "$TAG" ]; then
# fallback: use git to get tag from HEAD
TAG=$(git describe --tags --exact-match 2>/dev/null || true)
fi
if [ -z "$TAG" ]; then
echo "Cannot determine tag name" >&2
exit 1
fi
echo "Creating release $TAG for $OWNER/$REPO_NAME against $API_BASE"
CREATE_RESPONSE=$(curl -s -H "Content-Type: application/json" -H "Authorization: token $GITEA_TOKEN" \
-d "{\"tag_name\": \"$TAG\", \"name\": \"$TAG\", \"body\": \"Automated release for $TAG\", \"draft\": false, \"prerelease\": false}" \
"$API_BASE/repos/$OWNER/$REPO_NAME/releases")
RELEASE_ID=$(echo "$CREATE_RESPONSE" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2 | tr -d ' ')
if [ -z "$RELEASE_ID" ]; then
echo "Failed to create release. Response: $CREATE_RESPONSE" >&2
exit 1
fi
echo "RELEASE_ID=$RELEASE_ID" >> $GITHUB_ENV
echo "Created release id $RELEASE_ID"
- name: Upload APK to Gitea release
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
GITEA_SERVER: ${{ secrets.GITEA_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"
+68
View File
@@ -0,0 +1,68 @@
name: Release APK on Tag
on:
push:
tags:
- '*'
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Build release APK
run: ./gradlew :app:assembleRelease --no-daemon --stacktrace
- name: Find APK
id: find_apk
run: |
set -e
APK=$(ls app/build/outputs/apk/release/*.apk | head -n1 || true)
if [ -z "$APK" ]; then
echo "No APK found in app/build/outputs/apk/release"
exit 1
fi
echo "APK_PATH=$APK" >> $GITHUB_ENV
echo "apk=$APK"
- name: Upload artifact (for debugging)
uses: actions/upload-artifact@v4
with:
name: app-release-apk
path: ${{ env.APK_PATH }}
- name: Create GitHub Release
id: create_release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref_name }}
release_name: Release ${{ github.ref_name }}
body: Automated release for tag ${{ github.ref_name }}
draft: false
prerelease: false
- name: Upload APK to Release
uses: actions/upload-release-asset@v1
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.APK_PATH }}
asset_name: app-release.apk
asset_content_type: application/vnd.android.package-archive
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.
View File
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.
View File
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.
View File
+52
View File
@@ -0,0 +1,52 @@
<?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="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 2,
&quot;fromUser&quot;: false
}</component>
<component name="ProjectId" id="3EHNE822z12n2x6rUf57MsHEkbE" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/aln/Work/Matma&quot;
}
}</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>
+38
View File
@@ -0,0 +1,38 @@
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() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val webView = WebView(this)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
// Use WebViewAssetLoader to serve files from /assets/ over a secure origin.
val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(this))
.build()
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)
}
}
+11
View File
@@ -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>
Submodule QuizzyTemplate/App deleted from 149f214357
-410
View File
@@ -1,410 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--primary: #6366F1;
--primary-dark: #4F46E5;
--secondary: #8B5CF6;
--accent: #FFD166;
--text: #334155;
--text-light: #64748B;
--bg: #F8FAFC;
--bg-card: #FFFFFF;
--border: #E2E8F0;
--code-bg: #F1F5F9;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text);
line-height: 1.6;
padding: 0;
margin: 0;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
header {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 60px 0 80px;
position: relative;
}
header::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 4px;
background: var(--accent);
}
.logo {
font-size: 24px;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 20px;
}
.logo-icon {
margin-right: 10px;
font-size: 28px;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600;
line-height: 1.3;
margin-bottom: 20px;
color: var(--text);
}
h1 {
font-size: 2.5em;
margin-bottom: 15px;
color: white;
}
h2 {
font-size: 1.8em;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
margin-top: 50px;
}
h3 {
font-size: 1.4em;
margin-top: 30px;
}
h4 {
font-size: 1.2em;
margin-top: 20px;
}
p {
margin-bottom: 16px;
}
a {
color: var(--primary);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--primary-dark);
text-decoration: underline;
}
.header-subtitle {
font-size: 1.2em;
color: rgba(255, 255, 255, 0.9);
max-width: 700px;
}
.content {
padding: 60px 0;
position: relative;
}
.toc {
background-color: var(--bg-card);
border-radius: 10px;
padding: 25px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.05);
margin-bottom: 40px;
}
.toc-title {
font-size: 1.2em;
font-weight: 600;
margin-bottom: 15px;
color: var(--text);
display: flex;
align-items: center;
}
.toc-title svg {
margin-right: 10px;
}
.toc-list {
list-style-type: none;
columns: 2;
column-gap: 30px;
}
.toc-list li {
margin-bottom: 10px;
break-inside: avoid;
}
.toc-list a {
color: var(--text-light);
transition: color 0.2s;
display: flex;
align-items: center;
}
.toc-list a:hover {
color: var(--primary);
}
.toc-list a::before {
content: "•";
margin-right: 8px;
color: var(--primary);
font-weight: bold;
}
.section {
margin-bottom: 60px;
}
ul, ol {
margin-left: 20px;
margin-bottom: 20px;
}
li {
margin-bottom: 8px;
}
code {
font-family: 'Monaco', 'Consolas', monospace;
background-color: var(--code-bg);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
pre {
background-color: var(--code-bg);
padding: 15px;
border-radius: 8px;
overflow-x: auto;
margin-bottom: 20px;
border-left: 4px solid var(--primary);
}
pre code {
background: none;
padding: 0;
font-size: 0.9em;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.feature-card {
background: var(--bg-card);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.feature-icon {
font-size: 24px;
margin-bottom: 12px;
color: var(--primary);
}
.note {
background-color: rgba(99, 102, 241, 0.1);
border-left: 4px solid var(--primary);
padding: 15px;
border-radius: 0 8px 8px 0;
margin-bottom: 20px;
}
.warning {
background-color: rgba(255, 209, 102, 0.2);
border-left: 4px solid var(--accent);
padding: 15px;
border-radius: 0 8px 8px 0;
margin-bottom: 20px;
}
.project-structure {
font-family: 'Monaco', 'Consolas', monospace;
background-color: var(--code-bg);
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
white-space: pre;
overflow-x: auto;
line-height: 1.4;
font-size: 0.85em;
}
.code-filename {
color: var(--primary);
font-weight: 600;
margin-bottom: 5px;
font-size: 0.9em;
}
.steps {
list-style-type: none;
counter-reset: step-counter;
margin-left: 0;
}
.steps li {
counter-increment: step-counter;
margin-bottom: 15px;
position: relative;
padding-left: 45px;
}
.steps li::before {
content: counter(step-counter);
background-color: var(--primary);
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
left: 0;
top: -1px;
font-weight: 600;
font-size: 0.9em;
}
.table-container {
overflow-x: auto;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
th, td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background-color: var(--code-bg);
font-weight: 600;
}
tr:hover {
background-color: rgba(0, 0, 0, 0.01);
}
.badge {
display: inline-block;
background-color: var(--primary);
color: white;
border-radius: 20px;
padding: 3px 10px;
font-size: 0.75em;
font-weight: 600;
margin-right: 5px;
}
.badge.secondary {
background-color: var(--secondary);
}
.badge.accent {
background-color: var(--accent);
color: var(--text);
}
.screenshot {
max-width: 100%;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
footer {
background-color: var(--text);
color: white;
padding: 30px 0;
text-align: center;
margin-top: 100px;
}
.back-to-top {
position: fixed;
bottom: 30px;
right: 30px;
background-color: var(--primary);
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.back-to-top.visible {
opacity: 1;
visibility: visible;
}
.back-to-top:hover {
background-color: var(--primary-dark);
transform: translateY(-3px);
}
@media (max-width: 768px) {
h1 {
font-size: 2em;
}
h2 {
font-size: 1.5em;
}
.toc-list {
columns: 1;
}
.container {
padding: 0 15px;
}
.feature-grid {
grid-template-columns: 1fr;
}
}
-397
View File
@@ -1,397 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>QuizzyMind App Documentation</title>
<link rel="stylesheet" href="app.css">
</head>
<body>
<header>
<div class="container">
<div class="logo">
<span class="logo-icon">🎮</span>
Quizzy<span style="color: var(--accent);">Mind</span>
</div>
<h1>QuizzyMind App Documentation</h1>
<p class="header-subtitle">A comprehensive guide to installing, configuring, and customizing your React Native Expo quiz application.</p>
</div>
</header>
<div class="container">
<div class="content">
<div class="toc" id="table-of-contents">
<h3 class="toc-title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
Table of Contents
</h3>
<ul class="toc-list">
<li><a href="#introduction">Introduction</a></li>
<li><a href="#requirements">Requirements</a></li>
<li><a href="#installation">Installation</a></li>
<li><a href="#project-structure">Project Structure</a></li>
<li><a href="#configuration">Configuration</a></li>
<li><a href="#key-features">Key Features</a></li>
<li><a href="#customization-guide">Customization Guide</a></li>
<li><a href="#api-integration">API Integration</a></li>
<li><a href="#deployment">Deployment</a></li>
<li><a href="#troubleshooting">Troubleshooting</a></li>
<li><a href="#support">Support</a></li>
</ul>
</div>
<section class="section" id="introduction">
<h2>Introduction</h2>
<p>QuizzyMind is a fully functional quiz application built with React Native and Expo Router. This documentation will guide you through setting up and customizing the application to meet your specific needs.</p>
<h3>Key Features</h3>
<div class="feature-grid">
<div class="feature-card">
<div class="feature-icon">🧠</div>
<h4>Multiple Quiz Categories</h4>
<p>Extensive question bank with various categories</p>
</div>
<div class="feature-card">
<div class="feature-icon">🏆</div>
<h4>Real-time Leaderboards</h4>
<p>Compete with friends and track progress</p>
</div>
<div class="feature-card">
<div class="feature-icon">🎨</div>
<h4>Modern UI</h4>
<p>Clean design with smooth animations</p>
</div>
<div class="feature-card">
<div class="feature-icon">🧭</div>
<h4>Expo Router</h4>
<p>Optimized navigation experience</p>
</div>
<div class="feature-card">
<div class="feature-icon">📱</div>
<h4>Cross-platform</h4>
<p>Works on both iOS and Android</p>
</div>
</div>
</section>
<section class="section" id="requirements">
<h2>Requirements</h2>
<p>Before you begin, ensure you have the following installed:</p>
<ul>
<li><strong>Node.js</strong> (v14.0.0 or newer)</li>
<li><strong>npm</strong> (v6.0.0 or newer) or <strong>Yarn</strong> (v1.22.0 or newer)</li>
<li><strong>Expo CLI</strong> (<code>npm install -g expo-cli</code>)</li>
<li><strong>Android Studio</strong> (for Android development)</li>
<li><strong>Xcode</strong> (for iOS development, macOS only)</li>
<li><strong>Git</strong></li>
</ul>
<div class="note">
<p><strong>Note:</strong> You can verify your Node.js and npm versions by running <code>node -v</code> and <code>npm -v</code> in your terminal.</p>
</div>
</section>
<section class="section" id="installation">
<h2>Installation</h2>
<p>Follow these steps to get the app up and running:</p>
<ol class="steps">
<li>
<strong>Clone or extract the project files</strong>
<pre><code># If you downloaded as a zip, extract it
cd QuizzyMind-app</code></pre>
</li>
<li>
<strong>Install dependencies</strong>
<pre><code>npm install
# or with yarn
yarn install</code></pre>
</li>
<li>
<strong>Start the development server</strong>
<pre><code>npx expo start or npm start</code></pre>
</li>
<li>
<strong>Run on device/simulator</strong>
<ul>
<li>Press <code>i</code> to open in iOS simulator</li>
<li>Press <code>a</code> to open in Android emulator</li>
<li>Scan QR code with Expo Go app on physical device</li>
</ul>
</li>
</ol>
<div class="note">
<p><strong>Tip:</strong> If you encounter any issues during installation, refer to the <a href="#troubleshooting">Troubleshooting</a> section.</p>
</div>
</section>
<section class="section" id="project-structure">
<h2>Project Structure</h2>
<div class="project-structure">QuizzyMind-app/
├── app/ # Main application code using Expo Router
│ ├── (pages)/ # All screens inside this folder
│ ├────────── _layout.js # Root layout component
│ ├────────── CategoryScreen.jsx # Category Screen
│ ├────────── QuizScreen.jsx # QuizScreen Screen
│ ├────────── ResultScreen.jsx # ResultScreen Screen
│ ├── _layout.js # Root layout component
│ └── index.js # Home screen
├── assets/ # Static assets like images, fonts
├── components/ # Reusable UI components
│ ├── common/ # Shared components
│ ├── quiz/ # Quiz-related components
│ └── ui/ # UI elements (buttons, cards, etc.)
├── constants/ # App constants and theme settings
├── contexts/ # React contexts for state management
├── hooks/ # Custom React hooks
├── services/ # API services and external integrations
├── utils/ # Utility functions
├── app.json # Expo configuration
├── babel.config.js # Babel configuration
├── package.json # Dependencies and scripts
└── README.md # Basic readme</div>
</section>
<section class="section" id="key-features">
<h2>Key Features</h2>
<h3>Quiz Engine</h3>
<p>The quiz system supports:</p>
<ul>
<li>Multiple quiz categories</li>
<li>Various question types (multiple choice, true/false)</li>
<li>Timed quizzes</li>
<li>Score tracking and history</li>
<li>Difficulty levels</li>
</ul>
<h3>User Interface</h3>
<p>The UI is built with:</p>
<ul>
<li>Custom components for consistent design</li>
<li>Smooth animations and transitions</li>
<li>Dark/light mode support</li>
<li>Responsive layouts for different device sizes</li>
</ul>
</section>
<section class="section" id="customization-guide">
<h2>Customization Guide</h2>
<h3>Theme Customization</h3>
<p>Edit the theme variables in <code>constants/Theme.js</code>:</p>
<div class="code-filename">constants/Theme.js</div>
<pre><code>// Example theme customization
export const COLORS = {
primary: '#6366F1',
secondary: '#8B5CF6',
accent: '#FFD166',
// Add your custom colors
};
export const FONTS = {
// Customize font families
};
export const SIZES = {
// Customize spacing and sizes
};</code></pre>
<h3>Content Customization</h3>
<p>Quiz questions are stored in <code>data/quizData.js</code>. Modify this file to add your own questions:</p>
<div class="code-filename">data/quizData.js</div>
<pre><code>export const QUIZ_CATEGORIES = [
{
id: 'general',
title: 'General Knowledge',
icon: 'brain',
questions: [
{
id: 'q1',
question: 'What is the capital of France?',
options: ['London', 'Berlin', 'Paris', 'Madrid'],
correctAnswer: 'Paris',
difficulty: 'easy'
},
// Add more questions here
]
},
// Add more categories
];</code></pre>
<h3>Adding New Screens</h3>
<p>To add a new screen with Expo Router:</p>
<ol>
<li>Create a new file in the <code>app</code> directory, e.g., <code>app/newScreen.js</code></li>
<li>Define your screen component:</li>
</ol>
<div class="code-filename">app/newScreen.js</div>
<pre><code>import { View, Text, StyleSheet } from 'react-native';
import { useRouter } from 'expo-router';
export default function NewScreen() {
const router = useRouter();
return (
&lt;View style={styles.container}&gt;
&lt;Text&gt;New Screen Content&lt;/Text&gt;
&lt;/View&gt;
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});</code></pre>
</section>
<section class="section" id="deployment">
<h2>Deployment</h2>
<h3>Publishing to Expo</h3>
<ol>
<li>Create an Expo account at <a href="https://expo.dev" target="_blank">expo.dev</a></li>
<li>Configure your app in <code>app.json</code>:
<ul>
<li>Update <code>name</code>, <code>slug</code>, and <code>owner</code></li>
<li>Set appropriate version numbers</li>
<li>Configure splash screen and icons</li>
</ul>
</li>
<li>Build and publish:
<pre><code>expo build:android # For Android APK/AAB
expo build:ios # For iOS IPA
expo publish # For Expo Go distribution</code></pre>
</li>
</ol>
<h3>Submitting to App Stores</h3>
<h4>Android (Google Play)</h4>
<ol>
<li>Generate a signed AAB:
<pre><code>eas build -p android --profile production</code></pre>
</li>
<li>Follow Google Play Console submission guidelines</li>
</ol>
<h4>iOS (App Store)</h4>
<ol>
<li>Generate a signed IPA:
<pre><code>eas build -p ios --profile production</code></pre>
</li>
<li>Submit through App Store Connect</li>
</ol>
</section>
<section class="section" id="troubleshooting">
<h2>Troubleshooting</h2>
<h3>Common Issues</h3>
<h4>Metro Bundler Issues</h4>
<pre><code># Clear cache and restart
expo start -c</code></pre>
<h4>Dependency Problems</h4>
<pre><code># Reset node_modules
rm -rf node_modules
npm install</code></pre>
<h4>Expo SDK Version Conflicts</h4>
<p>Ensure all packages are compatible with your Expo SDK version in <code>package.json</code>.</p>
<div class="note">
<p><strong>Tip:</strong> Most common issues can be resolved by clearing your cache and reinstalling dependencies.</p>
</div>
</section>
<section class="section" id="support">
<h2>Support</h2>
<p>For additional support, please contact us at <a href="mailto:support@email.com">support@email.com</a> or visit our support page on TemplateMonster.</p>
</section>
</div>
</div>
<footer>
<div class="container">
<p>© 2025 QuizzyMind App | All Rights Reserved</p>
</div>
</footer>
<a href="#" class="back-to-top" id="backToTop">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 15l-6-6-6 6"/>
</svg>
</a>
<script>
// Back to top button functionality
const backToTopButton = document.getElementById('backToTop');
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTopButton.classList.add('visible');
} else {
backToTopButton.classList.remove('visible');
}
});
backToTopButton.addEventListener('click', (e) => {
e.preventDefault();
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
if (this.getAttribute('href') === '#') return;
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth'
});
}
});
});
</script>
</body>
</html>
+30
View File
@@ -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.
+68
View File
@@ -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,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
@@ -4,10 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
<title>Czytanie</title>
<link rel="stylesheet" href="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=202605261" />
<link rel="stylesheet" href="css/fonts/inter.css?v=202605261">
</head>
<body>
@@ -17,10 +15,7 @@
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
</nav>
<main class="screen-content">
<header class="subpage-header">
<h1 class="subpage-title">📖 Czytanie</h1>
<p class="subpage-subtitle">Wybierz tekst do przeczytania lub wklej własny.</p>
</header>
<div id="header-placeholder"></div>
<section class="content-panel">
<h2 class="panel-title">Gotowe teksty</h2>
@@ -56,7 +51,21 @@
</button>
</div>
<script src="nav.js?v=20260521"></script>
<script src="czytanie.js?v=20260524"></script>
<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>
@@ -4,10 +4,8 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
<title>Dyktando</title>
<link rel="stylesheet" href="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=20260524" />
<link rel="stylesheet" href="css/fonts/inter.css">
</head>
<body>
@@ -17,10 +15,7 @@
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
</nav>
<main class="screen-content">
<header class="subpage-header">
<h1 class="subpage-title">✏️ Dyktando</h1>
<p class="subpage-subtitle">Wybierz dyktando lub wklej własny tekst.</p>
</header>
<div id="header-placeholder"></div>
<section class="content-panel">
<h2 class="panel-title">Gotowe dyktanda</h2>
@@ -44,9 +39,7 @@
<span id="blank-progress" class="play-progress"></span>
</header>
<div class="play-progress-bar">
<div id="dyk-progress-bar-inner" class="play-progress-bar-inner"></div>
</div>
<div id="progressbar-placeholder"></div>
<div class="play-body">
<div class="play-scroll-area" id="dyk-scroll">
@@ -65,7 +58,23 @@
</div>
</div>
<script src="nav.js?v=20260521"></script>
<script src="dyktando.js?v=20260524"></script>
<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>
@@ -4,10 +4,8 @@
<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="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=20260524" />
<link rel="stylesheet" href="css/fonts/inter.css">
</head>
<body>
<div class="app-container">
@@ -17,10 +15,7 @@
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
</nav>
<div class="screen-content">
<header class="subpage-header">
<h1 class="subpage-title">÷ Nauka Dzielenia</h1>
<p class="subpage-subtitle">Wybierz dzielnik, przez który chcesz dzielić.</p>
</header>
<div id="header-placeholder"></div>
<section class="content-panel">
<h2 class="panel-title">Wybór dzielnika</h2>
<div class="table-grid" id="table-grid">
@@ -56,9 +51,7 @@
<span id="progress-label" class="progress-text">0/20</span>
</div>
</header>
<div class="read-progress-bar">
<div id="progress-inner" class="read-progress-bar-inner"></div>
</div>
<div id="progressbar-placeholder"></div>
<div class="play-body">
<div class="problem-display">
@@ -101,8 +94,23 @@
</main>
</div>
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
<script src="nav.js?v=20260521"></script>
<script src="dzielenie.js?v=20260524"></script>
<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>
@@ -4,10 +4,8 @@
<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="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=20260524" />
<link rel="stylesheet" href="css/fonts/inter.css">
</head>
<body>
<div class="app-container">
@@ -60,6 +58,6 @@
</div>
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
<script src="version.js?v=20260521" defer></script>
<script src="js/version.js?v=20260521" defer></script>
</body>
</html>
@@ -55,19 +55,19 @@ const statusEl = document.getElementById('status');
// load settings from localStorage
function loadSettings(){
try{
const raw = localStorage.getItem('matma:settings')
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
settingTimed.value = state.settings.timedSeconds
settingMaxResult.value = state.settings.maxResult
settingMaxOperand.value = state.settings.maxOperand
settingSessionProblems.value = state.settings.sessionProblems
settingAllowNegative.checked = !!state.settings.allowNegative
settingAllowFraction.checked = !!state.settings.allowFraction
// 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(){
@@ -145,16 +145,16 @@ function startPlay(){
renderProblem()
if (state.mode === 'timed'){
progressInner.style.width = '0%'
startTimer(state.settings.timedSeconds)
statusEl.textContent = 'Na czas';
timerEl.classList.remove('hidden');
if (progressInner) progressInner.style.width = '0%'
startTimer(state.settings.timedSeconds)
if (statusEl) statusEl.textContent = 'Na czas';
if (timerEl) timerEl.classList.remove('hidden');
} else {
progressInner.style.width = '0%'
state.sessionSolved = 0
state.sessionTarget = Math.max(1, state.settings.sessionProblems)
updateProgress()
timerEl.classList.add('hidden');
if (progressInner) progressInner.style.width = '0%'
state.sessionSolved = 0
state.sessionTarget = Math.max(1, state.settings.sessionProblems)
updateProgress()
if (timerEl) timerEl.classList.add('hidden');
}
}
@@ -311,14 +311,14 @@ backspaceBtn.addEventListener('click', ()=>{
function startTimer(seconds){
state.timeLeft = seconds
timerEl.textContent = state.timeLeft
if (timerEl) timerEl.textContent = state.timeLeft
stopTimer();
state.timerId = setInterval(()=>{
state.timeLeft -= 1
timerEl.textContent = state.timeLeft
if (timerEl) timerEl.textContent = state.timeLeft
const pct = Math.max(0, (state.timeLeft / seconds) * 100)
progressInner.style.width = pct + '%'
if (progressInner) progressInner.style.width = pct + '%'
if (state.timeLeft <= 0) {
stopTimer()
endSession()
@@ -357,9 +357,9 @@ summaryBack.addEventListener('click', ()=>{
function updateProgress(){
if (state.mode === 'training'){
const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100))
progressInner.style.width = pct + '%'
statusEl.textContent = `${state.sessionSolved}/${state.sessionTarget}`
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}`
}
}
@@ -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 => ...)
@@ -21,10 +21,10 @@
const readViewport = document.getElementById('read-viewport')
const readTextEl = document.getElementById('read-text')
const nextLineBtn = document.getElementById('next-line-btn')
const progressBar = document.getElementById('read-progress-bar-inner')
let progressBar = null
// ── Load text list from dyktanda.json ────────────────────────────────────
fetch('dyktanda.json')
fetch('json/dyktanda.json')
.then(r => r.json())
.then(data => {
data.forEach(item => {
@@ -52,11 +52,16 @@
clearInterval(autoTimer)
autoTimer = null
readTitleEl.textContent = title
readTextEl.textContent = text
readTextEl.style.transform = 'translateY(0)'
speedBtn.textContent = SPEED_LABELS[0]
progressBar.style.width = '0%'
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')
@@ -78,9 +83,8 @@
}
// ── Back to list ──────────────────────────────────────────────────────────
// Zakończ button: natychmiast wróć do listy bez potwierdzenia
readBackBtn.addEventListener('click', () => {
const active = yOffset > 0 || autoTimer !== null
if (active && !confirm('Wrócić do listy tekstów?')) return
clearInterval(autoTimer)
autoTimer = null
readWrap.classList.add('hidden')
@@ -132,6 +136,7 @@
function updateProgressBar() {
const progress = maxOffset > 0 ? (yOffset / maxOffset) * 100 : 100
progressBar.style.width = `${progress}%`
if (!progressBar) progressBar = document.getElementById('read-progress-bar-inner')
if (progressBar) progressBar.style.width = `${progress}%`
}
})()
@@ -35,10 +35,10 @@
const summaryScore = document.getElementById('summary-score')
const summaryBackBtn = document.getElementById('summary-back-btn')
const dykScroll = document.getElementById('dyk-scroll')
const progressBar = document.getElementById('dyk-progress-bar-inner')
let progressBar = null
// ── Load texts ────────────────────────────────────────────────────────────
fetch('dyktanda.json')
fetch('json/dyktanda.json')
.then(r => r.json())
.then(data => {
data.forEach(item => {
@@ -53,6 +53,20 @@
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
@@ -98,7 +112,7 @@
textDisplay.classList.remove('hidden')
choicesEl.classList.remove('hidden')
progressEl.textContent = ''
progressBar.style.width = '0%'
if (progressBar) progressBar.style.width = '0%'
listWrap.classList.add('hidden')
playWrap.classList.remove('hidden')
@@ -282,9 +296,8 @@
}
// ── Navigation ────────────────────────────────────────────────────────────
// Zakończ button: natychmiast wracamy do listy bez potwierdzenia
playBackBtn.addEventListener('click', () => {
const inProgress = blanks.length > 0 && current < blanks.length
if (inProgress && !confirm('Przerwać dyktando i wrócić do listy?')) return
goToList()
})
@@ -8,7 +8,7 @@
const problemEl = document.getElementById('problem')
const answerEl = document.getElementById('answer')
const feedbackEl = document.getElementById('feedback')
const progressInner = document.getElementById('progress-inner')
let progressInner = null
const progressLabel = document.getElementById('progress-label')
const summaryText = document.getElementById('summary-text')
const totalInput = document.getElementById('total-input')
@@ -99,7 +99,10 @@
function updateBar() {
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
progressInner.style.width = pct + '%'
if (!progressInner) {
progressInner = document.getElementById('progress-inner')
}
if (progressInner) progressInner.style.width = pct + '%'
progressLabel.textContent = `${st.solved}/${st.total}`
}
@@ -133,7 +136,8 @@
let sha = (window.COMMIT_SHA || '').toString().trim()
if (!sha) {
try {
const res = await fetch('/version.sha', { cache: 'no-cache' })
// 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)
@@ -8,7 +8,7 @@
const problemEl = document.getElementById('problem')
const answerEl = document.getElementById('answer')
const feedbackEl = document.getElementById('feedback')
const progressInner = document.getElementById('progress-inner')
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')
@@ -99,7 +99,8 @@
function updateBar() {
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
progressInner.style.width = pct + '%'
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}`
@@ -135,7 +136,8 @@
let sha = (window.COMMIT_SHA || '').toString().trim()
if (!sha) {
try {
const res = await fetch('/version.sha', { cache: 'no-cache' })
// 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)
@@ -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
})
})
})()
@@ -4,10 +4,8 @@
<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="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=20260524" />
<link rel="stylesheet" href="css/fonts/inter.css">
</head>
<body>
<div class="app-container">
@@ -17,10 +15,7 @@
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
</nav>
<div class="screen-content">
<header class="subpage-header">
<h1 class="subpage-title">× Nauka Mnożenia</h1>
<p class="subpage-subtitle">Wybierz tabliczkę, którą chcesz poćwiczyć.</p>
</header>
<div id="header-placeholder"></div>
<section class="content-panel">
<h2 class="panel-title">Wybór tabliczki</h2>
<div class="table-grid" id="table-grid">
@@ -56,9 +51,7 @@
<span id="progress-label" class="progress-text">0/20</span>
</div>
</header>
<div class="read-progress-bar">
<div id="progress-inner" class="read-progress-bar-inner"></div>
</div>
<div id="progressbar-placeholder"></div>
<div class="play-body">
<div class="problem-display">
@@ -101,8 +94,23 @@
</main>
</div>
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
<script src="nav.js?v=20260521"></script>
<script src="mnozenie.js?v=20260524"></script>
<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>
@@ -1,3 +1,31 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('../fonts/Inter-Regular.ttf') format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url('../fonts/Inter-Medium.ttf') format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url('../fonts/Inter-Bold.ttf') format('truetype');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
src: url('../fonts/Inter-ExtraBold.ttf') format('truetype');
}
:root {
--bg-color: #f1f5f9;
--text-color: #1e293b;
@@ -4,10 +4,8 @@
<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="styles.css?v=20260524" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css?v=20260524" />
<link rel="stylesheet" href="css/fonts/inter.css">
</head>
<body>
<div class="app-container">
@@ -17,10 +15,7 @@
<a href="index.html" class="back-btn" id="back-to-hub">← Wróć do menu</a>
</nav>
<div class="screen-content">
<header class="subpage-header">
<h1 class="subpage-title">📝 Testy Matematyczne</h1>
<p class="subpage-subtitle">Skonfiguruj swój test i sprawdź wiedzę.</p>
</header>
<div id="header-placeholder"></div>
<div class="test-config-grid">
<!-- Operations -->
@@ -105,9 +100,7 @@
<div id="status" class="play-status">Trening</div>
<div id="score" class="play-score">0</div>
</header>
<div class="play-progress-bar">
<div id="progress_inner" class="play-progress-bar-inner"></div>
</div>
<div id="progressbar-placeholder"></div>
<div class="play-body">
<div class="problem-display">
@@ -154,10 +147,24 @@
</div>
</div>
<footer class="app-footer">Version: <span id="commit-sha">(loading)</span></footer>
<script src="version.js?v=20260521" defer></script>
<script src="nav.js?v=20260521"></script>
<script src="app.js?v=20260524"></script>
<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": []
}

Some files were not shown because too many files have changed in this diff Show More