diff --git a/CHANGELOG.md b/CHANGELOG.md
index 333b761..1709a07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
## Unreleased
+Configurable native WebView loading indicators.
+
+- Preserves the current top loading bar as the default for existing apps.
+- Adds per-app modes for no indicator, top bar, bottom bar, centered spinner and fullscreen spinner.
+- Adds configurable indicator color, bar thickness, spinner size and fullscreen overlay dimming.
+- Loading UI remains separate from the mandatory STU production splash and follows the existing WebView navigation/progress callbacks.
+- Implementation remains compatible with the Android 6 / API 23 baseline.
+
Dynamic image recovery through Quantum Asset Store.
- Trusted image requests below `/assets/` that miss the native store are now warmed immediately in parallel while WebView keeps its ordinary live request path.
diff --git a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java
index cb25daf..1d41b81 100644
--- a/app/src/main/java/de/starlightunit/wrapper/MainActivity.java
+++ b/app/src/main/java/de/starlightunit/wrapper/MainActivity.java
@@ -26,6 +26,7 @@
import de.starlightunit.wrapper.media.QuantumNativeMediaPlayer;
import de.starlightunit.wrapper.navigation.NavigationPolicy;
import de.starlightunit.wrapper.session.QuantumSessionCookieStore;
+import de.starlightunit.wrapper.ui.LoadingIndicatorController;
import de.starlightunit.wrapper.web.GameWebChromeClient;
import de.starlightunit.wrapper.web.GameWebViewClient;
import de.starlightunit.wrapper.web.WebViewConfigurator;
@@ -40,7 +41,7 @@ public final class MainActivity extends Activity
private static final int FILE_CHOOSER_REQUEST = 7001;
private WebView webView;
- private ProgressBar progressBar;
+ private LoadingIndicatorController loadingIndicator;
private View errorPanel;
private GameWebChromeClient chromeClient;
private GameWebViewClient webViewClient;
@@ -60,9 +61,22 @@ protected void onCreate(Bundle savedInstanceState) {
enterImmersiveMode();
webView = findViewById(R.id.web_view);
- progressBar = findViewById(R.id.progress);
+ ProgressBar horizontalProgress = findViewById(R.id.progress);
+ FrameLayout loadingOverlay = findViewById(R.id.loading_overlay);
+ ProgressBar loadingSpinner = findViewById(R.id.loading_spinner);
errorPanel = findViewById(R.id.error_panel);
FrameLayout fullscreenContainer = findViewById(R.id.fullscreen_container);
+ loadingIndicator = new LoadingIndicatorController(
+ this,
+ horizontalProgress,
+ loadingOverlay,
+ loadingSpinner,
+ AppConfig.LOADING_INDICATOR_STYLE,
+ AppConfig.LOADING_INDICATOR_COLOR,
+ AppConfig.LOADING_BAR_THICKNESS_DP,
+ AppConfig.LOADING_SPINNER_SIZE_DP,
+ AppConfig.LOADING_OVERLAY_DIM_PERCENT
+ );
ImageView introOverlay = findViewById(R.id.intro_overlay);
Button retryButton = findViewById(R.id.retry_button);
@@ -172,12 +186,12 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onPageLoading() {
mainFrameFailed = false;
errorPanel.setVisibility(View.GONE);
- progressBar.setVisibility(View.VISIBLE);
+ loadingIndicator.show();
}
@Override
public void onPageReady() {
- progressBar.setVisibility(View.GONE);
+ loadingIndicator.hide();
if (!mainFrameFailed) {
errorPanel.setVisibility(View.GONE);
}
@@ -189,14 +203,13 @@ public void onPageReady() {
@Override
public void onMainFrameError() {
mainFrameFailed = true;
- progressBar.setVisibility(View.GONE);
+ loadingIndicator.hide();
errorPanel.setVisibility(View.VISIBLE);
}
@Override
public void onProgress(int progress) {
- progressBar.setProgress(progress);
- progressBar.setVisibility(progress >= 100 ? View.GONE : View.VISIBLE);
+ loadingIndicator.setProgress(progress);
}
@Override
diff --git a/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java b/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java
index 0ce5494..ee98294 100644
--- a/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java
+++ b/app/src/main/java/de/starlightunit/wrapper/config/AppConfig.java
@@ -26,6 +26,12 @@ public final class AppConfig {
public static final String APP_HEADER_VALUE = "starlight-unit-game";
public static final String CUSTOM_REQUEST_HEADERS_JSON = "{}";
+ public static final String LOADING_INDICATOR_STYLE = "top-bar";
+ public static final String LOADING_INDICATOR_COLOR = "#6fc7ff";
+ public static final int LOADING_BAR_THICKNESS_DP = 3;
+ public static final int LOADING_SPINNER_SIZE_DP = 56;
+ public static final int LOADING_OVERLAY_DIM_PERCENT = 35;
+
public static final boolean KEEP_SCREEN_ON = true;
public static final boolean ALLOW_AUTOPLAY_MEDIA = true;
public static final boolean ALLOW_THIRD_PARTY_COOKIES = true;
diff --git a/app/src/main/java/de/starlightunit/wrapper/ui/LoadingIndicatorController.java b/app/src/main/java/de/starlightunit/wrapper/ui/LoadingIndicatorController.java
new file mode 100644
index 0000000..68baca3
--- /dev/null
+++ b/app/src/main/java/de/starlightunit/wrapper/ui/LoadingIndicatorController.java
@@ -0,0 +1,148 @@
+package de.starlightunit.wrapper.ui;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+
+import java.util.Locale;
+
+public final class LoadingIndicatorController {
+
+ public static final String STYLE_NONE = "none";
+ public static final String STYLE_TOP_BAR = "top-bar";
+ public static final String STYLE_BOTTOM_BAR = "bottom-bar";
+ public static final String STYLE_CENTER_SPINNER = "center-spinner";
+ public static final String STYLE_FULLSCREEN_SPINNER = "fullscreen-spinner";
+
+ private final ProgressBar horizontalBar;
+ private final FrameLayout spinnerOverlay;
+ private final ProgressBar spinner;
+ private final String style;
+
+ public LoadingIndicatorController(
+ Context context,
+ ProgressBar horizontalBar,
+ FrameLayout spinnerOverlay,
+ ProgressBar spinner,
+ String configuredStyle,
+ String configuredColor,
+ int barThicknessDp,
+ int spinnerSizeDp,
+ int overlayDimPercent
+ ) {
+ this.horizontalBar = horizontalBar;
+ this.spinnerOverlay = spinnerOverlay;
+ this.spinner = spinner;
+ this.style = normalizeStyle(configuredStyle);
+
+ int color = parseColor(configuredColor);
+ ColorStateList tint = ColorStateList.valueOf(color);
+ horizontalBar.setProgressTintList(tint);
+ spinner.setIndeterminateTintList(tint);
+
+ float density = context.getResources().getDisplayMetrics().density;
+ configureHorizontalBar(
+ Math.max(1, Math.min(12, barThicknessDp)),
+ density
+ );
+ configureSpinner(
+ Math.max(24, Math.min(128, spinnerSizeDp)),
+ density
+ );
+
+ int dimPercent = Math.max(0, Math.min(90, overlayDimPercent));
+ int alpha = Math.round(255f * (dimPercent / 100f));
+ spinnerOverlay.setBackgroundColor(
+ STYLE_FULLSCREEN_SPINNER.equals(style)
+ ? Color.argb(alpha, 0, 0, 0)
+ : Color.TRANSPARENT
+ );
+ hide();
+ }
+
+ public void show() {
+ if (STYLE_NONE.equals(style)) {
+ hide();
+ return;
+ }
+
+ if (isBarStyle()) {
+ spinnerOverlay.setVisibility(View.GONE);
+ spinner.setVisibility(View.GONE);
+ horizontalBar.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ horizontalBar.setVisibility(View.GONE);
+ spinnerOverlay.setVisibility(View.VISIBLE);
+ spinner.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ horizontalBar.setVisibility(View.GONE);
+ spinner.setVisibility(View.GONE);
+ spinnerOverlay.setVisibility(View.GONE);
+ }
+
+ public void setProgress(int progress) {
+ int bounded = Math.max(0, Math.min(100, progress));
+ horizontalBar.setProgress(bounded);
+ if (bounded >= 100) {
+ hide();
+ } else {
+ show();
+ }
+ }
+
+ static String normalizeStyle(String value) {
+ String normalized = value == null ? "" : value.trim().toLowerCase(Locale.ROOT);
+ switch (normalized) {
+ case STYLE_NONE:
+ case STYLE_TOP_BAR:
+ case STYLE_BOTTOM_BAR:
+ case STYLE_CENTER_SPINNER:
+ case STYLE_FULLSCREEN_SPINNER:
+ return normalized;
+ default:
+ return STYLE_TOP_BAR;
+ }
+ }
+
+ private boolean isBarStyle() {
+ return STYLE_TOP_BAR.equals(style) || STYLE_BOTTOM_BAR.equals(style);
+ }
+
+ private void configureHorizontalBar(int thicknessDp, float density) {
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) horizontalBar.getLayoutParams();
+ params.width = FrameLayout.LayoutParams.MATCH_PARENT;
+ params.height = Math.max(1, Math.round(thicknessDp * density));
+ params.gravity = STYLE_BOTTOM_BAR.equals(style) ? Gravity.BOTTOM : Gravity.TOP;
+ horizontalBar.setLayoutParams(params);
+ horizontalBar.setMax(100);
+ horizontalBar.setIndeterminate(false);
+ }
+
+ private void configureSpinner(int spinnerSizeDp, float density) {
+ FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) spinner.getLayoutParams();
+ int pixels = Math.max(1, Math.round(spinnerSizeDp * density));
+ params.width = pixels;
+ params.height = pixels;
+ params.gravity = Gravity.CENTER;
+ spinner.setLayoutParams(params);
+ spinner.setIndeterminate(true);
+ }
+
+ private static int parseColor(String configuredColor) {
+ if (configuredColor != null) {
+ try {
+ return Color.parseColor(configuredColor.trim());
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+ return Color.parseColor("#6fc7ff");
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 4cce85b..f71ac23 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -20,6 +20,24 @@
android:max="100"
android:visibility="gone" />
+
+
+
+
+