5 Commits

Author SHA1 Message Date
Sebastian Molenda 365b12b0eb 0.2.3-t4
Deploy to FTP / deploy (push) Successful in 4s
Build APK / build (push) Failing after 1m56s
Release APK on Tag (Gitea Actions) / build-and-release (push) Failing after 1s
2026-05-27 21:38:16 +02:00
Sebastian Molenda 5ef6747e4d 0.2.3-t3
Deploy to FTP / deploy (push) Successful in 4s
Release APK on Tag (Gitea Actions) / build-and-release (push) Failing after 2s
Build APK / build (push) Failing after 1m57s
2026-05-27 21:21:49 +02:00
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
82 changed files with 459 additions and 99 deletions
+89 -10
View File
@@ -30,14 +30,93 @@ jobs:
- 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
if [ -n "$GITEA_SERVER" ]; then
API_BASE="$GITEA_SERVER/api/v1"
else
API_BASE="https://gitea.com/api/v1"
fi
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
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.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"
+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).p
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.
+1 -8
View File
@@ -4,14 +4,7 @@
<option name="autoReloadType" value="NONE" />
</component>
<component name="ChangeListManager">
<list default="true" id="51538617-7e5b-4e71-9f47-7bda274cf4cc" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/.gradle/8.5/checksums/checksums.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.5/checksums/checksums.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.5/dependencies-accessors/dependencies-accessors.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/8.5/fileHashes/fileHashes.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/8.5/fileHashes/fileHashes.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gradle/buildOutputCleanup/buildOutputCleanup.lock" beforeDir="false" afterPath="$PROJECT_DIR$/.gradle/buildOutputCleanup/buildOutputCleanup.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/MainActivity.kt" beforeDir="false" afterPath="$PROJECT_DIR$/MainActivity.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/app/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/app/build.gradle" afterDir="false" />
</list>
<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" />
+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.
+29
View File
@@ -14,10 +14,39 @@ android {
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
}
}
+23 -23
View File
@@ -55,19 +55,19 @@ const statusEl = document.getElementById('status');
// load settings from localStorage
function loadSettings(){
try{
let progressInner = null
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}`
}
}
@@ -1,7 +1,20 @@
// 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 => r.text())
.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);
@@ -83,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')
@@ -296,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()
})
@@ -136,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)
@@ -136,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)
@@ -4,14 +4,10 @@
const backBtn = document.getElementById('back-to-hub')
if (!backBtn) return
// Immediately navigate back to hub/menu without confirmation
backBtn.addEventListener('click', (e) => {
const playScreen = document.getElementById('play-screen')
const taskActive = playScreen && !playScreen.classList.contains('hidden')
if (taskActive) {
e.preventDefault()
const ok = confirm('Masz aktywne zadanie. Czy na pewno chcesz wyjść do menu?')
if (ok) window.location.href = backBtn.getAttribute('href')
}
const href = backBtn.getAttribute('href')
if (href) window.location.href = href
})
})
})()
@@ -1,4 +1,4 @@
#Wed May 27 14:04:13 CEST 2026
#Wed May 27 14:53:22 CEST 2026
base.2=/Users/aln/Work/Matma/app/build/intermediates/dex/debug/mergeProjectDexDebug/6/classes.dex
path.2=6/classes.dex
base.1=/Users/aln/Work/Matma/app/build/intermediates/dex/debug/mergeProjectDexDebug/0/classes.dex
@@ -1,2 +1,2 @@
3
4
0
Binary file not shown.
+23 -23
View File
@@ -55,19 +55,19 @@ const statusEl = document.getElementById('status');
// load settings from localStorage
function loadSettings(){
try{
let progressInner = null
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}`
}
}
+14 -1
View File
@@ -1,7 +1,20 @@
// 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 => r.text())
.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);
+1 -2
View File
@@ -83,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')
+1 -2
View File
@@ -296,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()
})
+2 -1
View File
@@ -136,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)
+2 -1
View File
@@ -136,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)
+3 -7
View File
@@ -4,14 +4,10 @@
const backBtn = document.getElementById('back-to-hub')
if (!backBtn) return
// Immediately navigate back to hub/menu without confirmation
backBtn.addEventListener('click', (e) => {
const playScreen = document.getElementById('play-screen')
const taskActive = playScreen && !playScreen.classList.contains('hidden')
if (taskActive) {
e.preventDefault()
const ok = confirm('Masz aktywne zadanie. Czy na pewno chcesz wyjść do menu?')
if (ok) window.location.href = backBtn.getAttribute('href')
}
const href = backBtn.getAttribute('href')
if (href) window.location.href = href
})
})
})()
@@ -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() {
@@ -11,16 +14,27 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
val webView = WebView(this)
// Enable remote debugging for WebView so we can inspect JS console via chrome://inspect
WebView.setWebContentsDebuggingEnabled(true)
// Enable remote debugging for WebView so we can inspect JS console via chrome://inspect
WebView.setWebContentsDebuggingEnabled(true)
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)
}
+12
View File
@@ -0,0 +1,12 @@
# Example properties for signing your release APK/AAB.
# DO NOT commit real passwords. Copy this to gradle.properties in your home
# directory (~/.gradle/gradle.properties) or to a secure CI secret store.
# Path to your keystore file (relative to project root or absolute path)
MYAPP_STORE_FILE=keystores/my-release-key.jks
# Your keystore password
MYAPP_STORE_PASSWORD=
# Your key alias inside the keystore
MYAPP_KEY_ALIAS=
# Your key password (often same as store password)
MYAPP_KEY_PASSWORD=