Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 20 additions & 7 deletions app/src/main/java/de/starlightunit/wrapper/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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);

Expand Down Expand Up @@ -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);
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
18 changes: 18 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@
android:max="100"
android:visibility="gone" />

<FrameLayout
android:id="@+id/loading_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="false"
android:focusable="false"
android:visibility="gone">

<ProgressBar
android:id="@+id/loading_spinner"
style="?android:attr/progressBarStyleLarge"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout>

<LinearLayout
android:id="@+id/error_panel"
android:layout_width="match_parent"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.starlightunit.wrapper.ui;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public final class LoadingIndicatorControllerTest {

@Test
public void knownStylesArePreserved() {
assertEquals("none", LoadingIndicatorController.normalizeStyle("none"));
assertEquals("top-bar", LoadingIndicatorController.normalizeStyle("top-bar"));
assertEquals("bottom-bar", LoadingIndicatorController.normalizeStyle("bottom-bar"));
assertEquals("center-spinner", LoadingIndicatorController.normalizeStyle("center-spinner"));
assertEquals("fullscreen-spinner", LoadingIndicatorController.normalizeStyle("fullscreen-spinner"));
}

@Test
public void invalidOrEmptyStyleFallsBackToTopBar() {
assertEquals("top-bar", LoadingIndicatorController.normalizeStyle(null));
assertEquals("top-bar", LoadingIndicatorController.normalizeStyle(""));
assertEquals("top-bar", LoadingIndicatorController.normalizeStyle("unknown"));
}
}
Loading