diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml
new file mode 100644
index 0000000..869dd43
--- /dev/null
+++ b/.github/workflows/release-on-tag.yml
@@ -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
diff --git a/.gradle/8.5/executionHistory/executionHistory.bin b/.gradle/8.5/executionHistory/executionHistory.bin
index 7cf4ed1..1606922 100644
Binary files a/.gradle/8.5/executionHistory/executionHistory.bin and b/.gradle/8.5/executionHistory/executionHistory.bin differ
diff --git a/.gradle/8.5/executionHistory/executionHistory.lock b/.gradle/8.5/executionHistory/executionHistory.lock
index 5191041..a85ebed 100644
Binary files a/.gradle/8.5/executionHistory/executionHistory.lock and b/.gradle/8.5/executionHistory/executionHistory.lock differ
diff --git a/.gradle/8.5/fileHashes/fileHashes.bin b/.gradle/8.5/fileHashes/fileHashes.bin
index b6e7a50..4dc751d 100644
Binary files a/.gradle/8.5/fileHashes/fileHashes.bin and b/.gradle/8.5/fileHashes/fileHashes.bin differ
diff --git a/.gradle/8.5/fileHashes/fileHashes.lock b/.gradle/8.5/fileHashes/fileHashes.lock
index c3ef55c..0b6f11a 100644
Binary files a/.gradle/8.5/fileHashes/fileHashes.lock and b/.gradle/8.5/fileHashes/fileHashes.lock differ
diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock
index 467162b..9e6f275 100644
Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe
index 5b02863..3166d01 100644
Binary files a/.gradle/file-system.probe and b/.gradle/file-system.probe differ
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 68393d6..aa9ec5e 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,70 +4,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/README.md b/README.md
index 1bf3db4..1b3d832 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/app/build.gradle b/app/build.gradle
index 9d0a9ee..3e7fdd3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
+ // - /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
}
}
diff --git a/app/build/intermediates/assets/debug/js/app.js b/app/build/intermediates/assets/debug/js/app.js
index d0f942b..d1bfe0a 100644
--- a/app/build/intermediates/assets/debug/js/app.js
+++ b/app/build/intermediates/assets/debug/js/app.js
@@ -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}`
}
}
diff --git a/app/build/intermediates/assets/debug/js/nav.js b/app/build/intermediates/assets/debug/js/nav.js
index 3893853..05e20de 100644
--- a/app/build/intermediates/assets/debug/js/nav.js
+++ b/app/build/intermediates/assets/debug/js/nav.js
@@ -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
})
})
})()
diff --git a/app/build/intermediates/compressed_assets/debug/out/assets/js/app.js.jar b/app/build/intermediates/compressed_assets/debug/out/assets/js/app.js.jar
index 78d6d1f..e8efa81 100644
Binary files a/app/build/intermediates/compressed_assets/debug/out/assets/js/app.js.jar and b/app/build/intermediates/compressed_assets/debug/out/assets/js/app.js.jar differ
diff --git a/app/build/intermediates/compressed_assets/debug/out/assets/js/nav.js.jar b/app/build/intermediates/compressed_assets/debug/out/assets/js/nav.js.jar
index a404203..37168a2 100644
Binary files a/app/build/intermediates/compressed_assets/debug/out/assets/js/nav.js.jar and b/app/build/intermediates/compressed_assets/debug/out/assets/js/nav.js.jar differ
diff --git a/app/build/intermediates/incremental/packageDebug/tmp/debug/dex-renamer-state.txt b/app/build/intermediates/incremental/packageDebug/tmp/debug/dex-renamer-state.txt
index 5b2e803..0e8ada7 100644
--- a/app/build/intermediates/incremental/packageDebug/tmp/debug/dex-renamer-state.txt
+++ b/app/build/intermediates/incremental/packageDebug/tmp/debug/dex-renamer-state.txt
@@ -1,4 +1,4 @@
-#Wed May 27 14:20:16 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
diff --git a/app/build/outputs/apk/debug/app-debug.apk b/app/build/outputs/apk/debug/app-debug.apk
index bc761a9..9e8df1b 100644
Binary files a/app/build/outputs/apk/debug/app-debug.apk and b/app/build/outputs/apk/debug/app-debug.apk differ
diff --git a/app/src/main/assets/js/app.js b/app/src/main/assets/js/app.js
index d0f942b..d1bfe0a 100644
--- a/app/src/main/assets/js/app.js
+++ b/app/src/main/assets/js/app.js
@@ -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}`
}
}
diff --git a/gradle.properties.example b/gradle.properties.example
new file mode 100644
index 0000000..6039d0a
--- /dev/null
+++ b/gradle.properties.example
@@ -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=