Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2721c2ec3 | |||
| 1385b3ace0 | |||
| 66a028880c | |||
| 365b12b0eb | |||
| 5ef6747e4d | |||
| 7ce23309f6 | |||
| 6c4b5f4adf | |||
| b8b7b3860b | |||
| 35b19cf140 | |||
| 0b3c8e8a02 | |||
| d890c6e15d | |||
| ccbb3549d1 | |||
| 145c148bbc | |||
| 3402bb1c13 | |||
| 92b66f5b8b | |||
| cd5d92feff | |||
| d3ef35f9ce | |||
| 367402e777 | |||
| f59f0f19c8 | |||
| 44338d1502 | |||
| d9885a2d17 | |||
| 8dd8ccd5f7 | |||
| d3407057f2 | |||
| 0386b615b1 | |||
| ea84777b97 | |||
| 3bcf995c10 | |||
| 202562878a |
@@ -0,0 +1,128 @@
|
|||||||
|
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: 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"
|
||||||
|
|
||||||
|
# Note: uploading artifacts with actions/upload-artifact@v4 is not supported
|
||||||
|
# on some self-hosted/enterprise runners (GHES). We skip storing artifacts
|
||||||
|
# via that action and instead upload the APK directly to the Gitea release
|
||||||
|
# in the steps below.
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
id: create_release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
GITEA_SERVER: ${{ secrets.GIT_SERVER }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
|
||||||
|
API_BASE="${GITEA_SERVER}/api/v1"
|
||||||
|
|
||||||
|
# Gitea Actions są kompatybilne z GITHUB_ envs
|
||||||
|
OWNER="${{ github.repository_owner }}"
|
||||||
|
REPO_NAME=$(echo "${{ github.repository }}" | cut -d/ -f2)
|
||||||
|
TAG="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
echo "URL: $API_BASE/repos/$OWNER/$REPO_NAME/releases"
|
||||||
|
echo "OWNER=$OWNER"
|
||||||
|
echo "REPO_NAME=$REPO_NAME"
|
||||||
|
echo "TAG=$TAG"
|
||||||
|
|
||||||
|
if [ -z "$TAG" ]; then
|
||||||
|
echo "Error: TAG (ref_name) is empty. Ensure you pushed a tag."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CREATE_RESPONSE=$(curl -s \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-X POST \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$TAG\",
|
||||||
|
\"name\": \"$TAG\",
|
||||||
|
\"body\": \"Automated release for $TAG\",
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": false
|
||||||
|
}" \
|
||||||
|
"$API_BASE/repos/$OWNER/$REPO_NAME/releases")
|
||||||
|
|
||||||
|
echo "$CREATE_RESPONSE"
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$CREATE_RESPONSE" \
|
||||||
|
| grep -o '"id":[0-9]*' \
|
||||||
|
| head -n1 \
|
||||||
|
| cut -d: -f2)
|
||||||
|
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Failed to create release"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "RELEASE_ID=$RELEASE_ID" >> $GITEA_ENV
|
||||||
|
- name: Upload APK to Gitea release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GIT_TOKEN }}
|
||||||
|
GITEA_SERVER: ${{ secrets.GIT_SERVER }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
API_BASE=${GITEA_SERVER:+$GITEA_SERVER/api/v1}
|
||||||
|
API_BASE=${API_BASE:-https://gitea.com/api/v1}
|
||||||
|
REMOTE_URL=$(git config --get remote.origin.url || true)
|
||||||
|
REPO_FULL=$(echo "$REMOTE_URL" | sed -E 's#.*[:/](.+/.+)\.git$#\1#')
|
||||||
|
OWNER=$(echo "$REPO_FULL" | cut -d/ -f1)
|
||||||
|
REPO_NAME=$(echo "$REPO_FULL" | cut -d/ -f2)
|
||||||
|
APK_PATH=${APK_PATH:-$APK_PATH}
|
||||||
|
if [ -z "$APK_PATH" ]; then
|
||||||
|
echo "APK_PATH not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
RELEASE_ID=${RELEASE_ID}
|
||||||
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "RELEASE_ID not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
UPLOAD_URL="$API_BASE/repos/$OWNER/$REPO_NAME/releases/$RELEASE_ID/assets?name=$(basename $APK_PATH)"
|
||||||
|
echo "Uploading $APK_PATH to $UPLOAD_URL"
|
||||||
|
|
||||||
|
curl --fail -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary @"$APK_PATH" "$UPLOAD_URL"
|
||||||
@@ -25,6 +25,6 @@ jobs:
|
|||||||
server: ${{ secrets.FTP_HOST }}
|
server: ${{ secrets.FTP_HOST }}
|
||||||
username: ${{ secrets.FTP_USER }}
|
username: ${{ secrets.FTP_USER }}
|
||||||
password: ${{ secrets.FTP_PASS }}
|
password: ${{ secrets.FTP_PASS }}
|
||||||
local-dir: ./
|
local-dir: app/src/main/assets/
|
||||||
server-dir: /
|
server-dir: /
|
||||||
|
|
||||||
|
|||||||
+36
-1
@@ -1,3 +1,38 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
./QuizzyTemplate
|
./QuizzyTemplate
|
||||||
./QuizzyTemplate/*
|
/.QuizzyTemplate
|
||||||
|
|
||||||
|
# Android/Gradle
|
||||||
|
/.gradle/
|
||||||
|
/build/
|
||||||
|
/app/build/
|
||||||
|
/**/build/
|
||||||
|
|
||||||
|
# Gradle wrapper
|
||||||
|
/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Local configuration
|
||||||
|
/local.properties
|
||||||
|
|
||||||
|
# Keystore
|
||||||
|
*.jks
|
||||||
|
|
||||||
|
# Generated APKs/outputs
|
||||||
|
**/outputs/
|
||||||
|
**/apk/**
|
||||||
|
*.apk
|
||||||
|
*.ap_
|
||||||
|
|
||||||
|
# IntelliJ / Android Studio
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# NPM / Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.log
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
|||||||
|
#Wed May 27 00:12:10 CEST 2026
|
||||||
|
gradle.version=8.5
|
||||||
Binary file not shown.
Binary file not shown.
Generated
+65
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="NONE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="51538617-7e5b-4e71-9f47-7bda274cf4cc" name="Changes" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="ClangdSettings">
|
||||||
|
<option name="formatViaClangd" value="false" />
|
||||||
|
</component>
|
||||||
|
<component name="ExternalProjectsManager">
|
||||||
|
<system id="GRADLE">
|
||||||
|
<state>
|
||||||
|
<projects_view>
|
||||||
|
<tree_state>
|
||||||
|
<expand />
|
||||||
|
<select />
|
||||||
|
</tree_state>
|
||||||
|
</projects_view>
|
||||||
|
</state>
|
||||||
|
</system>
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 2,
|
||||||
|
"fromUser": false
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="3EHNE822z12n2x6rUf57MsHEkbE" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.cidr.known.project.marker": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
|
"RunOnceActivity.readMode.enableVisualFormatting": "true",
|
||||||
|
"android.gradle.sync.needed": "true",
|
||||||
|
"cf.first.check.clang-format": "false",
|
||||||
|
"cidr.known.project.marker": "true",
|
||||||
|
"git-widget-placeholder": "master",
|
||||||
|
"kotlin-language-version-configured": "true",
|
||||||
|
"last_opened_file_path": "/Users/aln/Work/Matma"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="51538617-7e5b-4e71-9f47-7bda274cf4cc" name="Changes" comment="" />
|
||||||
|
<created>1779831422173</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1779831422173</updated>
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" packagePrefix="com.example.app" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -13,3 +13,33 @@ Pliki
|
|||||||
Jak uruchomić
|
Jak uruchomić
|
||||||
|
|
||||||
Otwórz plik `index.html` w przeglądarce (najlepiej na urządzeniu mobilnym lub w trybie responsywnym).
|
Otwórz plik `index.html` w przeglądarce (najlepiej na urządzeniu mobilnym lub w trybie responsywnym).
|
||||||
|
|
||||||
|
Signing a release APK/AAB
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Aby uniknąć ostrzeżeń "nieznany deweloper" lub instalatora na Androidzie, zbuduj podpisany pakiet APK lub AAB i zainstaluj go zamiast debugowego APK.
|
||||||
|
|
||||||
|
1. Utwórz magazyn kluczy (jeśli go nie masz):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my_key_alias
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Skopiuj `gradle.properties.example` do swojego osobistego pliku `~/.gradle/gradle.properties` i uzupełnij wartości (nie commituj prawdziwych haseł do kontroli wersji).
|
||||||
|
|
||||||
|
3. Zbuduj podpisany pakiet APK lub AAB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease # podpisany APK (jeśli podpisywanie jest skonfigurowane)
|
||||||
|
./gradlew bundleRelease # AAB do Sklepu Play
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Zainstaluj APK za pomocą adb:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb install -r app/build/outputs/apk/release/app-release.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
Lub przesłać AAB do Google Play (testowanie wewnętrzne) — zalecane w celu najłatwiejszej dystrybucji.
|
||||||
|
|
||||||
|
Jeśli chcesz tylko przetestować lokalnie i zobaczyć ostrzeżenie "Nieznane źródła", możesz tymczasowo włączyć instalowanie z nieznanych źródeł na urządzeniu, ale dystrybucja podpisanego wydania jest bezpieczniejszym podejściem.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.app'
|
||||||
|
compileSdk 34
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.example.app"
|
||||||
|
minSdk 21
|
||||||
|
targetSdk 34
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signing configuration for release builds.
|
||||||
|
// Configure the following properties in either your
|
||||||
|
// - ~/.gradle/gradle.properties (recommended for secrets), or
|
||||||
|
// - <project root>/gradle.properties (do NOT commit passwords)
|
||||||
|
//
|
||||||
|
// Example properties:
|
||||||
|
// MYAPP_STORE_FILE=keystores/my-release-key.jks
|
||||||
|
// MYAPP_STORE_PASSWORD=your_store_password
|
||||||
|
// MYAPP_KEY_ALIAS=my_key_alias
|
||||||
|
// MYAPP_KEY_PASSWORD=your_key_password
|
||||||
|
signingConfigs {
|
||||||
|
release {
|
||||||
|
// Only configure signing when the properties are provided.
|
||||||
|
if (project.hasProperty('MYAPP_STORE_FILE')) {
|
||||||
|
storeFile file(MYAPP_STORE_FILE)
|
||||||
|
storePassword MYAPP_STORE_PASSWORD
|
||||||
|
keyAlias MYAPP_KEY_ALIAS
|
||||||
|
keyPassword MYAPP_KEY_PASSWORD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
// Apply signing config if available. This keeps debug and CI builds
|
||||||
|
// working even when signing properties are absent locally.
|
||||||
|
if (project.hasProperty('MYAPP_STORE_FILE')) {
|
||||||
|
signingConfig signingConfigs.release
|
||||||
|
}
|
||||||
|
// Ensure release builds are not debuggable.
|
||||||
|
debuggable false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'androidx.core:core-ktx:1.9.0'
|
||||||
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.22"
|
||||||
|
implementation 'androidx.webkit:webkit:1.8.0'
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#- File Locator -
|
||||||
|
listingFile=../../../outputs/apk/debug/output-metadata.json
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
appMetadataVersion=1.1
|
||||||
|
androidGradlePluginVersion=8.2.2
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -55,19 +55,19 @@ const statusEl = document.getElementById('status');
|
|||||||
// load settings from localStorage
|
// load settings from localStorage
|
||||||
function loadSettings(){
|
function loadSettings(){
|
||||||
try{
|
try{
|
||||||
const raw = localStorage.getItem('matma:settings')
|
const raw = (typeof localStorage !== 'undefined') ? localStorage.getItem('matma:settings') : null
|
||||||
if (raw) {
|
if (raw) {
|
||||||
const s = JSON.parse(raw)
|
const s = JSON.parse(raw)
|
||||||
state.settings = Object.assign(state.settings, s)
|
state.settings = Object.assign(state.settings, s)
|
||||||
}
|
}
|
||||||
}catch(e){ console.warn('settings load failed', e) }
|
}catch(e){ console.warn('settings load failed', e) }
|
||||||
// reflect to inputs
|
// reflect to inputs (guard in case some inputs are missing)
|
||||||
settingTimed.value = state.settings.timedSeconds
|
if (settingTimed) settingTimed.value = state.settings.timedSeconds
|
||||||
settingMaxResult.value = state.settings.maxResult
|
if (settingMaxResult) settingMaxResult.value = state.settings.maxResult
|
||||||
settingMaxOperand.value = state.settings.maxOperand
|
if (settingMaxOperand) settingMaxOperand.value = state.settings.maxOperand
|
||||||
settingSessionProblems.value = state.settings.sessionProblems
|
if (settingSessionProblems) settingSessionProblems.value = state.settings.sessionProblems
|
||||||
settingAllowNegative.checked = !!state.settings.allowNegative
|
if (settingAllowNegative) settingAllowNegative.checked = !!state.settings.allowNegative
|
||||||
settingAllowFraction.checked = !!state.settings.allowFraction
|
if (settingAllowFraction) settingAllowFraction.checked = !!state.settings.allowFraction
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings(){
|
function saveSettings(){
|
||||||
@@ -145,16 +145,16 @@ function startPlay(){
|
|||||||
renderProblem()
|
renderProblem()
|
||||||
|
|
||||||
if (state.mode === 'timed'){
|
if (state.mode === 'timed'){
|
||||||
progressInner.style.width = '0%'
|
if (progressInner) progressInner.style.width = '0%'
|
||||||
startTimer(state.settings.timedSeconds)
|
startTimer(state.settings.timedSeconds)
|
||||||
statusEl.textContent = 'Na czas';
|
if (statusEl) statusEl.textContent = 'Na czas';
|
||||||
timerEl.classList.remove('hidden');
|
if (timerEl) timerEl.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
progressInner.style.width = '0%'
|
if (progressInner) progressInner.style.width = '0%'
|
||||||
state.sessionSolved = 0
|
state.sessionSolved = 0
|
||||||
state.sessionTarget = Math.max(1, state.settings.sessionProblems)
|
state.sessionTarget = Math.max(1, state.settings.sessionProblems)
|
||||||
updateProgress()
|
updateProgress()
|
||||||
timerEl.classList.add('hidden');
|
if (timerEl) timerEl.classList.add('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,14 +311,14 @@ backspaceBtn.addEventListener('click', ()=>{
|
|||||||
|
|
||||||
function startTimer(seconds){
|
function startTimer(seconds){
|
||||||
state.timeLeft = seconds
|
state.timeLeft = seconds
|
||||||
timerEl.textContent = state.timeLeft
|
if (timerEl) timerEl.textContent = state.timeLeft
|
||||||
|
|
||||||
stopTimer();
|
stopTimer();
|
||||||
state.timerId = setInterval(()=>{
|
state.timerId = setInterval(()=>{
|
||||||
state.timeLeft -= 1
|
state.timeLeft -= 1
|
||||||
timerEl.textContent = state.timeLeft
|
if (timerEl) timerEl.textContent = state.timeLeft
|
||||||
const pct = Math.max(0, (state.timeLeft / seconds) * 100)
|
const pct = Math.max(0, (state.timeLeft / seconds) * 100)
|
||||||
progressInner.style.width = pct + '%'
|
if (progressInner) progressInner.style.width = pct + '%'
|
||||||
if (state.timeLeft <= 0) {
|
if (state.timeLeft <= 0) {
|
||||||
stopTimer()
|
stopTimer()
|
||||||
endSession()
|
endSession()
|
||||||
@@ -358,8 +358,8 @@ summaryBack.addEventListener('click', ()=>{
|
|||||||
function updateProgress(){
|
function updateProgress(){
|
||||||
if (state.mode === 'training'){
|
if (state.mode === 'training'){
|
||||||
const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100))
|
const pct = Math.min(100, Math.round((state.sessionSolved/state.sessionTarget)*100))
|
||||||
progressInner.style.width = pct + '%'
|
if (progressInner) progressInner.style.width = pct + '%'
|
||||||
statusEl.textContent = `${state.sessionSolved}/${state.sessionTarget}`
|
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,7 +21,7 @@
|
|||||||
const readViewport = document.getElementById('read-viewport')
|
const readViewport = document.getElementById('read-viewport')
|
||||||
const readTextEl = document.getElementById('read-text')
|
const readTextEl = document.getElementById('read-text')
|
||||||
const nextLineBtn = document.getElementById('next-line-btn')
|
const nextLineBtn = document.getElementById('next-line-btn')
|
||||||
const progressBar = document.getElementById('read-progress-bar-inner')
|
let progressBar = null
|
||||||
|
|
||||||
// ── Load text list from dyktanda.json ────────────────────────────────────
|
// ── Load text list from dyktanda.json ────────────────────────────────────
|
||||||
fetch('json/dyktanda.json')
|
fetch('json/dyktanda.json')
|
||||||
@@ -52,11 +52,16 @@
|
|||||||
clearInterval(autoTimer)
|
clearInterval(autoTimer)
|
||||||
autoTimer = null
|
autoTimer = null
|
||||||
|
|
||||||
readTitleEl.textContent = title
|
if (!readTitleEl || !readTextEl || !speedBtn || !readWrap || !listWrap) {
|
||||||
readTextEl.textContent = text
|
console.warn('czytanie: missing DOM elements', { readTitleEl, readTextEl, speedBtn, readWrap, listWrap })
|
||||||
readTextEl.style.transform = 'translateY(0)'
|
}
|
||||||
speedBtn.textContent = SPEED_LABELS[0]
|
|
||||||
progressBar.style.width = '0%'
|
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')
|
listWrap.classList.add('hidden')
|
||||||
readWrap.classList.remove('hidden')
|
readWrap.classList.remove('hidden')
|
||||||
@@ -78,9 +83,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Back to list ──────────────────────────────────────────────────────────
|
// ── Back to list ──────────────────────────────────────────────────────────
|
||||||
|
// Zakończ button: natychmiast wróć do listy bez potwierdzenia
|
||||||
readBackBtn.addEventListener('click', () => {
|
readBackBtn.addEventListener('click', () => {
|
||||||
const active = yOffset > 0 || autoTimer !== null
|
|
||||||
if (active && !confirm('Wrócić do listy tekstów?')) return
|
|
||||||
clearInterval(autoTimer)
|
clearInterval(autoTimer)
|
||||||
autoTimer = null
|
autoTimer = null
|
||||||
readWrap.classList.add('hidden')
|
readWrap.classList.add('hidden')
|
||||||
@@ -132,6 +136,7 @@
|
|||||||
|
|
||||||
function updateProgressBar() {
|
function updateProgressBar() {
|
||||||
const progress = maxOffset > 0 ? (yOffset / maxOffset) * 100 : 100
|
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,7 +35,7 @@
|
|||||||
const summaryScore = document.getElementById('summary-score')
|
const summaryScore = document.getElementById('summary-score')
|
||||||
const summaryBackBtn = document.getElementById('summary-back-btn')
|
const summaryBackBtn = document.getElementById('summary-back-btn')
|
||||||
const dykScroll = document.getElementById('dyk-scroll')
|
const dykScroll = document.getElementById('dyk-scroll')
|
||||||
const progressBar = document.getElementById('dyk-progress-bar-inner')
|
let progressBar = null
|
||||||
|
|
||||||
// ── Load texts ────────────────────────────────────────────────────────────
|
// ── Load texts ────────────────────────────────────────────────────────────
|
||||||
fetch('json/dyktanda.json')
|
fetch('json/dyktanda.json')
|
||||||
@@ -53,6 +53,20 @@
|
|||||||
textList.innerHTML = '<p style="color:var(--muted)">Nie udało się wczytać tekstów.</p>'
|
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', () => {
|
customStartBtn.addEventListener('click', () => {
|
||||||
const txt = customInput.value.trim()
|
const txt = customInput.value.trim()
|
||||||
if (!txt) return
|
if (!txt) return
|
||||||
@@ -98,7 +112,7 @@
|
|||||||
textDisplay.classList.remove('hidden')
|
textDisplay.classList.remove('hidden')
|
||||||
choicesEl.classList.remove('hidden')
|
choicesEl.classList.remove('hidden')
|
||||||
progressEl.textContent = ''
|
progressEl.textContent = ''
|
||||||
progressBar.style.width = '0%'
|
if (progressBar) progressBar.style.width = '0%'
|
||||||
|
|
||||||
listWrap.classList.add('hidden')
|
listWrap.classList.add('hidden')
|
||||||
playWrap.classList.remove('hidden')
|
playWrap.classList.remove('hidden')
|
||||||
@@ -282,9 +296,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Navigation ────────────────────────────────────────────────────────────
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
|
// Zakończ button: natychmiast wracamy do listy bez potwierdzenia
|
||||||
playBackBtn.addEventListener('click', () => {
|
playBackBtn.addEventListener('click', () => {
|
||||||
const inProgress = blanks.length > 0 && current < blanks.length
|
|
||||||
if (inProgress && !confirm('Przerwać dyktando i wrócić do listy?')) return
|
|
||||||
goToList()
|
goToList()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
const problemEl = document.getElementById('problem')
|
const problemEl = document.getElementById('problem')
|
||||||
const answerEl = document.getElementById('answer')
|
const answerEl = document.getElementById('answer')
|
||||||
const feedbackEl = document.getElementById('feedback')
|
const feedbackEl = document.getElementById('feedback')
|
||||||
const progressInner = document.getElementById('progress-inner')
|
let progressInner = null
|
||||||
const progressLabel = document.getElementById('progress-label')
|
const progressLabel = document.getElementById('progress-label')
|
||||||
const summaryText = document.getElementById('summary-text')
|
const summaryText = document.getElementById('summary-text')
|
||||||
const totalInput = document.getElementById('total-input')
|
const totalInput = document.getElementById('total-input')
|
||||||
@@ -99,7 +99,10 @@
|
|||||||
|
|
||||||
function updateBar() {
|
function updateBar() {
|
||||||
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
|
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}`
|
progressLabel.textContent = `${st.solved}/${st.total}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +136,8 @@
|
|||||||
let sha = (window.COMMIT_SHA || '').toString().trim()
|
let sha = (window.COMMIT_SHA || '').toString().trim()
|
||||||
if (!sha) {
|
if (!sha) {
|
||||||
try {
|
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) {
|
if (res.ok) {
|
||||||
const txt = await res.text()
|
const txt = await res.text()
|
||||||
const first = txt.split(/\r?\n/).find(l => l.trim().length > 0)
|
const first = txt.split(/\r?\n/).find(l => l.trim().length > 0)
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
const problemEl = document.getElementById('problem')
|
const problemEl = document.getElementById('problem')
|
||||||
const answerEl = document.getElementById('answer')
|
const answerEl = document.getElementById('answer')
|
||||||
const feedbackEl = document.getElementById('feedback')
|
const feedbackEl = document.getElementById('feedback')
|
||||||
const progressInner = document.getElementById('progress-inner')
|
let progressInner = null
|
||||||
const progressLabel = document.getElementById('progress-label')
|
const progressLabel = document.getElementById('progress-label')
|
||||||
const scoreLabel = document.getElementById('score-label') // This element is removed, but we might re-purpose the logic
|
const scoreLabel = document.getElementById('score-label') // This element is removed, but we might re-purpose the logic
|
||||||
const summaryText = document.getElementById('summary-text')
|
const summaryText = document.getElementById('summary-text')
|
||||||
@@ -99,7 +99,8 @@
|
|||||||
|
|
||||||
function updateBar() {
|
function updateBar() {
|
||||||
const pct = st.total > 0 ? Math.round((st.solved / st.total) * 100) : 0
|
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}`
|
progressLabel.textContent = `${st.solved}/${st.total}`
|
||||||
// scoreLabel is removed, so we comment this out
|
// scoreLabel is removed, so we comment this out
|
||||||
// scoreLabel.textContent = `✔ ${st.score}`
|
// scoreLabel.textContent = `✔ ${st.score}`
|
||||||
@@ -135,7 +136,8 @@
|
|||||||
let sha = (window.COMMIT_SHA || '').toString().trim()
|
let sha = (window.COMMIT_SHA || '').toString().trim()
|
||||||
if (!sha) {
|
if (!sha) {
|
||||||
try {
|
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) {
|
if (res.ok) {
|
||||||
const txt = await res.text()
|
const txt = await res.text()
|
||||||
const first = txt.split(/\r?\n/).find(l => l.trim().length > 0)
|
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
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "COMPATIBLE_SCREEN_MANIFEST",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "com.example.app",
|
||||||
|
"variantName": "debug",
|
||||||
|
"elements": []
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user