diff --git a/IWantToHeal-Thor-v1.1.7.apk b/IWantToHeal-Thor-v1.1.7.apk new file mode 100644 index 0000000..b9d046d Binary files /dev/null and b/IWantToHeal-Thor-v1.1.7.apk differ diff --git a/android/app/build.gradle b/android/app/build.gradle index 6902e40..ef2fad8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.warren.iwanttoheal" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 85 - versionName "1.1.6" + versionCode 86 + versionName "1.1.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java b/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java index 727e9e5..2c440fb 100644 --- a/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java +++ b/android/app/src/main/java/com/warren/iwanttoheal/ControllerBridgeActivity.java @@ -7,19 +7,27 @@ import android.content.pm.PackageManager; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; +import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import com.getcapacitor.BridgeActivity; import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; public abstract class ControllerBridgeActivity extends BridgeActivity { public static final String EXTRA_INITIAL_URL = "com.warren.iwanttoheal.INITIAL_URL"; private static final String PREFS_NAME = "com.warren.iwanttoheal.performance"; private static final String CACHE_CLEARED_VERSION_KEY = "webview_cache_cleared_version"; + private static final float AXIS_DEAD_ZONE = 0.45f; private static final long DPAD_THROTTLE_MS = 55; private final long[] lastDpadDispatchAt = new long[16]; + private final Set activeMotionTokens = new HashSet<>(); + private final Map lastMotionDispatchAt = new HashMap<>(); @Override public void onCreate(Bundle savedInstanceState) { @@ -140,19 +148,98 @@ public abstract class ControllerBridgeActivity extends BridgeActivity { if (event.getAction() == KeyEvent.ACTION_DOWN) { boolean repeat = event.getRepeatCount() > 0; if (isDpadToken(token) && shouldThrottleDpad(token)) return true; - String script = - "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," - + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; - bridge.getWebView().post( - () -> { - bridge.getWebView().requestFocus(); - bridge.getWebView().evaluateJavascript(script, null); - } - ); + dispatchNativeControllerToken(token, repeat); } return true; } + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if ( + bridge == null + || event.getActionMasked() != MotionEvent.ACTION_MOVE + || !isControllerMotionEvent(event) + ) { + return super.dispatchGenericMotionEvent(event); + } + + Set currentTokens = new HashSet<>(); + addAxisTokens( + currentTokens, + event.getAxisValue(MotionEvent.AXIS_HAT_X), + "Button14", + "Button15" + ); + addAxisTokens( + currentTokens, + event.getAxisValue(MotionEvent.AXIS_HAT_Y), + "Button12", + "Button13" + ); + addAxisTokens( + currentTokens, + event.getAxisValue(MotionEvent.AXIS_X), + "Axis0-", + "Axis0+" + ); + addAxisTokens( + currentTokens, + event.getAxisValue(MotionEvent.AXIS_Y), + "Axis1-", + "Axis1+" + ); + + boolean hadMotionTokens = !activeMotionTokens.isEmpty(); + long now = SystemClock.uptimeMillis(); + for (String token : currentTokens) { + boolean repeat = activeMotionTokens.contains(token); + long lastDispatchAt = lastMotionDispatchAt.containsKey(token) + ? lastMotionDispatchAt.get(token) + : 0L; + if (!repeat || now - lastDispatchAt >= DPAD_THROTTLE_MS) { + dispatchNativeControllerToken(token, repeat); + lastMotionDispatchAt.put(token, now); + } + } + activeMotionTokens.clear(); + activeMotionTokens.addAll(currentTokens); + + if (currentTokens.isEmpty() && !hadMotionTokens) { + return super.dispatchGenericMotionEvent(event); + } + return true; + } + + private boolean isControllerMotionEvent(MotionEvent event) { + int source = event.getSource(); + return (source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK + || (source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD + || (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD; + } + + private void addAxisTokens( + Set tokens, + float value, + String negativeToken, + String positiveToken + ) { + if (value <= -AXIS_DEAD_ZONE) tokens.add(negativeToken); + if (value >= AXIS_DEAD_ZONE) tokens.add(positiveToken); + } + + private void dispatchNativeControllerToken(String token, boolean repeat) { + if (bridge == null || bridge.getWebView() == null) return; + String script = + "window.dispatchEvent(new CustomEvent('ashen-halls-native-controller'," + + "{detail:{token:'" + token + "',repeat:" + repeat + "}}));"; + bridge.getWebView().post( + () -> { + bridge.getWebView().requestFocus(); + bridge.getWebView().evaluateJavascript(script, null); + } + ); + } + private boolean shouldThrottleDpad(String token) { int buttonIndex = Integer.parseInt(token.substring("Button".length())); long now = SystemClock.uptimeMillis(); @@ -171,10 +258,13 @@ public abstract class ControllerBridgeActivity extends BridgeActivity { private String controllerToken(int keyCode) { switch (keyCode) { case KeyEvent.KEYCODE_BUTTON_A: + case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_NUMPAD_ENTER: return "Button0"; case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_ESCAPE: return "Button1"; case KeyEvent.KEYCODE_BUTTON_X: return "Button2"; diff --git a/src/App.css b/src/App.css index ae7b2eb..05c80d7 100644 --- a/src/App.css +++ b/src/App.css @@ -1433,6 +1433,14 @@ textarea:focus-visible, display: none; } +.game-shell.settings-shell { + padding: 6px 0; +} + +.game-shell.settings-shell > .app-header { + display: none; +} + .auth-shell { align-items: center; display: flex; @@ -1988,6 +1996,14 @@ h2 { outline-color: var(--gold); } +.settings-nav { + align-items: stretch; + display: grid; + gap: 8px; + grid-template-columns: minmax(92px, auto) minmax(0, 1fr); + margin-top: 0; +} + .settings-tabs, .talent-page-tabs { display: grid; @@ -1996,6 +2012,10 @@ h2 { margin-top: 14px; } +.settings-tabs { + margin-top: 0; +} + .settings-tabs button, .talent-page-tabs button { background: #15161c; @@ -2025,9 +2045,15 @@ h2 { height: calc(100dvh - 92px); } +.settings-shell .settings-screen { + height: calc(100dvh - 12px); + margin-top: 0; +} + .settings-tab-panel { flex: 1; min-height: 0; + overflow: auto; } .settings-bindings-panel { @@ -2045,6 +2071,7 @@ h2 { .settings-bindings-panel .binding-list { flex: 1; min-height: 0; + overflow-y: auto; } .equipment-screen .gear-summary, @@ -6962,6 +6989,178 @@ h2 { font-size: 16px; } + .settings-screen { + padding: 10px; + } + + .settings-shell .settings-screen { + height: calc(100dvh - 12px); + } + + .settings-nav { + gap: 6px; + grid-template-columns: minmax(76px, auto) minmax(0, 1fr); + } + + .settings-tabs { + gap: 6px; + } + + .settings-tabs button, + .settings-back-button { + font-size: 7px; + min-height: 32px; + padding: 5px 8px; + } + + .settings-tab-panel { + scrollbar-width: thin; + } + + .dual-screen-settings, + .controller-preferences { + gap: 10px; + margin: 8px 0 0; + padding: 10px; + } + + .dual-screen-settings.settings-tab-panel, + .controller-preferences.settings-tab-panel { + flex: 0 0 auto; + } + + .dual-screen-settings { + align-items: start; + grid-template-columns: minmax(0, 1fr) minmax(214px, auto); + } + + .dual-screen-settings h2, + .settings-heading h2 { + font-size: 12px; + } + + .dual-screen-settings p:not(.eyebrow), + .controller-preferences p:not(.eyebrow) { + font-size: 14px; + line-height: 1.15; + margin-top: 4px; + } + + .dual-screen-settings > small { + font-size: 13px; + } + + .dual-screen-actions { + align-content: start; + align-self: start; + gap: 7px; + min-width: 214px; + } + + .dual-screen-actions button, + .controller-preferences button { + min-height: 32px; + padding: 5px 8px; + } + + .controller-preferences { + align-items: stretch; + align-content: start; + grid-template-columns: minmax(0, 1fr) repeat(2, minmax(158px, auto)); + } + + .controller-preferences h3 { + color: var(--ink); + font-family: var(--pixel-font); + font-size: 10px; + line-height: 1.35; + margin: 0; + } + + .controller-icon-options { + gap: 6px; + grid-template-columns: minmax(112px, 1fr) repeat(3, minmax(104px, auto)); + padding: 6px; + } + + .controller-icon-options > span { + font-size: 7px; + } + + .controller-icon-options button { + font-size: 6px; + gap: 5px; + } + + .controller-style-preview .controller-face-icon { + height: 18px; + min-width: 18px; + } + + .controller-style-preview .controller-face-playstation { + font-size: 19px; + } + + .settings-bindings-panel .settings-heading { + padding: 4px 0 5px; + } + + .settings-heading > p { + font-size: 13px; + } + + .settings-heading .eyebrow { + display: none; + } + + .binding-tabs { + gap: 7px; + margin: 6px 0; + } + + .binding-tabs button { + min-height: 28px; + } + + .binding-list { + gap: 4px; + padding-right: 4px; + } + + .binding-list button { + min-height: 24px; + padding: 3px 8px; + } + + .binding-list button > span { + font-size: 12px; + } + + .binding-list kbd { + font-size: 6px; + min-height: 18px; + } + + .binding-list .controller-face-icon { + font-size: 10px; + height: 17px; + min-width: 17px; + } + + .binding-list .controller-face-playstation { + font-size: 18px; + } + + .settings-footer { + font-size: 13px; + margin-top: 6px; + padding-top: 6px; + } + + .settings-footer .text-button { + padding: 6px 9px; + } + .dungeon-card { gap: 12px; grid-template-columns: 72px 1fr; diff --git a/src/App.tsx b/src/App.tsx index d0803dd..d3e457d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -456,7 +456,7 @@ function App() { const canShowCloudSync = account.id !== -1 && cloudSync.available return ( -
+
+
+ +
- - {settingsTab === 'display' && (