Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb279f635 | ||
|
|
3fef1f3fa1 | ||
|
|
a4f26799ab | ||
|
|
08a6a3cba8 | ||
|
|
005fec2125 | ||
|
|
2e70b8ed73 | ||
|
|
74c0f2127a | ||
|
|
2bd7df5e1a | ||
|
|
b7b96875a1 | ||
|
|
51e5e3a2a4 | ||
|
|
c755d05473 | ||
|
|
aa883830ca | ||
|
|
45e39b7aaf | ||
|
|
bc99cacd02 | ||
|
|
2beeb715fd | ||
|
|
39469e6e8f | ||
|
|
15baa296bb | ||
|
|
e4a4775be5 | ||
|
|
77ef10d601 | ||
|
|
914b3588e1 | ||
|
|
2ade269be3 | ||
|
|
0d034b1996 |
No files matched your search
@@ -313,6 +313,11 @@ jobs:
|
||||
- name: Build the DiRT 3 / DiRT Showdown ws2_32 stand-in
|
||||
run: python3 tools/components/prepare.py dirt-ws2
|
||||
|
||||
- name: Cache graphics driver inputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/build/quest-inputs
|
||||
key: quest-inputs-${{ hashFiles('tools/quest/build-driver.py') }}
|
||||
- name: Prepare graphics driver variants
|
||||
run: python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
- name: Record and pack prepared components
|
||||
@@ -558,9 +563,11 @@ jobs:
|
||||
- name: Cache checksum-pinned pack inputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/droiddeck-pack-inputs
|
||||
key: pack-inputs-v1-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env') }}
|
||||
restore-keys: pack-inputs-v1-
|
||||
path: |
|
||||
~/.cache/droiddeck-pack-inputs
|
||||
~/.cache/droiddeck-build
|
||||
key: pack-inputs-v2-${{ hashFiles('tools/packs/build.sh', 'tools/packs/fetch-verified.sh', 'tools/packs/tracking/release.env', 'tools/powervr/release.json') }}
|
||||
restore-keys: pack-inputs-v2-
|
||||
- name: Build the add-on packs
|
||||
run: |
|
||||
python3 tools/powervr/fetch.py
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
labels: ${{ steps.runner.outputs.labels || '"ubuntu-24.04"' }}
|
||||
labels: ${{ steps.runner.outputs.labels || steps.fork.outputs.labels || '"ubuntu-24.04"' }}
|
||||
steps:
|
||||
- name: Pick the compile runner
|
||||
id: runner
|
||||
@@ -36,3 +36,25 @@ jobs:
|
||||
fi
|
||||
echo "labels=$labels" >> "$GITHUB_OUTPUT"
|
||||
echo "Compile runner: \`$labels\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
- name: Pick the fork runner
|
||||
id: fork
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
labels='"ubuntu-24.04"'
|
||||
gist=https://api.github.com/gists/0c7ed7ad6d6493de84f5123689c64ae6
|
||||
body=$(curl -fsS --max-time 15 -H "Authorization: Bearer $GH_TOKEN" "$gist" 2>/dev/null || curl -fsS --max-time 15 "$gist" 2>/dev/null || echo '{}')
|
||||
IDLE_AT=$(jq -r 'select(.owner.login == "maxjivi05") | .files["idle-at"].content // "0"' <<< "$body" 2>/dev/null || echo 0)
|
||||
echo "Heartbeat: ${IDLE_AT:-none}"
|
||||
age=-1
|
||||
if [[ "${IDLE_AT:-0}" =~ ^[0-9]+$ ]] && [ "${IDLE_AT:-0}" -gt 0 ]; then
|
||||
age=$(( $(date +%s) - IDLE_AT ))
|
||||
fi
|
||||
if [ "$age" -ge 0 ] && [ "$age" -lt 180 ]; then
|
||||
labels='"droiddeck-fork"'
|
||||
fi
|
||||
echo "labels=$labels" >> "$GITHUB_OUTPUT"
|
||||
echo "Compile runner: \`$labels\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
DroidDeck brings the SteamOS experience to Android: Steam Gaming Mode, full Linux desktop, integration with GOG, Epic Games, Xbox, and other storefronts, Steam VR, and more!
|
||||
|
||||
<p align="center"><b>100% open source. No ads. No telemetry. No source withheld.</b></p>
|
||||
<p align="center"><b>100% open source. No ads. No telemetry. No proprietary components. No source withheld.</b></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Droid-Deck/DroidDeck/releases/download/0.6.9/DroidDeck-0.6.9.apk"><img src="https://img.shields.io/badge/Download-DroidDeck%200.6.9%20APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Download DroidDeck 0.6.9"></a>
|
||||
@@ -79,7 +79,8 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
|
||||
- droiddeck-ntsync follows the [Linux](https://github.com/torvalds/linux) ntsync semantics in `Documentation/userspace-api/ntsync.rst` (GPL-2.0).
|
||||
- The proot fast path orders bindings like [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later).
|
||||
- DiRT 3 and DiRT Showdown keep their telemetry socket the way RoccoC's DiRTTelemetryErrorFix (MIT) does, based on the fix by Carsten Gottschling.
|
||||
- Games use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client keeps `TU_DEBUG=sysmem`.
|
||||
- Zink on PowerVR keeps colour textures in colour with Icenowy Zheng's upstream [Mesa](https://gitlab.freedesktop.org/mesa/mesa) fix d55418dae7 (MIT), carried as a runtime patch.
|
||||
- Games and AppImage programs such as emulators use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client and Chromium-based apps keep `TU_DEBUG=sysmem`.
|
||||
- Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose `311210.py` and `570940.py` fail under ARM64 Proton.
|
||||
- [ReSkate](https://github.com/Dingo-Shenanigans/ReSkate) (GPL-3.0) by Dingo Shenanigans uses the launch option from its README and D3D12 feature level 12_1.
|
||||
- The arm64 client's sign in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest).
|
||||
|
||||
@@ -131,8 +131,8 @@ android {
|
||||
applicationId "com.droiddeck.launcher"
|
||||
minSdkVersion 26
|
||||
targetSdkVersion 28
|
||||
versionCode 17
|
||||
versionName "0.6.9"
|
||||
versionCode 18
|
||||
versionName "0.7.0"
|
||||
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
|
||||
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
|
||||
buildConfigField "int", "PR_NUMBER", "${prNumber}"
|
||||
|
||||
@@ -603,11 +603,19 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
}
|
||||
|
||||
fun selectDriver(id: String, linux: Boolean) {
|
||||
if (!linux && id == TurnipDriver.AUTO) {
|
||||
DriverBundle.selectDisplay(activity, id)
|
||||
pairModeOpen = false
|
||||
lastAutoCheck = 0L
|
||||
setMode(auto = true)
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
val bundle = DriverBundle.containing(activity, id, linux)
|
||||
when {
|
||||
bundle != null -> useBundle(bundle)
|
||||
linux -> SessionPrefs.setLinuxDriver(activity, id)
|
||||
else -> SessionPrefs.setAndroidDriver(activity, id)
|
||||
else -> DriverBundle.selectDisplay(activity, id)
|
||||
}
|
||||
pairModeOpen = false
|
||||
setMode(auto = false, mix = true)
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -873,6 +874,7 @@ class FrontEndActivity : ComponentActivity() {
|
||||
android.widget.Toast.makeText(this, R.string.game_link_failed, android.widget.Toast.LENGTH_LONG).show()
|
||||
return false
|
||||
}
|
||||
Library.recordPlayed(this, game)
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.putExtra(SessionActivity.EXTRA_STEAMVR, steamVr)
|
||||
.returnTo(returnHome)
|
||||
@@ -888,7 +890,15 @@ class FrontEndActivity : ComponentActivity() {
|
||||
putExtra(SessionService.EXTRA_STEAM_URL,
|
||||
com.droiddeck.launcher.frontend.GameLaunchLink.steamUrl(game.gameIdString))
|
||||
}
|
||||
}, steamSession = true)
|
||||
}
|
||||
.playing(game), steamSession = true)
|
||||
}
|
||||
|
||||
private fun Intent.playing(game: Library.SteamGame): Intent =
|
||||
apply { if (game.library == Library.ADDED) putExtra(SessionActivity.EXTRA_PLAYED_GAME, game.gameIdString) }
|
||||
|
||||
private fun recordPlayed(intent: Intent) {
|
||||
intent.getStringExtra(SessionActivity.EXTRA_PLAYED_GAME)?.let { Library.recordPlayed(this, it) }
|
||||
}
|
||||
|
||||
private fun Intent.returnTo(returnHome: Boolean): Intent {
|
||||
@@ -915,14 +925,17 @@ class FrontEndActivity : ComponentActivity() {
|
||||
.putExtra(SessionService.EXTRA_PROGRAM, command.program)
|
||||
.putExtra(SessionService.EXTRA_PROGRAM_ARGS, command.args.toTypedArray())
|
||||
.putExtra(SessionService.EXTRA_GAME_ID, com.droiddeck.launcher.session.NoSteamLaunch.scope(game))
|
||||
.playing(game)
|
||||
when {
|
||||
!SessionState.running -> startSessionOrWait(next)
|
||||
SessionState.stopRequested -> android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
command.program == com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET && handToBattleNet(command.args, returnHome) { pendingSwitch = next } -> {}
|
||||
SessionState.mode == SessionService.MODE_RUN && SessionState.program == command.program && SessionState.programArgs == command.args ->
|
||||
command.program == com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET && handToBattleNet(command.args, next, returnHome) -> {}
|
||||
SessionState.mode == SessionService.MODE_RUN && SessionState.program == command.program && SessionState.programArgs == command.args -> {
|
||||
recordPlayed(next)
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
else -> pendingSwitch = next
|
||||
}
|
||||
}
|
||||
@@ -1123,6 +1136,10 @@ class FrontEndActivity : ComponentActivity() {
|
||||
applyLauncherFullscreen()
|
||||
updates.start()
|
||||
setContent {
|
||||
val prootPack by com.droiddeck.launcher.core.AddonPack.PROOT.state.collectAsState()
|
||||
androidx.compose.runtime.LaunchedEffect(prootPack) {
|
||||
if (prootPack == com.droiddeck.launcher.core.AddonPack.State.Ready && installed == null) refresh()
|
||||
}
|
||||
DroidDeckTheme(theme, appScale = appScale) {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize().exposeTestTags()) {
|
||||
if (decky.deckyPendingZip != null) com.droiddeck.launcher.ui.DeckyZipWarningDialog(
|
||||
@@ -2276,14 +2293,14 @@ class FrontEndActivity : ComponentActivity() {
|
||||
when {
|
||||
!SessionState.running -> startSessionOrWait(next)
|
||||
SessionState.stopRequested -> android.widget.Toast.makeText(this, R.string.game_link_session_busy, android.widget.Toast.LENGTH_LONG).show()
|
||||
handToBattleNet(full) { pendingSwitch = next } -> {}
|
||||
handToBattleNet(full, next) -> {}
|
||||
else -> pendingSwitch = next
|
||||
}
|
||||
}
|
||||
}, "battlenet-open").start()
|
||||
}
|
||||
|
||||
private fun handToBattleNet(args: List<String>, returnHome: Boolean = false, otherwise: () -> Unit): Boolean {
|
||||
private fun handToBattleNet(args: List<String>, next: Intent, returnHome: Boolean = false): Boolean {
|
||||
if (SessionState.mode != SessionService.MODE_RUN || SessionState.program != com.droiddeck.launcher.session.NoSteamLaunch.BATTLENET) return false
|
||||
val library = args.takeIf { it.firstOrNull() == "--library" }?.getOrNull(1)
|
||||
val sessionLibrary = SessionState.programArgs.takeIf { it.firstOrNull() == "--library" }?.getOrNull(1)
|
||||
@@ -2294,10 +2311,13 @@ class FrontEndActivity : ComponentActivity() {
|
||||
val handed = request.isEmpty() || com.droiddeck.launcher.stores.battlenet.BattleNetClient.request(LinuxRuntime.sessionRoot(app), request)
|
||||
ui.post {
|
||||
if (isDestroyed) return@post
|
||||
if (!handed) otherwise()
|
||||
else startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
if (!handed) pendingSwitch = next
|
||||
else {
|
||||
recordPlayed(next)
|
||||
startActivity(Intent(this, SessionActivity::class.java).setAction(SessionService.ACTION_RESUME)
|
||||
.returnTo(returnHome)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP))
|
||||
}
|
||||
}
|
||||
}, "battlenet-request").start()
|
||||
return true
|
||||
|
||||
@@ -178,6 +178,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
private var unfoldedPanel by mutableStateOf<OnScreenControls?>(null)
|
||||
private var unfoldedColumn: android.widget.LinearLayout? = null
|
||||
private var unfoldedDeckSurface: com.droiddeck.launcher.ui.InputSurfaceView? = null
|
||||
private var unfoldedDeckPanel by mutableStateOf<com.droiddeck.launcher.input.DeckPanel?>(null)
|
||||
private var deckPanelLayouts: Map<com.droiddeck.launcher.input.DeckPanel, com.droiddeck.launcher.input.SavedInputLayout?> = emptyMap()
|
||||
private lateinit var sessionRoot: FrameLayout
|
||||
private var inputRouter: InputRouter? = null
|
||||
private var physicalInputBindings: com.droiddeck.launcher.input.PhysicalInputBindings? = null
|
||||
@@ -565,6 +568,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var sessionActivityDisplayId = Display.DEFAULT_DISPLAY
|
||||
private lateinit var displayManager: DisplayManager
|
||||
private var secondScreenPresentation: SecondScreenPresentation? = null
|
||||
private val inputWindowDisplays = mutableListOf<Int>()
|
||||
private val bottomScreenListener: () -> Unit = {
|
||||
if (secondScreenPresentation?.display?.displayId?.let(com.droiddeck.launcher.input.DualScreen::taken) == true) closeSecondScreen(reset = false)
|
||||
updateBottomGame()
|
||||
@@ -977,7 +981,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
} else {
|
||||
com.droiddeck.launcher.ui.FocusGlideHost(androidx.compose.ui.Modifier.fillMaxSize()) {
|
||||
CursorOverlay({ cursorPos }, cursorVisible && !uiPointer && !bottomOutputActive,
|
||||
CursorOverlay({ cursorPos }, cursorVisible && (!uiPointer || onlyPcKeyboardOpen()) && !bottomOutputActive,
|
||||
resources.displayMetrics.density,
|
||||
cursorImage, cursorHotX, cursorHotY, cursorImageScale)
|
||||
if (memoryHud.text.isNotEmpty()) HudText(memoryHud.text, hudColor, hudOpacity)
|
||||
@@ -1222,7 +1226,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
textureLodBias = if (drawerGameProfileId == null) textureLodBias else drawerTextureLodBias,
|
||||
frameGen = frameGen,
|
||||
lossless = lossless,
|
||||
oscMode = oscMode, onScreenButtonsVisible = onScreenButtonsVisible, suspendPolicy = suspendPolicy,
|
||||
oscMode = oscMode, suspendPolicy = suspendPolicy,
|
||||
steamDownloadsInBackground = steamDownloadsInBackground, touchMode = touchMode,
|
||||
micAvailable = SessionState.micAvailable && !SessionState.suspended,
|
||||
micEnabled = SessionState.micEnabled,
|
||||
@@ -1443,6 +1447,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
},
|
||||
onClose = { drawerOpen = false },
|
||||
onDismissScreen = if (docked) ({ selectSecondScreenMode(SecondScreenMode.NONE) }) else null,
|
||||
components = drawerComponents,
|
||||
onComponentsRefresh = {
|
||||
refreshDrawerComponents()
|
||||
@@ -2225,6 +2230,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
intent.getStringExtra(SessionService.EXTRA_GAME_ID),
|
||||
sessionActivityDisplayId,
|
||||
)
|
||||
intent.getStringExtra(EXTRA_PLAYED_GAME)?.let { com.droiddeck.launcher.frontend.Library.recordPlayed(this, it) }
|
||||
}
|
||||
applyFrameGen()
|
||||
applyGameAlignment()
|
||||
@@ -2315,9 +2321,11 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (::touchscreenGestures.isInitialized) touchscreenGestures.cancel()
|
||||
if (::touchpad.isInitialized) touchpad.cancel()
|
||||
if (::directTouchHold.isInitialized) directTouchHold.cancel()
|
||||
if (::twoFingerGestures.isInitialized) twoFingerGestures.cancel()
|
||||
if (::scrollTouch.isInitialized) scrollTouch.cancel()
|
||||
scrollTouchGesture = false
|
||||
gesturePointerReady = false
|
||||
if (sessionTouchDown) dismissTouch = true
|
||||
}
|
||||
|
||||
private fun panelBounds(): android.graphics.Rect = if (Build.VERSION.SDK_INT >= 30) {
|
||||
@@ -2826,6 +2834,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return onScreenControls?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
steamTouchControls?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedPanel?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedDeckSurface?.let { takes(it, it::takesTouchAt) } == true ||
|
||||
unfoldedColumn?.getChildAt(0)?.let { takes(it) { _, _ -> true } } == true
|
||||
}
|
||||
|
||||
@@ -3378,6 +3387,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
var presentation = secondScreenPresentation
|
||||
if (presentation?.display?.displayId != target.displayId) {
|
||||
if (target.displayId in inputWindowDisplays) {
|
||||
panelMode(mode)
|
||||
return
|
||||
}
|
||||
dropDockedDrawer()
|
||||
presentation?.closeControls()
|
||||
presentation = secondScreenPresentation(target)
|
||||
@@ -3518,8 +3531,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val main = window.decorView.display
|
||||
if (role == InputRole.PRIMARY) return bottomGame?.display ?: main
|
||||
if (bottomGame != null) return main
|
||||
return displayManager.getDisplay(selectedSecondScreenDisplay)
|
||||
?: secondScreenDisplays.firstOrNull()?.let { displayManager.getDisplay(it.id) }
|
||||
return com.droiddeck.launcher.input.SecondScreenDisplays.editorCandidates(selectedSecondScreenDisplay,
|
||||
secondScreenDisplays.map { it.id }, com.droiddeck.launcher.input.DualScreen::taken).firstNotNullOfOrNull(displayManager::getDisplay)
|
||||
}
|
||||
|
||||
fun inputWindowShown(displayId: Int) { inputWindowDisplays += displayId }
|
||||
|
||||
fun inputWindowClosed(displayId: Int) {
|
||||
inputWindowDisplays -= displayId
|
||||
if (started && displayId !in inputWindowDisplays && secondScreenMode != SecondScreenMode.NONE && secondScreenPresentation == null) showSecondScreen(secondScreenMode)
|
||||
}
|
||||
|
||||
private fun refreshInputProfile() {
|
||||
@@ -3530,6 +3550,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
adaptiveSticks = primaryInputLayout?.adaptiveSticks
|
||||
quickButtons = primaryInputLayout?.takeIf { it.editable }?.let { l -> l.quickButtons.associateWith { it !in l.style.hidden } }.orEmpty()
|
||||
secondaryInputLayout = library.panelLayout(desktop, secondScreenMode.panel(this))
|
||||
deckPanelLayouts = com.droiddeck.launcher.input.DeckPanel.entries.associateWith { library.panelLayout(desktop, it.bindings) }
|
||||
inputUsesLocalActions = InputControl.padControls.any { inputProfile.action(InputRole.PHYSICAL, it) !is InputAction.Native }
|
||||
inputUsesPointer = inputProfile.bindings.values.any { bindings ->
|
||||
bindings.values.any { it is InputAction.Pointer || it is InputAction.MouseButton || it is InputAction.Scroll }
|
||||
@@ -3543,6 +3564,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
onScreenControls?.updateLayout(primaryInputLayout)
|
||||
bottomInputControls?.updateLayout(primaryInputLayout)
|
||||
unfoldedPanel?.updateLayout(primaryInputLayout)
|
||||
if (unfoldedPanel != null) applyUnfoldedControls()
|
||||
showUnfoldedDeckPanel()
|
||||
updateOnScreenPad()
|
||||
secondScreenPresentation?.refreshControls()
|
||||
}
|
||||
@@ -3594,11 +3617,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
Log.i(TAG, if (wanted) "unfolded controls: game ${width}x$gameHeight, controls ${width}x${height - gameHeight}"
|
||||
else "unfolded controls: game full screen")
|
||||
}
|
||||
val dock = com.droiddeck.launcher.input.OnScreenLayout.Dock(!com.droiddeck.launcher.session.SessionDisplay.foldable(this), SessionState.deckPad)
|
||||
val portrait = !com.droiddeck.launcher.session.SessionDisplay.foldable(this)
|
||||
val dock = if (SessionState.deckPad) com.droiddeck.launcher.input.OnScreenLayout.Dock.deck(portrait, deckPanelLayouts[com.droiddeck.launcher.input.DeckPanel.GAMEPAD])
|
||||
else com.droiddeck.launcher.input.OnScreenLayout.Dock(portrait, false)
|
||||
var panel = unfoldedPanel
|
||||
if (panel != null && (!wanted || panel.dock != dock)) {
|
||||
sessionRoot.removeView(unfoldedColumn)
|
||||
unfoldedColumn = null
|
||||
unfoldedDeckSurface = null
|
||||
unfoldedDeckPanel = null
|
||||
unfoldedPanel = null
|
||||
panel = null
|
||||
if (!wanted) updateOnScreenControls()
|
||||
@@ -3615,14 +3642,36 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
sessionRoot.addView(column, sessionRoot.indexOfChild(surfaceView) + 1)
|
||||
unfoldedColumn = column
|
||||
unfoldedPanel = panel
|
||||
showUnfoldedDeckPanel()
|
||||
updateOnScreenControls()
|
||||
}
|
||||
unfoldedColumn?.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, height - gameHeight, Gravity.BOTTOM)
|
||||
}
|
||||
|
||||
private fun unfoldedBar(): View = com.droiddeck.launcher.ui.bottomScreenBarView(this) {
|
||||
com.droiddeck.launcher.ui.sessionBarModel(getString(R.string.deck_tab_gamepad), { chooseUnfoldedControls(false) },
|
||||
{ drawerOpen = true }, ::tapSteamButton, ::tapQam)
|
||||
val deck = unfoldedDeckPanel
|
||||
com.droiddeck.launcher.ui.sessionBarModel(getString(if (deck != null) R.string.second_mode_deck_controls else R.string.deck_tab_gamepad),
|
||||
{ chooseUnfoldedControls(false) }, { drawerOpen = true }, ::tapSteamButton, ::tapQam,
|
||||
tabs = deck?.let { com.droiddeck.launcher.ui.deckPanelTabs(this, it, ::selectDeckPanel) })
|
||||
}
|
||||
|
||||
private fun showUnfoldedDeckPanel() {
|
||||
val column = unfoldedColumn
|
||||
val panel = unfoldedPanel
|
||||
val selected = SessionPrefs.deckPanel(this).takeIf { column != null && panel?.dock?.grips == true }
|
||||
unfoldedDeckPanel = selected
|
||||
val layout = selected?.takeIf { it != com.droiddeck.launcher.input.DeckPanel.GAMEPAD }?.let { deckPanelLayouts[it] }
|
||||
if (layout != null && unfoldedDeckSurface?.tag == layout) return
|
||||
unfoldedDeckSurface?.let { it.releaseInputs(); column?.removeView(it) }
|
||||
unfoldedDeckSurface = null
|
||||
if (column == null || panel == null) return
|
||||
if (layout == null) { panel.visibility = View.VISIBLE; return }
|
||||
panel.releaseAll()
|
||||
panel.visibility = View.GONE
|
||||
unfoldedDeckSurface = com.droiddeck.launcher.ui.InputSurfaceView(this, padBridge, layout.elements, InputRole.PRIMARY, layout.style, haptics = true).also {
|
||||
it.tag = layout
|
||||
column.addView(it, android.widget.LinearLayout.LayoutParams(android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f))
|
||||
}
|
||||
}
|
||||
|
||||
private fun bottomKeyboardPresentation(): SecondScreenPresentation? {
|
||||
@@ -3918,9 +3967,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private var lockedSwipeX = 0f
|
||||
private var lockedSwipeY = 0f
|
||||
private var dismissTouch = false
|
||||
private var sessionTouchDown = false
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE)) return onMouse(event)
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
sessionTouchDown = true
|
||||
dismissTouch = false
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> sessionTouchDown = false
|
||||
}
|
||||
if (drawerOpen && drawerDocked && event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
drawerLayerOpen = false
|
||||
drawerOpenState = false
|
||||
@@ -3932,7 +3989,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (event.actionMasked == MotionEvent.ACTION_UP || event.actionMasked == MotionEvent.ACTION_CANCEL) dismissTouch = false
|
||||
return true
|
||||
}
|
||||
val uiBlocking = drawerOpen || pcKeyboardOpen || achievementsOpen || gestureSettingsOpen || inputSettingsOpen || pipUi || SessionState.suspended
|
||||
val uiBlocking = drawerOpen || achievementsOpen || gestureSettingsOpen || inputSettingsOpen || pipUi || SessionState.suspended
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN && !uiBlocking && routedTouchMode != SessionPrefs.TOUCH_GESTURES) {
|
||||
scrollTouchGesture = scrollTouchMode()
|
||||
scrollRestX = 0f
|
||||
@@ -3960,7 +4017,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
com.droiddeck.launcher.input.TouchRouter.Route.SCROLL -> scrollTouch.onTouch(event)
|
||||
else -> {
|
||||
if (com.droiddeck.launcher.input.TouchRouter.steamKeyboardZone(snapshot)) {
|
||||
val wasActive = threeFingerTap.active
|
||||
val wasActive = threeFingerTap.active && event.actionMasked != MotionEvent.ACTION_DOWN
|
||||
if (threeFingerTap.onTouch(event)) padBridge?.triggerSteamKeyboard()
|
||||
if (threeFingerTap.active && !wasActive) {
|
||||
if (route == com.droiddeck.launcher.input.TouchRouter.Route.TOUCHPAD) touchpad.cancel()
|
||||
@@ -4036,9 +4093,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return true
|
||||
}
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) lockedSwipeUsed = false
|
||||
if (lockedSwipeUsed && (event.actionMasked != MotionEvent.ACTION_MOVE || !programLocksPointer())) return true
|
||||
if (programLocksPointer() && event.actionMasked == MotionEvent.ACTION_MOVE) {
|
||||
if (lockedSwipeUsed) directTouchHold.cancelPendingHold() else directTouchHold.cancelTouch()
|
||||
lockedSwipeUsed = true
|
||||
directTouchHold.cancelPendingHold()
|
||||
val scale = sceneScale(rect)
|
||||
sendSceneDelta((event.x - swipeX) * scale, (event.y - swipeY) * scale)
|
||||
return true
|
||||
@@ -4155,6 +4213,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
applyPlatformPointer(appUiOwnsPointer())
|
||||
}
|
||||
|
||||
private fun onlyPcKeyboardOpen(): Boolean =
|
||||
pcKeyboardOpen && !drawerOpen && !achievementsOpen && !inputSettingsOpen && !pipUi && !glassesOutputActive && !touchEditorOpen && !wowChatOnTop
|
||||
|
||||
private fun appUiOwnsPointer(): Boolean =
|
||||
drawerOpen || pcKeyboardOpen || achievementsOpen || inputSettingsOpen || pipUi || glassesOutputActive || bottomOutputActive || touchEditorOpen || wowChatOnTop
|
||||
|
||||
@@ -4245,7 +4306,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
updateOnScreenPad()
|
||||
val c = onScreenControls
|
||||
glassesSlot = when {
|
||||
c == null || c.visibility != View.VISIBLE -> 0
|
||||
c == null || c.visibility != View.VISIBLE || c.isPinnedOnly -> 0
|
||||
c.isButtonsOnly -> -1
|
||||
primaryInputLayout?.let { l -> l.elements.any { it.quickButton == InputCommand.KEYBOARD && !l.style.hides(it) } }
|
||||
?: (controllerSettings?.keyboardButton != false) -> 2
|
||||
@@ -4260,12 +4321,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
?.let { FileUtils.readString(it)?.trim()?.lowercase() }
|
||||
?: SessionPrefs.oscMode(this)
|
||||
val controls = onScreenControls ?: return
|
||||
fun showPinned() {
|
||||
controls.setPinnedOnly(true)
|
||||
val pinned = controls.hasPinned && !pipUi && !inputSettingsOpen
|
||||
if (!pinned) controls.releaseAll()
|
||||
controls.visibility = if (pinned) View.VISIBLE else View.GONE
|
||||
}
|
||||
if (pipUi || unfoldedPanel != null || pcKeyboardOpen || inputSettingsOpen ||
|
||||
(SessionState.mode == SessionService.MODE_STEAM && !SessionPrefs.builtinPad(this))) {
|
||||
onScreenButtonsVisible = false
|
||||
hideBottomControls()
|
||||
controls.releaseAll()
|
||||
controls.visibility = View.GONE
|
||||
showPinned()
|
||||
steamTouchControls?.let { it.cancelEditing(); it.releaseAll(); it.visibility = View.GONE }
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = false
|
||||
applyTouchRouting()
|
||||
@@ -4298,7 +4364,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val drawn = show && !keyboardCoversPad()
|
||||
if (steam != null) {
|
||||
hideBottomControls()
|
||||
if (controls.visibility == View.VISIBLE) { controls.releaseAll(); controls.visibility = View.GONE }
|
||||
showPinned()
|
||||
if (!drawn) { steam.cancelEditing(); steam.releaseAll() }
|
||||
steam.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
com.droiddeck.launcher.input.SteamTouchDevice.current?.plugged = show
|
||||
@@ -4307,7 +4373,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return
|
||||
}
|
||||
val bottomRoot = bottomGame?.surfaceView?.parent as? android.view.ViewGroup
|
||||
if (drawn && bottomRoot != null) {
|
||||
if (bottomRoot != null && (drawn || controls.hasPinned)) {
|
||||
controls.releaseAll(); controls.visibility = View.GONE
|
||||
var bottom = bottomInputControls
|
||||
if (bottom?.parent !== bottomRoot) {
|
||||
@@ -4320,13 +4386,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
bottom.updateLayout(primaryInputLayout)
|
||||
bottom.setButtonsOnly(buttonsOnly)
|
||||
bottom.setPinnedOnly(!drawn)
|
||||
bottom.visibility = View.VISIBLE
|
||||
return
|
||||
}
|
||||
hideBottomControls()
|
||||
if (drawn == (controls.visibility == View.VISIBLE)) return
|
||||
if (!drawn) controls.releaseAll()
|
||||
controls.visibility = if (drawn) View.VISIBLE else View.GONE
|
||||
val wasDrawn = controls.visibility == View.VISIBLE && !controls.isPinnedOnly
|
||||
if (!drawn) showPinned() else { controls.setPinnedOnly(false); controls.visibility = View.VISIBLE }
|
||||
if (drawn == wasDrawn) return
|
||||
Log.i(TAG, "on-screen controls " + when {
|
||||
!drawn -> "hidden"
|
||||
buttonsOnly -> "Steam + QAM"
|
||||
@@ -5069,7 +5136,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(SecondScreenMode.KEYBOARD_TRACKPAD.label), listOf(keyboardExtra)))
|
||||
SecondScreenMode.DECK_CONTROLS -> com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(SecondScreenMode.DECK_CONTROLS.label),
|
||||
tabs = com.droiddeck.launcher.ui.deckPanelTabs(this@SessionActivity, SessionPrefs.deckPanel(this@SessionActivity), ::selectDeckPanel)))
|
||||
else -> Unit
|
||||
else -> com.droiddeck.launcher.ui.BottomScreenBar(bar(getString(secondScreenMode.label)))
|
||||
}
|
||||
androidx.compose.runtime.key(profileLayout) {
|
||||
androidx.compose.ui.viewinterop.AndroidView(
|
||||
@@ -5390,6 +5457,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
const val EXTRA_RETURN_HOME = "returnHome"
|
||||
const val EXTRA_LINK_TASK = "linkTask"
|
||||
const val EXTRA_STEAMVR = "steamVr"
|
||||
const val EXTRA_PLAYED_GAME = "com.droiddeck.launcher.PLAYED_GAME"
|
||||
const val EXTRA_REPAIR_RUNTIME = "repairRuntime"
|
||||
private const val LEAVE_TIMEOUT_MS = 1_500L
|
||||
private const val TEXT_FIELD_TAP_MS = 1_000L
|
||||
|
||||
@@ -18,9 +18,10 @@ object Library {
|
||||
val source: String = SOURCE_STEAM,
|
||||
val storeId: String? = null,
|
||||
val logo: File? = null,
|
||||
val installedAt: Long = 0L,
|
||||
) {
|
||||
fun withArt(art: File?, hero: File?, icon: File?, logo: File? = this.logo) =
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId, logo)
|
||||
SteamGame(appId, name, art, library, gameId, hero, lastPlayed, gameFiles, protonPrefix, icon, source, storeId, logo, installedAt)
|
||||
|
||||
val gameIdString: String get() = java.lang.Long.toUnsignedString(gameId)
|
||||
val launchKey: String get() = if (storeId != null) "$source:$storeId" else gameIdString
|
||||
@@ -31,6 +32,7 @@ object Library {
|
||||
}
|
||||
}
|
||||
const val ADDED = "added"
|
||||
private const val PLAYED = "added_games_played"
|
||||
const val EPIC_LAUNCHER = "epic-launcher"
|
||||
const val EPIC_LAUNCHER_NAME = "Epic Games Launcher"
|
||||
const val BATTLENET_LAUNCHER = "battlenet-launcher"
|
||||
@@ -221,6 +223,18 @@ object Library {
|
||||
return (steam + own.map { g -> addedGame(context, g, roots, played[g.appId] ?: 0L) }).distinctBy { it.gameId }
|
||||
}
|
||||
|
||||
fun recordPlayed(context: Context, game: SteamGame) {
|
||||
if (game.library == ADDED) recordPlayed(context, game.gameIdString)
|
||||
}
|
||||
|
||||
fun recordPlayed(context: Context, gameId: String) {
|
||||
context.getSharedPreferences(PLAYED, Context.MODE_PRIVATE).edit()
|
||||
.putLong(gameId, System.currentTimeMillis() / 1000).apply()
|
||||
}
|
||||
|
||||
internal fun recordedPlay(context: Context, gameId: Long): Long =
|
||||
context.getSharedPreferences(PLAYED, Context.MODE_PRIVATE).getLong(java.lang.Long.toUnsignedString(gameId), 0L)
|
||||
|
||||
internal fun shortcutsLastPlayed(root: File): Map<Long, Long> = runCatching {
|
||||
val config = AddedGames.accountConfig(root) ?: return emptyMap()
|
||||
val bytes = File(config, "shortcuts.vdf").takeIf { it.isFile }?.readBytes() ?: return emptyMap()
|
||||
@@ -249,12 +263,13 @@ object Library {
|
||||
AddedGameArt.resolve(context, g).let { art ->
|
||||
SteamGame(
|
||||
g.steamAppId ?: g.appId.toInt(), g.name, art.portrait ?: art.header, ADDED, g.gameId,
|
||||
hero = art.hero ?: art.header, lastPlayed = lastPlayed, gameFiles = g.folder,
|
||||
hero = art.hero ?: art.header, lastPlayed = maxOf(lastPlayed, recordedPlay(context, g.gameId)), gameFiles = g.folder,
|
||||
protonPrefix = if (g.source == com.droiddeck.launcher.stores.Store.BATTLENET.id) File(com.droiddeck.launcher.runtime.BattleNet.prefix(context), "pfx").takeIf { it.isDirectory }
|
||||
else protonPrefix(context, g.steamAppId?.toLong() ?: g.appId, libraries = roots),
|
||||
icon = art.icon?.takeIf { it.extension.lowercase() != "ico" },
|
||||
source = g.source, storeId = g.storeId,
|
||||
logo = art.logo,
|
||||
installedAt = g.folder.lastModified(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ object LibraryCache {
|
||||
source = o.optString("source").ifEmpty { if (o.getString("library") == Library.ADDED) Library.ADDED else Library.SOURCE_STEAM },
|
||||
storeId = o.optString("storeId").ifEmpty { null },
|
||||
logo = o.file("logo"),
|
||||
installedAt = o.optLong("installedAt"),
|
||||
)
|
||||
} to x64
|
||||
}.getOrElse { e -> Log.w(TAG, "unreadable, ignored: ${e.message}"); Pair(emptyList(), false) }
|
||||
@@ -44,7 +45,7 @@ object LibraryCache {
|
||||
JSONObject().put("appId", g.appId).put("name", g.name).put("library", g.library).put("gameId", g.gameId)
|
||||
.put("lastPlayed", g.lastPlayed).putFile("art", g.art).putFile("hero", g.hero)
|
||||
.putFile("gameFiles", g.gameFiles).putFile("protonPrefix", g.protonPrefix).putFile("icon", g.icon).putFile("logo", g.logo)
|
||||
.put("source", g.source).apply { g.storeId?.let { put("storeId", it) } },
|
||||
.put("source", g.source).put("installedAt", g.installedAt).apply { g.storeId?.let { put("storeId", it) } },
|
||||
)
|
||||
val f = file(context)
|
||||
val tmp = File(f.path + ".tmp")
|
||||
|
||||
@@ -159,6 +159,12 @@ object DriverBundle {
|
||||
SessionPrefs.setLinuxDriver(context, bundle.linuxId)
|
||||
}
|
||||
|
||||
fun selectDisplay(context: Context, id: String) {
|
||||
val left = active(context)
|
||||
SessionPrefs.setAndroidDriver(context, id)
|
||||
if (left != null && left.androidId != id) SessionPrefs.setLinuxDriver(context, "")
|
||||
}
|
||||
|
||||
fun remove(context: Context, bundle: Bundle) {
|
||||
TurnipDriver(context).remove(bundle.androidId)
|
||||
LinuxVulkanDriverManager(context).removeDriver(bundle.linuxId)
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import android.view.Display
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
@@ -33,6 +34,8 @@ object BottomScreenGuests {
|
||||
else -> null
|
||||
}
|
||||
|
||||
internal fun current(arrivedAt: Long, ownShownAt: Long?): Boolean = ownShownAt == null || arrivedAt >= ownShownAt
|
||||
|
||||
internal fun ignorable(type: Int): Boolean = type == AccessibilityWindowInfo.TYPE_INPUT_METHOD ||
|
||||
type == AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY ||
|
||||
type == AccessibilityWindowInfo.TYPE_SPLIT_SCREEN_DIVIDER ||
|
||||
@@ -43,7 +46,7 @@ object BottomScreenGuests {
|
||||
}
|
||||
|
||||
class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
private class Arrival(val pkg: String)
|
||||
private class Arrival(val pkg: String, val at: Long)
|
||||
|
||||
private val thread = HandlerThread("bottom-screen").apply { start() }
|
||||
private val handler = Handler(thread.looper)
|
||||
@@ -52,6 +55,8 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
private var homes: Set<String> = emptySet()
|
||||
private var keyboards: Set<String> = emptySet()
|
||||
private var holders: Map<Int, String?> = emptyMap()
|
||||
private var ownWindows: Map<Int, Set<Int>> = emptyMap()
|
||||
private val ownShownAt = mutableMapOf<Int, Long>()
|
||||
private val check = Runnable { evaluate() }
|
||||
|
||||
fun start() {
|
||||
@@ -72,7 +77,7 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> {
|
||||
val pkg = event.packageName?.toString() ?: return
|
||||
if (event.displayId == Display.DEFAULT_DISPLAY || pkg == own || pkg == SYSTEM_UI) return
|
||||
arrivals[event.displayId] = Arrival(pkg)
|
||||
arrivals[event.displayId] = Arrival(pkg, event.eventTime)
|
||||
}
|
||||
AccessibilityEvent.TYPE_WINDOWS_CHANGED -> Unit
|
||||
else -> return
|
||||
@@ -97,19 +102,25 @@ class BottomScreenWatch(private val service: AccessibilityService) {
|
||||
val all = runCatching { service.windowsOnAllDisplays }.getOrNull() ?: return
|
||||
val displays = service.getSystemService(DisplayManager::class.java) ?: return
|
||||
val taken = mutableMapOf<Int, String?>()
|
||||
val shown = mutableMapOf<Int, Set<Int>>()
|
||||
val now = SystemClock.uptimeMillis()
|
||||
for (i in 0 until all.size()) {
|
||||
val id = all.keyAt(i)
|
||||
if (id == Display.DEFAULT_DISPLAY) continue
|
||||
val display = displays.getDisplay(id)?.takeIf(DualScreen::builtIn) ?: continue
|
||||
val windows = all.valueAt(i)
|
||||
val topPkg = windows.asSequence()
|
||||
.filter { !BottomScreenGuests.ignorable(it.type) && cover(it, display) >= BottomScreenGuests.MIN_COVER }
|
||||
.sortedByDescending { it.layer }
|
||||
.firstNotNullOfOrNull { packageOf(it)?.takeUnless { pkg -> pkg == own } }
|
||||
val arrival = seen[id]
|
||||
if (topPkg != null && arrival != null) arrivals.remove(id, arrival)
|
||||
.map { it to packageOf(it) }
|
||||
val ours = windows.filter { it.second == own }.map { it.first.id }.toSet()
|
||||
if (!ownWindows[id].orEmpty().containsAll(ours)) ownShownAt[id] = now
|
||||
shown[id] = ours
|
||||
val topPkg = windows.firstNotNullOfOrNull { it.second?.takeUnless { pkg -> pkg == own } }
|
||||
val arrival = seen[id]?.takeIf { BottomScreenGuests.current(it.at, ownShownAt[id]) }
|
||||
seen[id]?.takeIf { topPkg != null || arrival == null }?.let { arrivals.remove(id, it) }
|
||||
BottomScreenGuests.holder(topPkg, arrival?.pkg?.takeUnless { it in keyboards }, homes)?.let { taken[id] = it }
|
||||
}
|
||||
ownWindows = shown
|
||||
if (current !== this) return
|
||||
if (taken != holders) Log.i(TAG, "bottom screen holders: $taken")
|
||||
holders = taken
|
||||
|
||||
@@ -73,7 +73,7 @@ object ControllerPrefs {
|
||||
keyboardButton = p.getBoolean("keyboardButton", true),
|
||||
selectSteam = p.getBoolean("selectSteam", false),
|
||||
joyConVr = joyConVr(context),
|
||||
snap = p.getBoolean("snap", true),
|
||||
snap = snap(context),
|
||||
customLayout = p.all.keys.any { it.startsWith("layout.") },
|
||||
mapping = mappable.associate { (id, _) -> id to target(context, id) },
|
||||
deadZone = deadZone(context),
|
||||
@@ -87,11 +87,6 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putString("map.$id", target).apply()
|
||||
}
|
||||
|
||||
fun resetMapping(context: Context) {
|
||||
val p = prefs(context)
|
||||
p.edit().apply { p.all.keys.filter { it.startsWith("map.") }.forEach { remove(it) } }.apply()
|
||||
}
|
||||
|
||||
fun setTint(context: Context, tint: Int) {
|
||||
prefs(context).edit().putInt("tint", tint).apply()
|
||||
}
|
||||
@@ -129,10 +124,6 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putBoolean("steamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setQamButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("qamButton", on).apply()
|
||||
}
|
||||
|
||||
fun setKeyboardButton(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("keyboardButton", on).apply()
|
||||
}
|
||||
@@ -147,6 +138,8 @@ object ControllerPrefs {
|
||||
prefs(context).edit().putBoolean("joyConVr", on).apply()
|
||||
}
|
||||
|
||||
fun snap(context: Context): Boolean = prefs(context).getBoolean("snap", true)
|
||||
|
||||
fun setSnap(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("snap", on).apply()
|
||||
}
|
||||
@@ -172,9 +165,4 @@ object ControllerPrefs {
|
||||
fun resetAll(context: Context) {
|
||||
prefs(context).edit().clear().putBoolean("rumble", true).apply()
|
||||
}
|
||||
|
||||
fun resetAllLayouts(context: Context) {
|
||||
val p = prefs(context)
|
||||
p.edit().apply { p.all.keys.filter { it.startsWith("layout.") }.forEach { remove(it) } }.apply()
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,14 @@ class DirectTouchHold(
|
||||
handler.removeCallbacks(hold)
|
||||
}
|
||||
|
||||
fun cancelTouch() {
|
||||
handler.removeCallbacks(hold)
|
||||
handler.removeCallbacks(lift)
|
||||
liftPending = false
|
||||
if (forwarded) touch.cancel()
|
||||
forwarded = false
|
||||
}
|
||||
|
||||
fun onTouch(event: MotionEvent): Boolean {
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
if (liftPending) {
|
||||
|
||||
@@ -4,12 +4,13 @@ import android.content.Context
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
|
||||
object InputLibraries {
|
||||
private const val VERSION = 12
|
||||
private const val VERSION = 14
|
||||
private const val KEY = "library"
|
||||
private const val RECOVERY = "library-recovery."
|
||||
const val OVERLAY = "builtin.overlay"
|
||||
@@ -30,6 +31,8 @@ object InputLibraries {
|
||||
if (version < 11) library = neutralStockLayouts(library)
|
||||
if (version < 10) library = restyleGamepads(context, library)
|
||||
if (version < 12) library = barStockLayouts(library)
|
||||
if (version < 13) library = LegacyScreenKeyMigration.pin(library)
|
||||
if (version < 14) library = deckGamepadSurfaces(context, library)
|
||||
if (version < 6 || !p.contains(LegacyScreenKeyMigration.BACKUP)) {
|
||||
val legacy = LegacyScreenKeyMigration.snapshot(context)
|
||||
if (legacy != null) {
|
||||
@@ -263,6 +266,15 @@ object InputLibraries {
|
||||
return addStockLayouts(result)
|
||||
}
|
||||
|
||||
private fun deckGamepadSurfaces(context: Context, library: InputLibrary): InputLibrary {
|
||||
val leftPad = SessionPrefs.gamepadSurface(context, right = false) == SessionPrefs.SURFACE_TRACKPAD
|
||||
val rightStick = SessionPrefs.gamepadSurface(context, right = true) == SessionPrefs.SURFACE_STICK
|
||||
if (!leftPad && !rightStick) return library
|
||||
val saved = library.layouts.firstOrNull { it.id == DeckPanel.GAMEPAD.layout } ?: return library
|
||||
if (saved.elements != InputStockLayouts.gamepad(leftPad = false, rightStick = false)) return library
|
||||
return library.withLayout(saved.copy(elements = InputStockLayouts.gamepad(leftPad, rightStick))).validate()
|
||||
}
|
||||
|
||||
private fun migrationBindingId(library: InputLibrary, binding: String, layout: String): String {
|
||||
val seed = JSONArray(listOf(binding, layout)).toString().toByteArray(Charsets.UTF_8)
|
||||
val base = "migrated.bindings." + UUID.nameUUIDFromBytes(seed)
|
||||
@@ -292,7 +304,7 @@ object InputLibraries {
|
||||
for (e in layout.elements) elements.put(JSONObject().put("id", e.id).put("kind", e.kind.name)
|
||||
.put("control", e.control.id).put("name", e.name).put("x", e.x.toDouble()).put("y", e.y.toDouble())
|
||||
.put("width", e.width.toDouble()).put("height", e.height.toDouble()).put("floating", e.floating).put("hideWhenIdle", e.hideWhenIdle).put("mouseGestures", e.mouseGestures).put("toggle", e.toggle).put("icon", e.icon).put("image", e.image)
|
||||
.put("elementOpacity", e.opacity.toDouble()).put("buttonShape", e.buttonShape.name).put("showLabel", e.showLabel))
|
||||
.put("elementOpacity", e.opacity.toDouble()).put("buttonShape", e.buttonShape.name).put("showLabel", e.showLabel).put("alwaysShown", e.alwaysShown))
|
||||
layouts.put(JSONObject().put("id", layout.id).put("name", layout.name).put("template", layout.template.name).put("elements", elements)
|
||||
.put("tint", layout.style.tint ?: JSONObject.NULL).put("opacity", layout.style.opacity).put("screen", layout.screen.name).put("aspect", layout.aspect.toDouble()).put("scale", layout.scale.toDouble())
|
||||
.put("hidden", JSONArray(layout.style.hidden.map { it.name }.sorted())).put("origin", layout.origin ?: JSONObject.NULL))
|
||||
@@ -333,7 +345,7 @@ object InputLibraries {
|
||||
e.getDouble("y").toFloat(), e.getDouble("width").toFloat(), e.getDouble("height").toFloat(),
|
||||
e.getString("id"), InputElementKind.valueOf(e.getString("kind")), e.getString("name"),
|
||||
e.optBoolean("floating", true), e.optBoolean("hideWhenIdle", false), e.optBoolean("mouseGestures", false), e.optBoolean("toggle", false), e.optInt("icon", 0), e.optString("image", ""),
|
||||
e.optDouble("elementOpacity", 1.0).toFloat(), InputButtonShape.valueOf(e.optString("buttonShape", InputButtonShape.ROUNDED.name)), e.optBoolean("showLabel", true))
|
||||
e.optDouble("elementOpacity", 1.0).toFloat(), InputButtonShape.valueOf(e.optString("buttonShape", InputButtonShape.ROUNDED.name)), e.optBoolean("showLabel", true), e.optBoolean("alwaysShown", false))
|
||||
}
|
||||
SavedInputLayout(layout.getString("id"), layout.getString("name"), elements, InputLayoutTemplate.valueOf(layout.getString("template")),
|
||||
InputSurfaceStyle(if (!layout.has("tint")) ControllerPrefs.STEAM_BLUE else if (layout.isNull("tint")) null else layout.getInt("tint"), layout.optInt("opacity", 100),
|
||||
|
||||
@@ -45,6 +45,14 @@ internal object InputStockLayouts {
|
||||
InputSurfaceElement(InputControl.SELECT, .42f, .3f, .07f, .1f, buttonShape = InputButtonShape.PILL),
|
||||
InputSurfaceElement(InputControl.START, .51f, .3f, .07f, .1f, buttonShape = InputButtonShape.PILL))
|
||||
}
|
||||
fun gamepad(leftPad: Boolean, rightStick: Boolean) = gamepad().map {
|
||||
when {
|
||||
leftPad && it.control == InputControl.LS -> InputSurfaceElement(InputControl.LEFT_PAD, it.x, it.y, it.width, it.height)
|
||||
rightStick && it.control == InputControl.RIGHT_PAD -> InputSurfaceElement(InputControl.RS, it.x, it.y, it.width, it.height)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
|
||||
fun layouts() = listOf(
|
||||
SavedInputLayout(InputLibraries.DECK, "Deck grips + trackpads", pads() + grips(), style = NEUTRAL, screen = InputLayoutScreen.SECONDARY),
|
||||
SavedInputLayout(InputLibraries.TRACKPAD, "Trackpad + keyboard", listOf(
|
||||
|
||||
@@ -34,6 +34,7 @@ data class InputSurfaceElement(
|
||||
val opacity: Float = 1f,
|
||||
val buttonShape: InputButtonShape = InputButtonShape.ROUNDED,
|
||||
val showLabel: Boolean = true,
|
||||
val alwaysShown: Boolean = false,
|
||||
) {
|
||||
val hasClick get() = kind == InputElementKind.STICK || kind == InputElementKind.TRACKPAD
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class InputSurfaceSource(private val pad: PadBridge, private val role: InputRole
|
||||
private val gestureRight = if (element.mouseGestures) router?.channel(role, InputControl.A, fixedAction = InputAction.MouseButton(273)) else null
|
||||
private val gestureMiddle = if (element.mouseGestures) router?.channel(role, InputControl.A, fixedAction = InputAction.MouseButton(274)) else null
|
||||
val mouseGestures get() = element.mouseGestures && route?.action is InputAction.Pointer
|
||||
val directions get() = route?.action as? InputAction.Directions
|
||||
|
||||
fun gestureContact(touching: Boolean) { if (!closed) route?.update(0f, 0f, touching) }
|
||||
fun pointerDelta(dx: Float, dy: Float) {
|
||||
|
||||
@@ -225,7 +225,8 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
)
|
||||
}
|
||||
|
||||
fun dock(stored: List<InputSurfaceElement>, portrait: Boolean, grips: Boolean): List<InputSurfaceElement> {
|
||||
fun dock(stored: List<InputSurfaceElement>, portrait: Boolean, grips: Boolean, left: InputControl = InputControl.LS,
|
||||
right: InputControl = InputControl.RS): List<InputSurfaceElement> {
|
||||
val boxes = GamepadGeometry.layout(width.toFloat(), height.toFloat(), resources.displayMetrics.density, portrait)
|
||||
val placed = mutableListOf<InputSurfaceElement>()
|
||||
fun box(control: InputControl, slot: GamepadGeometry.Slot, shape: InputButtonShape) {
|
||||
@@ -243,9 +244,9 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
b.cx + direction.first * spread, b.cy + direction.second * spread, radius)
|
||||
}
|
||||
}
|
||||
box(InputControl.LS, GamepadGeometry.Slot.LEFT_MAIN, InputButtonShape.CIRCLE)
|
||||
box(left, GamepadGeometry.Slot.LEFT_MAIN, InputButtonShape.CIRCLE)
|
||||
cross(DPAD_ORDER, GamepadGeometry.Slot.LEFT_SECOND)
|
||||
box(InputControl.RS, GamepadGeometry.Slot.RIGHT_MAIN, InputButtonShape.CIRCLE)
|
||||
box(right, GamepadGeometry.Slot.RIGHT_MAIN, InputButtonShape.CIRCLE)
|
||||
cross(FACE_ORDER, GamepadGeometry.Slot.RIGHT_SECOND)
|
||||
box(InputControl.LT, GamepadGeometry.Slot.LEFT_OUTER_SHOULDER, InputButtonShape.PILL)
|
||||
box(InputControl.LB, GamepadGeometry.Slot.LEFT_INNER_SHOULDER, InputButtonShape.PILL)
|
||||
@@ -254,7 +255,8 @@ internal class LegacyGamepadLayout(private val context: Context, private val wid
|
||||
box(InputControl.SELECT, GamepadGeometry.Slot.VIEW, InputButtonShape.PILL)
|
||||
box(InputControl.START, GamepadGeometry.Slot.MENU, InputButtonShape.PILL)
|
||||
if (grips) GRIPS.forEach { (control, slot) -> box(control, slot, InputButtonShape.ROUNDED) }
|
||||
return withGeometry(stored, placed) + placed.filter { p -> GRIPS.any { it.first == p.control } && stored.none { it.id == p.id } }
|
||||
val surfaces = listOf(left, right).filter { it.isTrackpad }
|
||||
return withGeometry(stored, placed) + placed.filter { p -> (GRIPS.any { it.first == p.control } || p.control in surfaces) && stored.none { it.id == p.id } }
|
||||
}
|
||||
|
||||
private fun circle(e: InputSurfaceElement, cx: Float, cy: Float, radius: Float) =
|
||||
|
||||
@@ -40,7 +40,7 @@ internal object LegacyScreenKeyMigration {
|
||||
if (result.profiles.any { it.id == id }) continue
|
||||
val converted = convert(context, set.second, metrics.widthPixels, metrics.heightPixels, metrics.density)
|
||||
if (converted.isEmpty()) continue
|
||||
val layout = SavedInputLayout("$id.layout", set.first, converted.map { it.first }, style = style, screen = InputLayoutScreen.MAIN,
|
||||
val layout = SavedInputLayout("$id.layout", set.first, converted.map { it.first.copy(alwaysShown = true) }, style = style, screen = InputLayoutScreen.MAIN,
|
||||
aspect = (metrics.widthPixels.coerceAtLeast(1).toFloat() / metrics.heightPixels.coerceAtLeast(1)).coerceIn(.05f, 20f))
|
||||
val bindings = SavedInputBindings("$id.bindings", set.first, false, layout.id, elements = converted.associate { it.first.id to it.second })
|
||||
val base = library.profile(library.steam)
|
||||
@@ -68,6 +68,17 @@ internal object LegacyScreenKeyMigration {
|
||||
return result.validate()
|
||||
}
|
||||
|
||||
fun pin(library: InputLibrary): InputLibrary {
|
||||
val keys = library.layouts.firstOrNull { it.id == "$SHOWN.layout" }?.elements.orEmpty()
|
||||
return library.copy(layouts = library.layouts.map { layout ->
|
||||
val imported = layout.id.startsWith("migrated.screenkeys.")
|
||||
layout.copy(elements = layout.elements.map { e ->
|
||||
val key = e.id.startsWith("imported:") && (imported || keys.any { it.id == e.id && it.kind == e.kind && it.name == e.name })
|
||||
if (key) e.copy(alwaysShown = true) else e
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
internal fun convert(context: Context, raw: String, width: Int, height: Int, density: Float): List<Pair<InputSurfaceElement, InputAction>> =
|
||||
convert(context, parse(raw), width, height, density)
|
||||
|
||||
|
||||
@@ -29,6 +29,9 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
private set
|
||||
var isQuickHidden = false
|
||||
private set
|
||||
var isPinnedOnly = false
|
||||
private set
|
||||
val hasPinned get() = layout?.elements?.any { it.alwaysShown } == true
|
||||
var onTouchMiss: ((MotionEvent) -> Unit)?
|
||||
get() = surface.onTouchMiss
|
||||
set(value) { surface.onTouchMiss = value }
|
||||
@@ -61,6 +64,13 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
render()
|
||||
}
|
||||
|
||||
fun setPinnedOnly(enabled: Boolean) {
|
||||
if (isPinnedOnly == enabled) return
|
||||
releaseAll()
|
||||
isPinnedOnly = enabled
|
||||
render()
|
||||
}
|
||||
|
||||
private data class Placement(val layout: SavedInputLayout?, val width: Int, val height: Int, val safe: Rect, val buttonsOnly: Boolean)
|
||||
|
||||
private fun placed(): List<InputSurfaceElement> {
|
||||
@@ -72,12 +82,16 @@ class OnScreenControls(context: Context, pad: PadBridge?, val dock: OnScreenLayo
|
||||
|
||||
private fun render() {
|
||||
val elements = placed()
|
||||
surface.updateCustomLayout(if (isQuickHidden) elements.filter { it.id == "builtin:keyboard" } else elements)
|
||||
surface.updateCustomLayout(when {
|
||||
isPinnedOnly -> elements.filter { it.alwaysShown }
|
||||
isQuickHidden -> elements.filter { it.id == "builtin:keyboard" || it.alwaysShown }
|
||||
else -> elements
|
||||
})
|
||||
val style = layout?.style ?: InputSurfaceStyle()
|
||||
surface.updateStyle(style)
|
||||
quickToggle.tint = style.tint
|
||||
quickToggle.alpha = style.opacity / 100f
|
||||
quickToggle.visibility = if (isButtonsOnly || layout == null || dock != null) GONE else VISIBLE
|
||||
quickToggle.visibility = if (isButtonsOnly || isPinnedOnly || layout == null || dock != null) GONE else VISIBLE
|
||||
quickToggle.invalidate()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,18 @@ import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
object OnScreenLayout {
|
||||
data class Dock(val portrait: Boolean, val grips: Boolean)
|
||||
data class Dock(val portrait: Boolean, val grips: Boolean, val left: InputControl = InputControl.LS, val right: InputControl = InputControl.RS) {
|
||||
companion object {
|
||||
fun deck(portrait: Boolean, gamepad: SavedInputLayout?): Dock {
|
||||
fun side(right: Boolean) = gamepad?.elements?.firstOrNull {
|
||||
(it.kind == InputElementKind.STICK || it.kind == InputElementKind.TRACKPAD) && (it.x + it.width / 2f >= .5f) == right
|
||||
}?.kind
|
||||
val left = if (side(false) == InputElementKind.TRACKPAD) InputControl.LEFT_PAD else InputControl.LS
|
||||
val right = if (side(true) == InputElementKind.STICK) InputControl.RS else InputControl.RIGHT_PAD
|
||||
return Dock(portrait, true, left, right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val TOLERANCE = .01f
|
||||
private const val KEYBOARD_ID = "builtin:keyboard"
|
||||
@@ -17,13 +28,13 @@ object OnScreenLayout {
|
||||
val stored = layout?.elements ?: return emptyList()
|
||||
if (width <= 0 || height <= 0) return stored
|
||||
val generator = LegacyGamepadLayout(context, width, height, safe, layout.scale)
|
||||
if (buttonsOnly && dock == null) return generator.buttonsOnly(stored)
|
||||
val (reference, generated) = reference(context, layout, width, height)
|
||||
if (buttonsOnly && dock == null) return generator.buttonsOnly(stored) + fit(stored.filter { it.alwaysShown }, reference.first, reference.second, width, height, safe)
|
||||
val gamepad = if (generated) stored.filter(::isGamepad) else emptyList()
|
||||
val others = fit(stored.filterNot { generated && isGamepad(it) }, reference.first, reference.second, width, height, safe)
|
||||
val others = fit(stored.filterNot { generated && isGamepad(it) || dock != null && it.alwaysShown }, reference.first, reference.second, width, height, safe)
|
||||
val placed = when {
|
||||
gamepad.isEmpty() -> emptyList()
|
||||
dock != null -> generator.dock(gamepad, dock.portrait, dock.grips)
|
||||
dock != null -> generator.dock(gamepad, dock.portrait, dock.grips, dock.left, dock.right)
|
||||
else -> generator.regenerate(gamepad)
|
||||
}
|
||||
val byId = (placed + others).associateBy { it.id }
|
||||
|
||||
@@ -52,4 +52,7 @@ object SecondScreenDisplays {
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
fun editorCandidates(selected: Int, available: List<Int>, taken: (Int) -> Boolean): List<Int> =
|
||||
(listOf(selected) + available).distinct().filterNot(taken)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ object DesktopCatalog {
|
||||
const val CATALOG_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/desktop.json"
|
||||
const val STEAM_SEED_URL = "https://raw.githubusercontent.com/Droid-Deck/DroidDeck-Components/main/steam-seed.json"
|
||||
const val PROTON_SEED_ID = "proton-arm64"
|
||||
const val APPIMAGE_EXEC = "/usr/local/bin/droiddeck-appimage-exec"
|
||||
|
||||
class Entry(
|
||||
val id: String, val name: String, val tier: Int, val version: String, val kind: String,
|
||||
@@ -216,7 +217,7 @@ object DesktopCatalog {
|
||||
target.setExecutable(true, false)
|
||||
FileUtils.writeString(File(root, "usr/share/applications/droiddeck-${entry.id}.desktop"),
|
||||
"[Desktop Entry]\nType=Application\nName=${entry.name}\n" +
|
||||
"Exec=env APPIMAGE_EXTRACT_AND_RUN=1 /opt/appimages/${entry.id}.AppImage\n" +
|
||||
"Exec=$APPIMAGE_EXEC /opt/appimages/${entry.id}.AppImage\n" +
|
||||
"Icon=${entry.icon}\nTerminal=false\nCategories=${entry.category};\n")
|
||||
}
|
||||
else -> {
|
||||
|
||||
@@ -21,9 +21,11 @@ object LegacyNames {
|
||||
"etc/bannerlator-network.json" to "etc/droiddeck-network.json",
|
||||
"etc/bannerlator-wifi-scan-request" to "etc/droiddeck-wifi-scan-request",
|
||||
)
|
||||
private const val MENU_DIR = "usr/local/share/applications"
|
||||
private const val OLD_BIN = "/usr/local/bin/bannerlator-"
|
||||
private const val NEW_BIN = "/usr/local/bin/droiddeck-"
|
||||
private val MENU_REWRITES = listOf(
|
||||
Triple("usr/local/share/applications", "/usr/local/bin/bannerlator-", "/usr/local/bin/droiddeck-"),
|
||||
Triple("usr/share/applications", "Exec=env APPIMAGE_EXTRACT_AND_RUN=1 /opt/appimages/",
|
||||
"Exec=${DesktopCatalog.APPIMAGE_EXEC} /opt/appimages/"),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Synchronized
|
||||
@@ -95,14 +97,16 @@ object LegacyNames {
|
||||
}
|
||||
|
||||
private fun rewriteMenuEntries(root: File) {
|
||||
val entries = File(root, MENU_DIR).listFiles { f -> f.isFile && f.name.startsWith("droiddeck-") && f.name.endsWith(".desktop") } ?: return
|
||||
for (entry in entries) {
|
||||
val text = FileUtils.readString(entry) ?: continue
|
||||
if (!text.contains(OLD_BIN)) continue
|
||||
val staged = File(entry.parentFile, entry.name + ".staged")
|
||||
if (!FileUtils.writeString(staged, text.replace(OLD_BIN, NEW_BIN)) || !staged.renameTo(entry)) {
|
||||
staged.delete()
|
||||
Log.w(TAG, "could not rewrite $entry")
|
||||
for ((dir, old, new) in MENU_REWRITES) {
|
||||
val entries = File(root, dir).listFiles { f -> f.isFile && f.name.startsWith("droiddeck-") && f.name.endsWith(".desktop") } ?: continue
|
||||
for (entry in entries) {
|
||||
val text = FileUtils.readString(entry) ?: continue
|
||||
if (!text.contains(old)) continue
|
||||
val staged = File(entry.parentFile, entry.name + ".staged")
|
||||
if (!FileUtils.writeString(staged, text.replace(old, new)) || !staged.renameTo(entry)) {
|
||||
staged.delete()
|
||||
Log.w(TAG, "could not rewrite $entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,12 @@ public final class LinuxRuntimeInstaller {
|
||||
private LinuxRuntimeInstaller() {}
|
||||
|
||||
public static String installedVersion(Context context) {
|
||||
return LinuxRuntime.isInstalled(context) ? rootfsVersion(context) : null;
|
||||
}
|
||||
|
||||
private static String rootfsVersion(Context context) {
|
||||
File marker = new File(LinuxRuntime.rootDir(context), VERSION_FILE);
|
||||
if (!marker.isFile() || !LinuxRuntime.isInstalled(context)) return null;
|
||||
if (!marker.isFile()) return null;
|
||||
String v = FileUtils.readString(marker);
|
||||
return v == null ? null : v.trim();
|
||||
}
|
||||
@@ -144,6 +148,10 @@ public final class LinuxRuntimeInstaller {
|
||||
}
|
||||
};
|
||||
boolean desktop = DesktopCatalog.INSTANCE.desktopInstalled(app);
|
||||
if (packsOnly(app, release, progress)) {
|
||||
job.ok = true;
|
||||
return true;
|
||||
}
|
||||
job.ok = installOnce(app, release, progress);
|
||||
if (job.ok && desktop) keepDesktop(app, progress);
|
||||
return job.ok;
|
||||
@@ -283,6 +291,31 @@ public final class LinuxRuntimeInstaller {
|
||||
for (File file : files) file.delete();
|
||||
}
|
||||
|
||||
private static kotlin.jvm.functions.Function1<AddonPack.State, kotlin.Unit> packProgress(ProgressListener listener, String stage) {
|
||||
return state -> {
|
||||
if (listener != null && state instanceof AddonPack.State.Downloading) {
|
||||
listener.onProgress(Step.DOWNLOADING, stage, Math.round(((AddonPack.State.Downloading) state).getProgress() * 100f));
|
||||
}
|
||||
return kotlin.Unit.INSTANCE;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean packsOnly(Context context, Release release, ProgressListener listener) {
|
||||
if (!LinuxRuntime.hasRootfs(context) || LinuxRuntime.isInstalled(context) || !release.version.equals(rootfsVersion(context))) return false;
|
||||
File root = LinuxRuntime.rootDir(context);
|
||||
try {
|
||||
recoverInterruptedSwap(root, new File(root.getParentFile(), LinuxRuntime.DIR + ".new"), new File(root.getParentFile(), LinuxRuntime.DIR + ".old"));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "packs only: " + e);
|
||||
return false;
|
||||
}
|
||||
String downloading = context.getString(R.string.rtinst_downloading);
|
||||
if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0);
|
||||
return AddonPack.PROOT.await(context, () -> true, packProgress(listener, downloading))
|
||||
&& AddonPack.DIRECTAUDIO.await(context, () -> true, packProgress(listener, downloading))
|
||||
&& LinuxRuntime.isInstalled(context);
|
||||
}
|
||||
|
||||
private static boolean installOnce(Context context, Release release, ProgressListener listener) {
|
||||
File archive = new File(context.getCacheDir(), archiveName(release));
|
||||
boolean keepPartial = false;
|
||||
@@ -294,10 +327,10 @@ public final class LinuxRuntimeInstaller {
|
||||
emptyTrash(LinuxRuntime.rootDir(context).getParentFile());
|
||||
String downloading = context.getString(R.string.rtinst_downloading);
|
||||
if (listener != null) listener.onProgress(Step.DOWNLOADING, downloading, 0);
|
||||
if (!LinuxRuntime.patchedProot(context) && !AddonPack.PROOT.await(context)
|
||||
if (!LinuxRuntime.patchedProot(context) && !AddonPack.PROOT.await(context, () -> true, packProgress(listener, downloading))
|
||||
&& !SessionPrefs.allowStockProot(context))
|
||||
throw new IOException("Cannot download proot");
|
||||
if (!AddonPack.DIRECTAUDIO.await(context)) throw new IOException("Cannot download DirectAudio");
|
||||
if (!AddonPack.DIRECTAUDIO.await(context, () -> true, packProgress(listener, downloading))) throw new IOException("Cannot download DirectAudio");
|
||||
boolean ok = Downloader.downloadFile(release.url, archive, true, release.sha256, (fraction) -> {
|
||||
if (listener != null) {
|
||||
listener.onProgress(Step.DOWNLOADING, downloading,
|
||||
|
||||
@@ -35,6 +35,7 @@ object UserApps {
|
||||
private const val GITHUB_REPO = "github-repo"
|
||||
private const val GITHUB_TAG = "github-tag"
|
||||
private const val GITHUB_PUBLISHED = "github-published"
|
||||
private const val GITHUB_ASSET = "github-asset"
|
||||
|
||||
enum class Kind { SCRIPT, APPIMAGE, FLATPAK }
|
||||
|
||||
@@ -56,7 +57,7 @@ object UserApps {
|
||||
sealed class Source {
|
||||
class Script(val file: File) : Source()
|
||||
class AppImage(val file: File) : Source()
|
||||
class GitHub(val repo: String) : Source()
|
||||
class GitHub(val repo: String, val asset: String? = null) : Source()
|
||||
class Flatpak(val id: String) : Source()
|
||||
class FlatpakBundle(val file: File) : Source()
|
||||
}
|
||||
@@ -113,7 +114,7 @@ object UserApps {
|
||||
when (val s = request.source) {
|
||||
is Source.Script -> addScript(context, s.file, name, icon, request.meta, onProgress)
|
||||
is Source.AppImage -> AppImageManager.import(context, s.file, name, icon, request.meta) { onProgress(it, -1) }
|
||||
is Source.GitHub -> addFromGitHub(context, s.repo, name, icon, onProgress)
|
||||
is Source.GitHub -> addFromGitHub(context, s.repo, s.asset, name, icon, onProgress)
|
||||
is Source.Flatpak -> addFlatpak(context, s.id, name, icon, onProgress)
|
||||
is Source.FlatpakBundle -> addFlatpakBundle(context, s.file, name, icon, onProgress)
|
||||
}
|
||||
@@ -305,14 +306,26 @@ object UserApps {
|
||||
return if (repo.isEmpty() || repo == "." || repo == "..") null else "${m.groupValues[1]}/$repo"
|
||||
}
|
||||
|
||||
internal fun pickAsset(names: List<String>): String? {
|
||||
fun githubAsset(input: String): String? =
|
||||
Regex("github\\.com/[\\w.-]+/[\\w.-]+/releases/(?:download/[^/]+|latest/download)/([^/?#]+\\.appimage)(?:[?#].*)?$", RegexOption.IGNORE_CASE)
|
||||
.find(input.trim())?.groupValues?.get(1)?.let { runCatching { java.net.URLDecoder.decode(it, "UTF-8") }.getOrNull() }
|
||||
|
||||
private fun assetVariant(name: String): Set<String> =
|
||||
name.lowercase().replace(Regex("x86[_-]64|amd64"), "x64").replace(Regex("x86[_-]32|i[36]86"), "x32")
|
||||
.split(Regex("[^a-z0-9]+")).filter { it.isNotEmpty() && !Regex("[0-9a-f]{7,}|v?[0-9]+").matches(it) }.toSet()
|
||||
|
||||
internal fun pickAsset(names: List<String>, wanted: String? = null): String? {
|
||||
val images = names.filter { it.endsWith(".appimage", ignoreCase = true) }
|
||||
if (wanted != null) {
|
||||
images.firstOrNull { it == wanted }?.let { return it }
|
||||
images.firstOrNull { assetVariant(it) == assetVariant(wanted) }?.let { return it }
|
||||
}
|
||||
return images.firstOrNull { Regex("aarch64|arm64", RegexOption.IGNORE_CASE).containsMatchIn(it) }
|
||||
?: images.firstOrNull { !Regex("x86_64|x86-64|amd64|x64|i[36]86|armhf|armv7", RegexOption.IGNORE_CASE).containsMatchIn(it) }
|
||||
?: images.firstOrNull { Regex("x86_64|x86-64|amd64|x64", RegexOption.IGNORE_CASE).containsMatchIn(it) }
|
||||
}
|
||||
|
||||
private fun latestRelease(context: Context, repo: String): Pair<Release?, String?> {
|
||||
private fun latestRelease(context: Context, repo: String, wanted: String? = null): Pair<Release?, String?> {
|
||||
val body = Downloader.downloadString("https://api.github.com/repos/$repo/releases?per_page=15")
|
||||
val releases = body?.let { runCatching { JSONArray(it) }.getOrNull() }
|
||||
?: return null to context.getString(R.string.user_apps_github_unreachable, repo)
|
||||
@@ -321,7 +334,7 @@ object UserApps {
|
||||
if (r.optBoolean("draft")) return@mapNotNull null
|
||||
val assets = r.optJSONArray("assets") ?: return@mapNotNull null
|
||||
val byName = (0 until assets.length()).associate { j -> assets.getJSONObject(j).let { it.getString("name") to it } }
|
||||
pickAsset(byName.keys.toList())?.let { asset ->
|
||||
pickAsset(byName.keys.toList(), wanted)?.let { asset ->
|
||||
r.optBoolean("prerelease") to byName.getValue(asset).let { a ->
|
||||
Release(r.optString("tag_name", asset), r.optString("published_at"), asset, a.getString("browser_download_url"),
|
||||
Hashes.githubSha256(a.optString("digest")))
|
||||
@@ -344,11 +357,11 @@ object UserApps {
|
||||
}
|
||||
|
||||
private fun githubMeta(repo: String, release: Release) =
|
||||
mapOf(GITHUB_REPO to repo, GITHUB_TAG to release.tag, GITHUB_PUBLISHED to release.published)
|
||||
mapOf(GITHUB_REPO to repo, GITHUB_TAG to release.tag, GITHUB_PUBLISHED to release.published, GITHUB_ASSET to release.asset)
|
||||
|
||||
private fun addFromGitHub(context: Context, repo: String, name: String?, icon: File?, onProgress: (String, Int) -> Unit): String? {
|
||||
private fun addFromGitHub(context: Context, repo: String, asset: String?, name: String?, icon: File?, onProgress: (String, Int) -> Unit): String? {
|
||||
onProgress(context.getString(R.string.user_apps_github_checking, repo), -1)
|
||||
val (release, problem) = latestRelease(context, repo)
|
||||
val (release, problem) = latestRelease(context, repo, asset)
|
||||
if (release == null) return problem
|
||||
return withDownload(context, release, onProgress) { file ->
|
||||
AppImageManager.import(context, file, name ?: repo.substringAfter('/'), icon, githubMeta(repo, release)) { onProgress(it, -1) }
|
||||
@@ -381,9 +394,10 @@ object UserApps {
|
||||
|
||||
fun checkUpdate(context: Context, app: App): UpdateCheck {
|
||||
val repo = app.repo ?: return UpdateCheck.Failed(context.getString(R.string.user_apps_update_failed))
|
||||
val (release, problem) = latestRelease(context, repo)
|
||||
val dir = AppImageManager.dir(context, app.key.substringAfter(':'))
|
||||
val (release, problem) = latestRelease(context, repo, FileUtils.readString(File(dir, GITHUB_ASSET))?.trim()?.takeIf { it.isNotEmpty() })
|
||||
if (release == null) return UpdateCheck.Failed(problem ?: context.getString(R.string.user_apps_update_failed))
|
||||
val published = FileUtils.readString(File(AppImageManager.dir(context, app.key.substringAfter(':')), GITHUB_PUBLISHED))?.trim().orEmpty()
|
||||
val published = FileUtils.readString(File(dir, GITHUB_PUBLISHED))?.trim().orEmpty()
|
||||
val newer = if (published.isNotEmpty() && release.published.isNotEmpty()) release.published > published else release.tag != app.version
|
||||
return if (newer) UpdateCheck.Available(release) else UpdateCheck.Current(app.version ?: release.tag)
|
||||
}
|
||||
|
||||
@@ -68,8 +68,9 @@ object GameStorage {
|
||||
externalStoragePath: Boolean = isExternalStoragePath(path),
|
||||
): Boolean = when {
|
||||
state == Environment.MEDIA_MOUNTED || state == Environment.MEDIA_MOUNTED_READ_ONLY -> true
|
||||
path.isDirectory -> true
|
||||
externalStoragePath -> false
|
||||
state == Environment.MEDIA_UNKNOWN || state == null -> path.isDirectory || path.parentFile?.isDirectory == true
|
||||
state == Environment.MEDIA_UNKNOWN || state == null -> path.parentFile?.isDirectory == true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ object SessionFiles {
|
||||
"usr/local/bin/droiddeck-flatpak-setup" to "usr/local/bin/droiddeck-flatpak-setup",
|
||||
"usr/local/bin/droiddeck-flatpak-run" to "usr/local/bin/droiddeck-flatpak-run",
|
||||
"usr/local/bin/droiddeck-appimage-run" to "usr/local/bin/droiddeck-appimage-run",
|
||||
"usr/local/bin/droiddeck-appimage-exec" to "usr/local/bin/droiddeck-appimage-exec",
|
||||
"usr/local/bin/droiddeck-script-run" to "usr/local/bin/droiddeck-script-run",
|
||||
"usr/local/bin/droiddeck-open" to "usr/local/bin/droiddeck-open",
|
||||
"usr/local/bin/droiddeck-proton-run" to "usr/local/bin/droiddeck-proton-run",
|
||||
|
||||
@@ -1555,10 +1555,6 @@ object SessionPrefs {
|
||||
prefs(context).getString(if (right) "gamepadRight" else "gamepadLeft", null)
|
||||
?: if (right) SURFACE_TRACKPAD else SURFACE_STICK
|
||||
|
||||
fun setGamepadSurface(context: Context, right: Boolean, surface: String) {
|
||||
prefs(context).edit().putString(if (right) "gamepadRight" else "gamepadLeft", surface).apply()
|
||||
}
|
||||
|
||||
fun deckPanel(context: Context): com.droiddeck.launcher.input.DeckPanel =
|
||||
com.droiddeck.launcher.input.DeckPanel.from(prefs(context).getString("deckPanel", null))
|
||||
|
||||
|
||||
@@ -65,6 +65,8 @@ private fun releaseDateValue(raw: String): Long =
|
||||
runCatching { java.time.LocalDate.parse(day).toEpochDay() }.getOrDefault(0L)
|
||||
} ?: 0L
|
||||
|
||||
internal fun entryTime(first: Long?, fallback: Long?): Long = first?.takeIf { it > 0 } ?: fallback?.takeIf { it > 0 } ?: 0L
|
||||
|
||||
fun <T> sortGames(
|
||||
items: List<T>,
|
||||
sort: String,
|
||||
|
||||
@@ -204,7 +204,8 @@ object BattleNetBackend : StoreBackend {
|
||||
fun installedGames(context: Context): List<InstalledStoreGame> = installed(context).map { game ->
|
||||
val art = BattleNet.art(context, game.title)
|
||||
val cover = art.cover?.let { Uri.fromFile(it).toString() }
|
||||
InstalledStoreGame(StoreGameSidecar(store, game.title.uid, game.name, "", cover = cover, hero = art.header?.let { Uri.fromFile(it).toString() } ?: cover), game.folder)
|
||||
InstalledStoreGame(StoreGameSidecar(store, game.title.uid, game.name, "", installedAt = game.folder.lastModified(),
|
||||
cover = cover, hero = art.header?.let { Uri.fromFile(it).toString() } ?: cover), game.folder)
|
||||
}
|
||||
|
||||
private fun shelves(context: Context): StoreShelves {
|
||||
|
||||
@@ -151,7 +151,7 @@ internal fun AddAppDialog(runtimeReady: Boolean, onDismiss: () -> Unit, onAdd: (
|
||||
?.let { UserApps.Request(UserApps.Source.Script(it), custom, iconPath) to (custom ?: it.nameWithoutExtension) }
|
||||
AddSource.APPIMAGE -> imagePath?.takeIf { it.endsWith(".appimage", ignoreCase = true) }?.let { File(it) }
|
||||
?.let { UserApps.Request(UserApps.Source.AppImage(it), custom, iconPath) to (custom ?: it.nameWithoutExtension) }
|
||||
AddSource.GITHUB -> repoId?.let { UserApps.Request(UserApps.Source.GitHub(it), custom, iconPath) to (custom ?: it) }
|
||||
AddSource.GITHUB -> repoId?.let { UserApps.Request(UserApps.Source.GitHub(it, UserApps.githubAsset(repo)), custom, iconPath) to (custom ?: it) }
|
||||
AddSource.FLATPAK -> bundlePath?.takeIf { it.endsWith(".flatpak", ignoreCase = true) }?.let { File(it) }
|
||||
?.let { UserApps.Request(UserApps.Source.FlatpakBundle(it), custom, iconPath) to (custom ?: it.nameWithoutExtension) }
|
||||
?: flatpakId?.let { UserApps.Request(UserApps.Source.Flatpak(it), custom, iconPath) to (custom ?: it) }
|
||||
|
||||
@@ -16,7 +16,7 @@ import com.droiddeck.launcher.input.*
|
||||
internal fun InputActionEditor(
|
||||
control: InputControl, current: InputAction, title: String, onBack: () -> Unit, onSave: (InputAction) -> Unit,
|
||||
element: InputSurfaceElement? = null, nativeTargets: Boolean = true, detail: String? = null,
|
||||
properties: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
properties: (@Composable ColumnScope.(InputAction) -> Unit)? = null,
|
||||
click: Boolean = false,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -46,7 +46,7 @@ internal fun InputActionEditor(
|
||||
SettingsPage(host, title, onBack, eyebrow = stringResource(R.string.input_properties), lede = detail,
|
||||
action = { PrimaryButton(stringResource(R.string.gesture_save_changes), enabled = valid(draft),
|
||||
modifier = Modifier.testTag("input-action-save")) { onSave(draft) } }) {
|
||||
properties?.invoke(this)
|
||||
properties?.invoke(this, draft)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
SheetChips(listOf(stringResource(R.string.input_route_controller), stringResource(R.string.input_route_local), stringResource(R.string.input_disabled)), route) {
|
||||
when (it) {
|
||||
|
||||
@@ -58,12 +58,17 @@ internal fun InputDisplayContent(host: ComponentActivity, display: Display, onDi
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
(host as? com.droiddeck.launcher.SessionActivity)?.inputWindowShown(display.displayId)
|
||||
showPresentation()
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP && !host.screenIsOff()) presentation.hide()
|
||||
if (event == Lifecycle.Event.ON_START) showPresentation()
|
||||
}
|
||||
host.lifecycle.addObserver(observer)
|
||||
onDispose { host.lifecycle.removeObserver(observer); presentation.dismiss() }
|
||||
onDispose {
|
||||
host.lifecycle.removeObserver(observer)
|
||||
presentation.dismiss()
|
||||
(host as? com.droiddeck.launcher.SessionActivity)?.inputWindowClosed(display.displayId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Composable
|
||||
internal fun InputElementProperties(element: InputSurfaceElement, onChange: (InputSurfaceElement) -> Unit) {
|
||||
internal fun InputElementProperties(element: InputSurfaceElement, action: InputAction? = null, onChange: (InputSurfaceElement) -> Unit) {
|
||||
val current by rememberUpdatedState(element)
|
||||
val change by rememberUpdatedState(onChange)
|
||||
OutlinedTextField(element.name, { onChange(element.copy(name = it.take(80))) }, label = { Text(stringResource(R.string.input_name)) },
|
||||
@@ -72,7 +72,9 @@ internal fun InputElementProperties(element: InputSurfaceElement, onChange: (Inp
|
||||
}
|
||||
val host = rememberMenuHost()
|
||||
if ((element.kind == InputElementKind.BUTTON || element.kind == InputElementKind.TRIGGER) && element.icon == 0 && element.image.isEmpty() ||
|
||||
element.kind == InputElementKind.STICK && element.id.startsWith("legacy:"))
|
||||
element.kind == InputElementKind.STICK && element.id.startsWith("legacy:") ||
|
||||
(element.kind == InputElementKind.STICK || element.kind == InputElementKind.DPAD) && (action as? InputAction.Directions)
|
||||
?.let { listOf(it.up, it.right, it.down, it.left).plus(if (element.kind == InputElementKind.STICK) it.diagonals.orEmpty() else emptyList()).any { code -> code != 0 } } == true)
|
||||
ToggleRow(host, "element-label", stringResource(R.string.input_show_label), null, element.showLabel) { onChange(element.copy(showLabel = it)) }
|
||||
SliderRow(stringResource(R.string.ctrl_opacity), null, (element.opacity * 100).roundToInt().coerceIn(20, 100), 20..100, step = 5,
|
||||
format = { "$it%" }, modifier = Modifier.testTag("setting-element-opacity")) { onChange(element.copy(opacity = it / 100f)) }
|
||||
|
||||
@@ -169,7 +169,7 @@ private fun InputSettingsContent(onBack: () -> Unit, onChanged: () -> Unit, embe
|
||||
?: b.controls[editing.control] ?: InputAction.Native()
|
||||
InputActionEditor(editing.control, current, if (editing.click) stringResource(R.string.input_click_name, editing.element!!.name)
|
||||
else editing.element?.name ?: editing.key?.label ?: editing.control.label,
|
||||
properties = { if (!editing.click) elementProperties?.let { e -> InputElementProperties(e) { elementProperties = it } } },
|
||||
properties = { action -> if (!editing.click) elementProperties?.let { e -> InputElementProperties(e, action) { elementProperties = it } } },
|
||||
onBack = { edit = null }, element = editing.element, nativeTargets = editing.key == null, click = editing.click,
|
||||
detail = if (library.usersOfBindings(b.id).size > 1) stringResource(R.string.input_shared_detail, b.name, library.usersOfBindings(b.id).size)
|
||||
else "${profile.name} · ${roleLabel(role)}", onSave = { action ->
|
||||
|
||||
@@ -69,7 +69,7 @@ private class GamepadScaler(private val context: android.content.Context, privat
|
||||
}
|
||||
|
||||
private class SurfaceDraft(val layout: SavedInputLayout, val preset: SavedInputBindings, val defaults: List<InputSurfaceElement>? = null,
|
||||
private val gamepad: GamepadScaler? = null) {
|
||||
private val gamepad: GamepadScaler? = null, snap: Boolean = true) {
|
||||
var elements by mutableStateOf(layout.elements)
|
||||
var actions by mutableStateOf(preset.elements)
|
||||
var clicks by mutableStateOf(preset.clicks)
|
||||
@@ -77,7 +77,7 @@ private class SurfaceDraft(val layout: SavedInputLayout, val preset: SavedInputB
|
||||
var toolsExpanded by mutableStateOf(false)
|
||||
var toolsX by mutableFloatStateOf(.02f)
|
||||
var toolsY by mutableFloatStateOf(.02f)
|
||||
var snap by mutableStateOf(true)
|
||||
var snap by mutableStateOf(snap)
|
||||
var adding by mutableStateOf(false)
|
||||
var properties by mutableStateOf(false)
|
||||
var clickProperties by mutableStateOf(false)
|
||||
@@ -132,7 +132,7 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val (w, h) = OnScreenLayout.generatedSize(context, savedLayout, metrics.widthPixels, metrics.heightPixels) ?: (metrics.widthPixels to metrics.heightPixels)
|
||||
GamepadScaler(display, w, h)
|
||||
}
|
||||
SurfaceDraft(savedLayout, savedBindings, InputLibraries.defaultPositions(context, library, savedLayout), gamepad)
|
||||
SurfaceDraft(savedLayout, savedBindings, InputLibraries.defaultPositions(context, library, savedLayout), gamepad, ControllerPrefs.snap(context))
|
||||
}
|
||||
val manager = remember { context.getSystemService(DisplayManager::class.java) }
|
||||
var revision by remember { mutableIntStateOf(0) }
|
||||
@@ -162,7 +162,7 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val listener = object : DisplayManager.DisplayListener {
|
||||
override fun onDisplayAdded(id: Int) { revision++ }
|
||||
override fun onDisplayRemoved(id: Int) { revision++ }
|
||||
override fun onDisplayChanged(id: Int) { revision++ }
|
||||
override fun onDisplayChanged(id: Int) = Unit
|
||||
}
|
||||
manager.registerDisplayListener(listener, Handler(Looper.getMainLooper()))
|
||||
onDispose { manager.unregisterDisplayListener(listener) }
|
||||
@@ -171,13 +171,18 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
val presentation = SurfaceEditorPresentation(host, target, transparent = host is SessionActivity && role == InputRole.PRIMARY) {
|
||||
SurfaceCanvas(draft, transparent = host is SessionActivity && role == InputRole.PRIMARY)
|
||||
}
|
||||
(host as? SessionActivity)?.inputWindowShown(target.displayId)
|
||||
runCatching { presentation.show() }.onFailure { failure = it.message }
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_STOP && !host.screenIsOff()) presentation.hide()
|
||||
if (event == Lifecycle.Event.ON_START) runCatching { presentation.show() }
|
||||
}
|
||||
host.lifecycle.addObserver(observer)
|
||||
onDispose { host.lifecycle.removeObserver(observer); presentation.dismiss() }
|
||||
onDispose {
|
||||
host.lifecycle.removeObserver(observer)
|
||||
presentation.dismiss()
|
||||
(host as? SessionActivity)?.inputWindowClosed(target.displayId)
|
||||
}
|
||||
}
|
||||
// Compose 1.6 custom dialog sizing subtracts system bars even when they are hidden.
|
||||
Dialog(onDismissRequest = ::leave, properties = DialogProperties(usePlatformDefaultWidth = true, decorFitsSystemWindows = false)) {
|
||||
@@ -198,8 +203,8 @@ fun InputSurfaceDialog(profile: String, onDismiss: () -> Unit, onChanged: () ->
|
||||
if (draft.clickProperties) draft.clickAction(element) else draft.action(element),
|
||||
if (draft.clickProperties) stringResource(R.string.input_click_name, element.name) else element.name,
|
||||
onBack = { draft.properties = false }, element = element, click = draft.clickProperties,
|
||||
properties = {
|
||||
if (!draft.clickProperties) InputElementProperties(properties) { properties = it }
|
||||
properties = { action ->
|
||||
if (!draft.clickProperties) InputElementProperties(properties, action) { properties = it }
|
||||
if (layoutUsers > 1) Text(stringResource(R.string.input_editor_shared, layoutUsers),
|
||||
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp))
|
||||
@@ -309,13 +314,14 @@ private fun FloatingSurfaceTools(draft: SurfaceDraft, onSave: () -> Unit, onLeav
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun SurfaceToolbar(draft: SurfaceDraft, onSave: () -> Unit, onLeave: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
FlowRow(Modifier.fillMaxWidth().padding(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
SecondaryButton(stringResource(R.string.common_cancel), compact = true, onClick = onLeave)
|
||||
SecondaryButton(stringResource(R.string.input_editor_add), enabled = draft.elements.size < 64, compact = true) { draft.adding = true }
|
||||
SecondaryButton(stringResource(R.string.input_editor_undo), enabled = draft.history.isNotEmpty(), compact = true) { draft.undo() }
|
||||
if (draft.defaults != null) SecondaryButton(stringResource(R.string.input_reset_positions), enabled = draft.positioned != null, compact = true) { draft.resetPositions() }
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(draft.snap, { draft.snap = it })
|
||||
Checkbox(draft.snap, { draft.snap = it; ControllerPrefs.setSnap(context, it) })
|
||||
Text(stringResource(R.string.ctrl_editor_snap))
|
||||
}
|
||||
PrimaryButton(stringResource(R.string.input_editor_done), compact = true, onClick = onSave)
|
||||
|
||||
@@ -32,10 +32,12 @@ import com.droiddeck.launcher.input.InputSurfaceStyle
|
||||
import com.droiddeck.launcher.input.pointerSubset
|
||||
import com.droiddeck.launcher.input.InputSurfaceSource
|
||||
import com.droiddeck.launcher.input.InputElementKind
|
||||
import com.droiddeck.launcher.input.InputAction
|
||||
import com.droiddeck.launcher.input.InputButtonIcons
|
||||
import com.droiddeck.launcher.input.InputButtonShape
|
||||
import com.droiddeck.launcher.input.InputButtonImages
|
||||
import com.droiddeck.launcher.input.PadBridge
|
||||
import com.droiddeck.launcher.input.pcKeyLabel
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
@@ -195,7 +197,7 @@ class InputSurfaceView(
|
||||
canvas.addView(when (element.kind) {
|
||||
InputElementKind.STICK -> StickView(context, element, source, element.floating, element.hideWhenIdle && pad != null)
|
||||
InputElementKind.TRACKPAD -> TrackpadView(context, element.control == InputControl.RIGHT_PAD, source = source)
|
||||
InputElementKind.DPAD -> DirectionPad(context, source)
|
||||
InputElementKind.DPAD -> DirectionPad(context, element, source)
|
||||
else -> HoldButton(context, element, source)
|
||||
}.apply { tag = element; contentDescription = element.name; styleElement(this); source?.onStateChanged = { postInvalidate() } })
|
||||
}
|
||||
@@ -541,6 +543,7 @@ class InputSurfaceView(
|
||||
if (touching || clicking) shade(color, 160, 1f) else shade(color, 120, 0.35f), if (touching) light(color, 230, 0.7f) else light(color, 150, 0.25f))
|
||||
if (!element.showLabel) return
|
||||
labelPaint.color = textColor(touching)
|
||||
if (drawDirectionLabels(canvas, source, false, cx, cy, radius * 0.66f, radius * 0.34f, labelPaint)) return
|
||||
labelPaint.textSize = knobRadius * 0.8f
|
||||
val label = if (element.name == element.control.label) glyph(element.control) else element.name
|
||||
canvas.drawText(label, cx + knobX, cy + knobY + labelPaint.textSize * 0.35f, labelPaint)
|
||||
@@ -552,6 +555,9 @@ class InputSurfaceView(
|
||||
canvas.drawCircle(cx, cy, reach(), ring)
|
||||
knob.color = if (touching) ACCENT else KNOB
|
||||
canvas.drawCircle(cx + knobX, cy + knobY, reach() * 0.45f, knob)
|
||||
if (!element.showLabel) return
|
||||
labelPaint.color = if (touching) ACCENT else TEXT
|
||||
drawDirectionLabels(canvas, source, false, cx, cy, reach() * 1.32f, reach() * 0.68f, labelPaint)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -613,10 +619,11 @@ class InputSurfaceView(
|
||||
}
|
||||
}
|
||||
|
||||
private inner class DirectionPad(context: Context, private val source: InputSurfaceSource? = null) : View(context) {
|
||||
private inner class DirectionPad(context: Context, private val element: InputSurfaceElement, private val source: InputSurfaceSource? = null) : View(context) {
|
||||
private val density = resources.displayMetrics.density
|
||||
private var held = 0
|
||||
private val arm = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val label = Paint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER; typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD); color = TEXT }
|
||||
private val rect = RectF()
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
@@ -636,6 +643,7 @@ class InputSurfaceView(
|
||||
armRect(2, cx + thick, cy - thick, cx + half, cy + thick)
|
||||
arm.color = KNOB
|
||||
canvas.drawRect(cx - thick, cy - thick, cx + thick, cy + thick, arm)
|
||||
if (element.showLabel) drawDirectionLabels(canvas, source, true, cx, cy, (half + thick) / 2f, thick * 1.7f, label)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@@ -717,6 +725,29 @@ class InputSurfaceView(
|
||||
else -> control.id.uppercase()
|
||||
}
|
||||
|
||||
private val CARDINAL = listOf(0f to -1f, 1f to 0f, 0f to 1f, -1f to 0f)
|
||||
private val DIAGONAL = listOf(.7071f to -.7071f, .7071f to .7071f, -.7071f to .7071f, -.7071f to -.7071f)
|
||||
|
||||
internal fun directionLabels(context: Context, action: InputAction.Directions?, cardinalOnly: Boolean): List<Pair<Pair<Float, Float>, String>> {
|
||||
if (action == null) return emptyList()
|
||||
val codes = listOf(action.up, action.right, action.down, action.left).zip(CARDINAL) +
|
||||
if (cardinalOnly) emptyList() else action.diagonals.orEmpty().zip(DIAGONAL)
|
||||
return codes.filter { it.first != 0 }.map { (code, unit) -> unit to pcKeyLabel(context, code) }
|
||||
}
|
||||
|
||||
private fun View.drawDirectionLabels(canvas: Canvas, source: InputSurfaceSource?, cardinalOnly: Boolean, cx: Float, cy: Float,
|
||||
distance: Float, box: Float, paint: Paint): Boolean {
|
||||
val labels = directionLabels(context, source?.directions, cardinalOnly)
|
||||
paint.style = Paint.Style.FILL
|
||||
for ((unit, text) in labels) {
|
||||
paint.textSize = box * .6f
|
||||
val measured = paint.measureText(text)
|
||||
if (measured > box) paint.textSize *= box / measured
|
||||
canvas.drawText(text, cx + unit.first * distance, cy + unit.second * distance - (paint.ascent() + paint.descent()) / 2f, paint)
|
||||
}
|
||||
return labels.isNotEmpty()
|
||||
}
|
||||
|
||||
private fun drawArrow(canvas: Canvas, x: Float, y: Float, size: Float, direction: Int, paint: Paint) {
|
||||
val (fx, fy) = when (direction) { 0 -> 0f to -1f; 1 -> 1f to 0f; 2 -> 0f to 1f; else -> -1f to 0f }
|
||||
val path = Path()
|
||||
|
||||
@@ -109,6 +109,7 @@ import com.droiddeck.launcher.stores.itch.ItchUpdates
|
||||
import com.droiddeck.launcher.stores.gog.GogMaintenance
|
||||
import com.droiddeck.launcher.stores.retainOrder
|
||||
import com.droiddeck.launcher.stores.xbox.XboxLibraryFilter
|
||||
import com.droiddeck.launcher.stores.entryTime
|
||||
import com.droiddeck.launcher.stores.sortGames
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -296,8 +297,8 @@ internal fun LibraryPage(s: FrontEndState, a: FrontEndActions, selected: String,
|
||||
val sizesPending = sizeSort && !sizesWaited && candidates.any { sizeOf(it) == null }
|
||||
val sorted = sortGames(
|
||||
candidates, sort, title = { it.title }, releaseDate = { it.item?.releaseDate.orEmpty() },
|
||||
lastPlayed = { e -> e.game?.lastPlayed ?: e.storeKey?.let { lastPlayed[it] } ?: 0L },
|
||||
installedAt = { e -> e.storeKey?.let { installedAt[it] } ?: 0L },
|
||||
lastPlayed = { e -> entryTime(e.game?.lastPlayed, e.storeKey?.let { lastPlayed[it] }) },
|
||||
installedAt = { e -> entryTime(e.storeKey?.let { installedAt[it] }, e.game?.installedAt) },
|
||||
size = { e -> sizeOf(e) ?: 0L },
|
||||
)
|
||||
var stable by remember { mutableStateOf(emptyList<LibraryEntry>()) }
|
||||
|
||||
@@ -323,7 +323,7 @@ class SecondScreenPresentation(
|
||||
context.getString(if (keyboardShown.value) R.string.second_hide_keyboard else R.string.second_show_keyboard),
|
||||
icon = R.drawable.ic_bottom_keyboard, active = keyboardShown.value, tag = "bottom-keyboard") { showKeyboard() }))
|
||||
SecondScreenMode.DECK_CONTROLS -> model(swap, deckPanelTabs(context, deckPanel.value, ::selectDeckPanel))
|
||||
SecondScreenMode.DASHBOARD -> model(swap)
|
||||
SecondScreenMode.DASHBOARD, SecondScreenMode.CUSTOM_CONTROLS -> model(swap)
|
||||
SecondScreenMode.TERMINAL -> model(listOf(BarExtra(context.getString(R.string.drawer_keyboard), icon = R.drawable.ic_bottom_keyboard,
|
||||
tag = "bottom-keyboard") { terminalPanel?.showKeyboard() }))
|
||||
else -> null
|
||||
|
||||
@@ -219,7 +219,6 @@ class DrawerActions(
|
||||
val frameGen: FrameGen.Mode,
|
||||
val lossless: Lossless.State,
|
||||
val oscMode: String,
|
||||
val onScreenButtonsVisible: Boolean,
|
||||
val suspendPolicy: String,
|
||||
val steamDownloadsInBackground: Boolean = false,
|
||||
val micAvailable: Boolean = false,
|
||||
@@ -325,6 +324,7 @@ class DrawerActions(
|
||||
val onStop: () -> Unit,
|
||||
val onStopFrom: ((androidx.compose.ui.geometry.Rect?) -> Unit)? = null,
|
||||
val onClose: () -> Unit,
|
||||
val onDismissScreen: (() -> Unit)? = null,
|
||||
val components: ComponentsManager.Snapshot? = null,
|
||||
val systemPowerVr: Boolean = false,
|
||||
val onComponentsRefresh: () -> Unit = {},
|
||||
@@ -488,7 +488,7 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
) {
|
||||
SteamActionPill(
|
||||
stringResource(R.string.common_close), Icons.Outlined.Close, focus.track(page, "close"),
|
||||
) { host.open = null; a.onClose() }
|
||||
) { host.open = null; (a.onDismissScreen ?: a.onClose)() }
|
||||
if (roomyHeader) Spacer(Modifier.weight(1f))
|
||||
SteamActionPill(
|
||||
stringResource(R.string.stores_dl_pause), Icons.Outlined.Pause, focus.track(page, "pause"),
|
||||
|
||||
@@ -1259,7 +1259,6 @@
|
||||
<string name="deck_tab_gamepad">يد التحكّم</string>
|
||||
<string name="deck_surface_stick">عصا التحكّم</string>
|
||||
<string name="deck_surface_trackpad">لوحة اللمس</string>
|
||||
<string name="deck_surface_switch">التبديل بين عصا التحكّم ولوحة اللمس</string>
|
||||
<string name="deck_close">إغلاق عناصر التحكّم على الشاشة الثانية</string>
|
||||
<string name="deck_open_steam_menu">فتح قائمة Steam</string>
|
||||
<string name="deck_right_trackpad">لوحة اللمس اليمنى</string>
|
||||
|
||||
@@ -1308,7 +1308,6 @@
|
||||
<string name="deck_tab_gamepad">Control</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Cambiar entre stick y trackpad</string>
|
||||
<string name="deck_close">Cerrar controles de la segunda pantalla</string>
|
||||
<string name="deck_open_steam_menu">Abrir menú de Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad derecho</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стик</string>
|
||||
<string name="deck_surface_trackpad">Тракпад</string>
|
||||
<string name="deck_surface_switch">Превключване между стик и тракпад</string>
|
||||
<string name="deck_close">Затваряне на контролите за втория екран</string>
|
||||
<string name="deck_open_steam_menu">Отваряне на менюто на Steam</string>
|
||||
<string name="deck_right_trackpad">Десен тракпад</string>
|
||||
|
||||
@@ -1245,7 +1245,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Páčka</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Přepnout mezi páčkou a trackpadem</string>
|
||||
<string name="deck_close">Zavřít ovládání druhé obrazovky</string>
|
||||
<string name="deck_open_steam_menu">Otevřít nabídku Steamu</string>
|
||||
<string name="deck_right_trackpad">Pravý trackpad</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Skift mellem stick og trackpad</string>
|
||||
<string name="deck_close">Luk kontrollerne til den anden skærm</string>
|
||||
<string name="deck_open_steam_menu">Åbn Steam-menuen</string>
|
||||
<string name="deck_right_trackpad">Højre trackpad</string>
|
||||
|
||||
@@ -1223,7 +1223,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Zwischen Stick und Trackpad wechseln</string>
|
||||
<string name="deck_close">Steuerung auf dem Zweitbildschirm schließen</string>
|
||||
<string name="deck_open_steam_menu">Steam-Menü öffnen</string>
|
||||
<string name="deck_right_trackpad">Rechtes Trackpad</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Χειριστήριο</string>
|
||||
<string name="deck_surface_stick">Μοχλός</string>
|
||||
<string name="deck_surface_trackpad">Πινακίδα αφής</string>
|
||||
<string name="deck_surface_switch">Εναλλαγή μεταξύ μοχλού και πινακίδας αφής</string>
|
||||
<string name="deck_close">Κλείσιμο χειριστηρίων δεύτερης οθόνης</string>
|
||||
<string name="deck_open_steam_menu">Άνοιγμα μενού Steam</string>
|
||||
<string name="deck_right_trackpad">Δεξιά πινακίδα αφής</string>
|
||||
|
||||
@@ -1238,7 +1238,6 @@
|
||||
<string name="deck_tab_gamepad">Mando</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Cambiar entre stick y trackpad</string>
|
||||
<string name="deck_close">Cerrar controles de la segunda pantalla</string>
|
||||
<string name="deck_open_steam_menu">Abrir menú de Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad derecho</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Peliohjain</string>
|
||||
<string name="deck_surface_stick">Tatti</string>
|
||||
<string name="deck_surface_trackpad">Kosketuslevy</string>
|
||||
<string name="deck_surface_switch">Vaihda tatin ja kosketuslevyn välillä</string>
|
||||
<string name="deck_close">Sulje toisen näytön ohjaimet</string>
|
||||
<string name="deck_open_steam_menu">Avaa Steam-valikko</string>
|
||||
<string name="deck_right_trackpad">Oikea kosketuslevy</string>
|
||||
|
||||
@@ -1238,7 +1238,6 @@
|
||||
<string name="deck_tab_gamepad">Manette</string>
|
||||
<string name="deck_surface_stick">Joystick</string>
|
||||
<string name="deck_surface_trackpad">Pavé tactile</string>
|
||||
<string name="deck_surface_switch">Basculer entre joystick et pavé tactile</string>
|
||||
<string name="deck_close">Fermer les commandes du deuxième écran</string>
|
||||
<string name="deck_open_steam_menu">Ouvrir le menu Steam</string>
|
||||
<string name="deck_right_trackpad">Pavé tactile droit</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">गेमपैड</string>
|
||||
<string name="deck_surface_stick">स्टिक</string>
|
||||
<string name="deck_surface_trackpad">ट्रैकपैड</string>
|
||||
<string name="deck_surface_switch">स्टिक और ट्रैकपैड के बीच बदलें</string>
|
||||
<string name="deck_close">दूसरी स्क्रीन के कंट्रोल बंद करें</string>
|
||||
<string name="deck_open_steam_menu">Steam मेनू खोलें</string>
|
||||
<string name="deck_right_trackpad">दायां ट्रैकपैड</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Kontroller</string>
|
||||
<string name="deck_surface_stick">Kar</string>
|
||||
<string name="deck_surface_trackpad">Érintőpad</string>
|
||||
<string name="deck_surface_switch">Váltás a kar és az érintőpad között</string>
|
||||
<string name="deck_close">Második képernyős vezérlők bezárása</string>
|
||||
<string name="deck_open_steam_menu">Steam-menü megnyitása</string>
|
||||
<string name="deck_right_trackpad">Jobb érintőpad</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stik</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Beralih antara stik dan trackpad</string>
|
||||
<string name="deck_close">Tutup kontrol layar kedua</string>
|
||||
<string name="deck_open_steam_menu">Buka menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad kanan</string>
|
||||
|
||||
@@ -1230,7 +1230,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Levetta</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alterna tra levetta e trackpad</string>
|
||||
<string name="deck_close">Chiudi i controlli sul secondo schermo</string>
|
||||
<string name="deck_open_steam_menu">Apri il menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad destro</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">ゲームパッド</string>
|
||||
<string name="deck_surface_stick">スティック</string>
|
||||
<string name="deck_surface_trackpad">トラックパッド</string>
|
||||
<string name="deck_surface_switch">スティックとトラックパッドを切り替え</string>
|
||||
<string name="deck_close">セカンドスクリーンコントロールを閉じる</string>
|
||||
<string name="deck_open_steam_menu">Steamメニューを開く</string>
|
||||
<string name="deck_right_trackpad">右トラックパッド</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">게임패드</string>
|
||||
<string name="deck_surface_stick">스틱</string>
|
||||
<string name="deck_surface_trackpad">터치패드</string>
|
||||
<string name="deck_surface_switch">스틱과 터치패드 간 전환</string>
|
||||
<string name="deck_close">보조 화면 컨트롤 닫기</string>
|
||||
<string name="deck_open_steam_menu">Steam 메뉴 열기</string>
|
||||
<string name="deck_right_trackpad">오른쪽 터치패드</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stik</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Tukar antara stik dan trackpad</string>
|
||||
<string name="deck_close">Tutup kawalan skrin kedua</string>
|
||||
<string name="deck_open_steam_menu">Buka menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad kanan</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Kontroller</string>
|
||||
<string name="deck_surface_stick">Stikke</string>
|
||||
<string name="deck_surface_trackpad">Styreflate</string>
|
||||
<string name="deck_surface_switch">Bytt mellom stikke og styreflate</string>
|
||||
<string name="deck_close">Lukk kontroller for andre skjerm</string>
|
||||
<string name="deck_open_steam_menu">Åpne Steam-menyen</string>
|
||||
<string name="deck_right_trackpad">Høyre styreflate</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Wisselen tussen stick en trackpad</string>
|
||||
<string name="deck_close">Bediening op tweede scherm sluiten</string>
|
||||
<string name="deck_open_steam_menu">Steam-menu openen</string>
|
||||
<string name="deck_right_trackpad">Rechtertrackpad</string>
|
||||
|
||||
@@ -1245,7 +1245,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Gałka</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Przełącz między gałką a trackpadem</string>
|
||||
<string name="deck_close">Zamknij sterowanie na drugim ekranie</string>
|
||||
<string name="deck_open_steam_menu">Otwórz menu Steam</string>
|
||||
<string name="deck_right_trackpad">Prawy trackpad</string>
|
||||
|
||||
@@ -1230,7 +1230,6 @@
|
||||
<string name="deck_tab_gamepad">Comando</string>
|
||||
<string name="deck_surface_stick">Joystick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alternar entre joystick e trackpad</string>
|
||||
<string name="deck_close">Fechar controlos do segundo ecrã</string>
|
||||
<string name="deck_open_steam_menu">Abrir menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad direito</string>
|
||||
|
||||
@@ -1230,7 +1230,6 @@
|
||||
<string name="deck_tab_gamepad">Controle</string>
|
||||
<string name="deck_surface_stick">Analógico</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Alternar entre analógico e trackpad</string>
|
||||
<string name="deck_close">Fechar controles da segunda tela</string>
|
||||
<string name="deck_open_steam_menu">Abrir o menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad direito</string>
|
||||
|
||||
@@ -1238,7 +1238,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Manșă</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Comută între manșă și trackpad</string>
|
||||
<string name="deck_close">Închide comenzile celui de-al doilea ecran</string>
|
||||
<string name="deck_open_steam_menu">Deschide meniul Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad-ul drept</string>
|
||||
|
||||
@@ -1282,7 +1282,6 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стик</string>
|
||||
<string name="deck_surface_trackpad">Тачпад</string>
|
||||
<string name="deck_surface_switch">Переключение между стиком и тачпадом</string>
|
||||
<string name="deck_close">Закрыть управление вторым экраном</string>
|
||||
<string name="deck_open_steam_menu">Открыть меню Steam</string>
|
||||
<string name="deck_right_trackpad">Правый тачпад</string>
|
||||
|
||||
@@ -1231,7 +1231,6 @@
|
||||
<string name="deck_tab_gamepad">Handkontroll</string>
|
||||
<string name="deck_surface_stick">Styrspak</string>
|
||||
<string name="deck_surface_trackpad">Styrplatta</string>
|
||||
<string name="deck_surface_switch">Växla mellan styrspak och styrplatta</string>
|
||||
<string name="deck_close">Stäng kontrollerna på andra skärmen</string>
|
||||
<string name="deck_open_steam_menu">Öppna Steam-menyn</string>
|
||||
<string name="deck_right_trackpad">Höger styrplatta</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">เกมแพด</string>
|
||||
<string name="deck_surface_stick">สติ๊ก</string>
|
||||
<string name="deck_surface_trackpad">แทร็กแพด</string>
|
||||
<string name="deck_surface_switch">สลับระหว่างสติ๊กและแทร็กแพด</string>
|
||||
<string name="deck_close">ปิดการควบคุมหน้าจอที่สอง</string>
|
||||
<string name="deck_open_steam_menu">เปิดเมนู Steam</string>
|
||||
<string name="deck_right_trackpad">แทร็กแพดขวา</string>
|
||||
|
||||
@@ -1223,7 +1223,6 @@
|
||||
<string name="deck_tab_gamepad">Oyun kolu</string>
|
||||
<string name="deck_surface_stick">Çubuk</string>
|
||||
<string name="deck_surface_trackpad">Dokunmatik yüzey</string>
|
||||
<string name="deck_surface_switch">Çubuk ile dokunmatik yüzey arasında geçiş yap</string>
|
||||
<string name="deck_close">İkinci ekran kontrollerini kapat</string>
|
||||
<string name="deck_open_steam_menu">Steam menüsünü aç</string>
|
||||
<string name="deck_right_trackpad">Sağ dokunmatik yüzey</string>
|
||||
|
||||
@@ -1245,7 +1245,6 @@
|
||||
<string name="deck_tab_gamepad">Геймпад</string>
|
||||
<string name="deck_surface_stick">Стік</string>
|
||||
<string name="deck_surface_trackpad">Трекпад</string>
|
||||
<string name="deck_surface_switch">Перемкнути між стіком і трекпадом</string>
|
||||
<string name="deck_close">Закрити керування на другому екрані</string>
|
||||
<string name="deck_open_steam_menu">Відкрити меню Steam</string>
|
||||
<string name="deck_right_trackpad">Правий трекпад</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">Tay cầm</string>
|
||||
<string name="deck_surface_stick">Cần</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Chuyển giữa cần và trackpad</string>
|
||||
<string name="deck_close">Đóng điều khiển màn hình phụ</string>
|
||||
<string name="deck_open_steam_menu">Mở menu Steam</string>
|
||||
<string name="deck_right_trackpad">Trackpad phải</string>
|
||||
|
||||
@@ -1308,7 +1308,6 @@
|
||||
<string name="deck_tab_gamepad">手柄</string>
|
||||
<string name="deck_surface_stick">摇杆</string>
|
||||
<string name="deck_surface_trackpad">触控板</string>
|
||||
<string name="deck_surface_switch">在摇杆与触控板之间切换</string>
|
||||
<string name="deck_close">关闭副屏控制</string>
|
||||
<string name="deck_open_steam_menu">打开 Steam 菜单</string>
|
||||
<string name="deck_right_trackpad">右触控板</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">控制器</string>
|
||||
<string name="deck_surface_stick">搖桿</string>
|
||||
<string name="deck_surface_trackpad">觸控板</string>
|
||||
<string name="deck_surface_switch">在搖桿和觸控板之間切換</string>
|
||||
<string name="deck_close">關閉第二螢幕控制</string>
|
||||
<string name="deck_open_steam_menu">開啟 Steam 選單</string>
|
||||
<string name="deck_right_trackpad">右側觸控板</string>
|
||||
|
||||
@@ -1224,7 +1224,6 @@
|
||||
<string name="deck_tab_gamepad">手把</string>
|
||||
<string name="deck_surface_stick">搖桿</string>
|
||||
<string name="deck_surface_trackpad">觸控板</string>
|
||||
<string name="deck_surface_switch">在搖桿與觸控板之間切換</string>
|
||||
<string name="deck_close">關閉第二螢幕控制</string>
|
||||
<string name="deck_open_steam_menu">開啟 Steam 選單</string>
|
||||
<string name="deck_right_trackpad">右側觸控板</string>
|
||||
|
||||
@@ -1382,7 +1382,6 @@
|
||||
<string name="deck_tab_gamepad">Gamepad</string>
|
||||
<string name="deck_surface_stick">Stick</string>
|
||||
<string name="deck_surface_trackpad">Trackpad</string>
|
||||
<string name="deck_surface_switch">Switch between stick and trackpad</string>
|
||||
<string name="deck_close">Close second-screen controls</string>
|
||||
<string name="deck_open_steam_menu">Open Steam menu</string>
|
||||
<string name="deck_right_trackpad">Right trackpad</string>
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.os.Looper
|
||||
import com.droiddeck.launcher.session.QuestExperimental
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import com.droiddeck.launcher.gpu.TpUbwcHint
|
||||
import com.droiddeck.launcher.gpu.TurnipDriver
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -34,6 +35,31 @@ class DriverMenusAutoTest {
|
||||
ShadowBuild.setDevice("eureka")
|
||||
}
|
||||
|
||||
@Test fun pickingTheAutoDisplayHandsBothHalvesToAuto() {
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MIX)
|
||||
SessionPrefs.setAndroidDriver(activity, "chosen-display")
|
||||
SessionPrefs.setLinuxDriver(activity, "chosen-linux")
|
||||
val menus = DriverMenus(activity, Handler(Looper.getMainLooper()))
|
||||
menus.selectDriver(TurnipDriver.AUTO, linux = false)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(activity))
|
||||
assertEquals(QuestExperimental.DRIVER, SessionPrefs.androidDriver(activity))
|
||||
assertEquals("", SessionPrefs.linuxDriver(activity))
|
||||
}
|
||||
|
||||
@Test fun pickingTheAutoDisplayWithNoPairForTheGpuStillPutsTheDisplayOnAuto() {
|
||||
ShadowBuild.setModel("Generic phone")
|
||||
ShadowBuild.setDevice("generic")
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MIX)
|
||||
SessionPrefs.setAndroidDriver(activity, "chosen-display")
|
||||
SessionPrefs.setLinuxDriver(activity, "chosen-linux")
|
||||
DriverMenus(activity, Handler(Looper.getMainLooper())).selectDriver(TurnipDriver.AUTO, linux = false)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(activity))
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(activity))
|
||||
assertEquals("chosen-linux", SessionPrefs.linuxDriver(activity))
|
||||
}
|
||||
|
||||
@Test fun selectingAutoUsesTheQuestPairWithoutStartingADownload() {
|
||||
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
|
||||
SessionPrefs.setGpuDriverMode(activity, SessionPrefs.GPU_DRIVERS_MANUAL)
|
||||
|
||||
@@ -45,6 +45,30 @@ class LibraryShortcutsLastPlayedTest {
|
||||
assertEquals(mapOf(3388195926L to 1792931476L, 1234L to 1700000000L), Library.shortcutsLastPlayed(root))
|
||||
}
|
||||
|
||||
@Test fun aLaunchedAddedGameRemembersItsPlayTime() {
|
||||
val context = org.robolectric.RuntimeEnvironment.getApplication()
|
||||
val added = Library.SteamGame(-1, "World of Warcraft", null, Library.ADDED, gameId = 0xC0FFEE0002000000uL.toLong())
|
||||
val steam = Library.SteamGame(570, "Dota 2", null, "/steam")
|
||||
assertEquals(0L, Library.recordedPlay(context, added.gameId))
|
||||
val before = System.currentTimeMillis() / 1000
|
||||
Library.recordPlayed(context, added)
|
||||
Library.recordPlayed(context, steam)
|
||||
assert(Library.recordedPlay(context, added.gameId) >= before)
|
||||
assertEquals(0L, Library.recordedPlay(context, steam.gameId))
|
||||
}
|
||||
|
||||
@Test fun anAddedGameBuiltAgainKeepsItsRecordedPlayTime() {
|
||||
val context = org.robolectric.RuntimeEnvironment.getApplication()
|
||||
val dir = tmp.newFolder("Celeste")
|
||||
val exe = File(dir, "Celeste.exe").apply { writeText("x") }
|
||||
val g = AddedGames.Game(dir, "Celeste", exe, "/g/Celeste.exe", "/g", 0x82000003L, 0x82000003L, emptyList())
|
||||
val shown = Library.addedGame(context, g, emptyList())
|
||||
assertEquals(0L, shown.lastPlayed)
|
||||
Library.recordPlayed(context, shown)
|
||||
assertEquals(Library.recordedPlay(context, g.gameId), Library.addedGame(context, g, emptyList()).lastPlayed)
|
||||
assertEquals(1900000000L, Library.addedGame(context, g, emptyList(), lastPlayed = 1900000000L).lastPlayed)
|
||||
}
|
||||
|
||||
@Test fun missingOrBrokenShortcutsGiveNothing() {
|
||||
val root = steamRoot()
|
||||
assertEquals(emptyMap<Long, Long>(), Library.shortcutsLastPlayed(root))
|
||||
|
||||
@@ -101,6 +101,33 @@ class DriverBundleTest {
|
||||
assertTrue(DriverBundle.all(context).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun aDisplayPickedAwayFromABundleTakesItsRuntimeHalfWithIt() {
|
||||
val b = DriverBundle.install(context, bundle())
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, TurnipDriver.AUTO)
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
assertNull(DriverBundle.active(context))
|
||||
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, "turnip25.1.0")
|
||||
assertEquals("turnip25.1.0", SessionPrefs.androidDriver(context))
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
}
|
||||
|
||||
@Test fun aDisplayPickOutsideABundleKeepsTheChosenRuntime() {
|
||||
SessionPrefs.setLinuxDriver(context, "my-linux-driver")
|
||||
SessionPrefs.setAndroidDriver(context, "turnip25.1.0")
|
||||
DriverBundle.selectDisplay(context, TurnipDriver.AUTO)
|
||||
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
|
||||
assertEquals("my-linux-driver", SessionPrefs.linuxDriver(context))
|
||||
|
||||
val b = DriverBundle.install(context, bundle())
|
||||
DriverBundle.select(context, b)
|
||||
DriverBundle.selectDisplay(context, b.androidId)
|
||||
assertEquals(b.linuxId, SessionPrefs.linuxDriver(context))
|
||||
}
|
||||
|
||||
@Test fun aRefusedHalfLeavesNothingInstalled() {
|
||||
try {
|
||||
DriverBundle.install(context, bundle(linuxLibc = "libc.so"))
|
||||
|
||||
@@ -48,6 +48,17 @@ class BottomScreenGuestsTest {
|
||||
assertEquals(null, BottomScreenGuests.holder(null, null, homes))
|
||||
}
|
||||
|
||||
@Test fun anAppThatArrivedBeforeDroidDeckCoveredTheScreenAgainNoLongerHoldsIt() {
|
||||
assertFalse(BottomScreenGuests.current(arrivedAt = 1_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 3_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 2_000, ownShownAt = 2_000))
|
||||
assertTrue(BottomScreenGuests.current(arrivedAt = 1_000, ownShownAt = null))
|
||||
fun holder(arrivedAt: Long, ownShownAt: Long) = BottomScreenGuests.holder(null,
|
||||
"com.android.chrome".takeIf { BottomScreenGuests.current(arrivedAt, ownShownAt) }, homes)
|
||||
assertEquals(null, holder(arrivedAt = 1_000, ownShownAt = 2_000))
|
||||
assertEquals("com.android.chrome", holder(arrivedAt = 3_000, ownShownAt = 2_000))
|
||||
}
|
||||
|
||||
@Test fun keyboardsAndOverlaysNeverTakeTheScreen() {
|
||||
assertTrue(BottomScreenGuests.ignorable(AccessibilityWindowInfo.TYPE_INPUT_METHOD))
|
||||
assertTrue(BottomScreenGuests.ignorable(AccessibilityWindowInfo.TYPE_ACCESSIBILITY_OVERLAY))
|
||||
|
||||
@@ -141,6 +141,26 @@ class DirectTouchHoldTest {
|
||||
assertEquals(listOf("true at 40.0,30.0", "false at 40.0,30.0"), buttons)
|
||||
}
|
||||
|
||||
@Test fun cancelTouchEndsALiveTouchAndKeepsAHeldButton() {
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
touch(MotionEvent.ACTION_MOVE, 60f)
|
||||
hold.cancelTouch()
|
||||
assertEquals(listOf("down 0 at 40.0,30.0", "move 0 at 60.0,30.0", "cancel"), touches)
|
||||
assertTrue(buttons.isEmpty())
|
||||
|
||||
touches.clear()
|
||||
hold.configure(TouchGestureConfig(longPressDelay = 500,
|
||||
directHoldBehavior = TouchGestureConfig.MOUSE_BEHAVIOR_HOLD))
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
advance(500)
|
||||
hold.cancelTouch()
|
||||
advance(1000)
|
||||
assertEquals(listOf("true at 40.0,30.0"), buttons)
|
||||
assertTrue(touches.isEmpty())
|
||||
touch(MotionEvent.ACTION_UP)
|
||||
assertEquals(listOf("true at 40.0,30.0", "false at 40.0,30.0"), buttons)
|
||||
}
|
||||
|
||||
@Test fun cancelForwardsACancelWhenTheTouchIsLive() {
|
||||
touch(MotionEvent.ACTION_DOWN)
|
||||
touch(MotionEvent.ACTION_MOVE, 60f)
|
||||
|
||||
@@ -205,6 +205,28 @@ class OnScreenControlsTest {
|
||||
assertFalse(view.isQuickHidden)
|
||||
}
|
||||
|
||||
@Test fun hidingTheGamepadKeepsAlwaysShownKeysDrawnAndTouchable() {
|
||||
val key = InputSurfaceElement(InputControl.A, .45f, .1f, .1f, .1f, "imported:0", name = "Find", alwaysShown = true)
|
||||
view.updateLayout(SavedInputLayout("test", "Test", listOf(left, button, key), aspect = ASPECT))
|
||||
measure(view)
|
||||
assertTrue(view.hasPinned)
|
||||
view.setPinnedOnly(true)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
assertFalse(view.takesTouchAt(800f, 250f))
|
||||
assertFalse(view.takesTouchAt(200f, 300f))
|
||||
view.setPinnedOnly(false)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(800f, 250f))
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
view.setQuickHidden(true)
|
||||
measure(view)
|
||||
assertTrue(view.takesTouchAt(500f, 90f))
|
||||
assertFalse(view.takesTouchAt(800f, 250f))
|
||||
view.updateLayout(SavedInputLayout("test", "Test", listOf(left, button), aspect = ASPECT))
|
||||
assertFalse(view.hasPinned)
|
||||
}
|
||||
|
||||
private companion object { const val ASPECT = 1000f / 600f }
|
||||
|
||||
private fun touch(action: Int, vararg pointers: Pair<Int, Pair<Float, Float>>, flags: Int = 0): Boolean {
|
||||
|
||||
@@ -19,7 +19,7 @@ class OnScreenLayoutTest {
|
||||
private val density get() = context.resources.displayMetrics.density
|
||||
|
||||
@Before fun reset() {
|
||||
for (name in listOf("input-profiles", "input-surfaces", "controller")) context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
for (name in listOf("input-profiles", "input-surfaces", "controller", "session")) context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().commit()
|
||||
}
|
||||
|
||||
private fun gamepad() = InputLibraries.read(context).layouts.first { it.id == InputLibraries.OVERLAY }
|
||||
@@ -188,4 +188,105 @@ class OnScreenLayoutTest {
|
||||
val selected = old.selectProfile(false, "migrated.screenkeys.0").selectProfile(true, "migrated.screenkeys.0")
|
||||
assertEquals(selected, LegacyScreenKeyMigration.merge(selected, shown))
|
||||
}
|
||||
|
||||
@Test fun alwaysShownKeysStayInButtonsOnlyModeAndLeaveTheUnfoldedStrip() {
|
||||
val key = InputSurfaceElement(InputControl.A, .45f, .1f, .1f, .1f, "imported:0", name = "Find", alwaysShown = true)
|
||||
val plain = InputSurfaceElement(InputControl.A, .45f, .4f, .1f, .1f, "imported:1", name = "Jump")
|
||||
val layout = gamepad().let { it.copy(elements = it.elements + key + plain) }
|
||||
val buttons = OnScreenLayout.elements(context, layout, 1000, 500, Rect(), buttonsOnly = true)
|
||||
assertTrue(buttons.any { it.id == key.id })
|
||||
assertFalse(buttons.any { it.id == plain.id })
|
||||
assertEquals(OnScreenLayout.elements(context, gamepad(), 1000, 500, Rect(), buttonsOnly = true), buttons.filterNot { it.id == key.id })
|
||||
val strip = OnScreenLayout.elements(context, layout, 2208, 800, Rect(), dock = OnScreenLayout.Dock(false, grips = false))
|
||||
assertFalse(strip.any { it.id == key.id })
|
||||
assertTrue(strip.any { it.id == plain.id })
|
||||
assertTrue(OnScreenLayout.elements(context, layout, 1000, 500, Rect()).map { it.id }.containsAll(listOf(key.id, plain.id)))
|
||||
}
|
||||
|
||||
@Test fun migratedScreenKeysAreAlwaysShownButOtherElementsAreNot() {
|
||||
prefs.edit().clear().commit()
|
||||
ControllerPrefs.prefs(context).edit().putString("screenKeys", legacyKeys).commit()
|
||||
val migrated = InputLibraries.read(context)
|
||||
assertTrue(importedOn(migrated).all { it.alwaysShown })
|
||||
assertTrue(migrated.layouts.first { it.id == "migrated.screenkeys.0.layout" }.elements.all { it.alwaysShown })
|
||||
assertTrue(migrated.layouts.flatMap { it.elements }.filterNot { it.id.startsWith("imported:") }.none { it.alwaysShown })
|
||||
assertFalse(LegacyScreenKeyMigration.convert(context, legacyKeys, 1000, 500, 2f).any { it.first.alwaysShown })
|
||||
}
|
||||
|
||||
@Test fun keysMergedByAnEarlierVersionBecomeAlwaysShownOnUpgrade() {
|
||||
val fresh = InputLibraries.read(context)
|
||||
val backup = JSONObject().put("screenKeys", legacyKeys).toString()
|
||||
val unpinned = LegacyScreenKeyMigration.merge(LegacyScreenKeyMigration.migrate(context, fresh, backup), backup).let { library ->
|
||||
library.copy(layouts = library.layouts.map { l -> l.copy(elements = l.elements.map { it.copy(alwaysShown = false) }) })
|
||||
}
|
||||
val other = SavedInputLayout("user.keys", "Mine", listOf(InputSurfaceElement(InputControl.A, .1f, .1f, .1f, .1f, "imported:0", name = "Other")))
|
||||
val stored = unpinned.withLayout(other)
|
||||
prefs.edit().putString("library", JSONObject(InputLibraries.encode(stored)).put("version", 12).toString())
|
||||
.putString(LegacyScreenKeyMigration.BACKUP, backup).putBoolean(LegacyScreenKeyMigration.MERGED, true).commit()
|
||||
val upgraded = InputLibraries.read(context)
|
||||
assertEquals(1, importedOn(upgraded).size)
|
||||
assertTrue(importedOn(upgraded).single().alwaysShown)
|
||||
assertTrue(upgraded.layouts.first { it.id == "migrated.screenkeys.0.layout" }.elements.all { it.alwaysShown })
|
||||
assertFalse(upgraded.layouts.first { it.id == "user.keys" }.elements.single().alwaysShown)
|
||||
assertEquals(stored.layouts.map { l -> l.elements.map { it.copy(alwaysShown = false) } }, upgraded.layouts.filter { l -> stored.layouts.any { it.id == l.id } }
|
||||
.map { l -> l.elements.map { it.copy(alwaysShown = false) } })
|
||||
}
|
||||
|
||||
@Test fun aDeckStripPutsTheDeckGamepadSurfacesInTheStickSlotsAndOtherPadsKeepSticks() {
|
||||
val stock = InputStockLayouts.layouts().first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
val deck = OnScreenLayout.Dock.deck(false, stock)
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LS, InputControl.RIGHT_PAD), deck)
|
||||
assertEquals(deck, OnScreenLayout.Dock.deck(false, null))
|
||||
val (w, h) = 2208 to 800
|
||||
val boxes = GamepadGeometry.layout(w.toFloat(), h.toFloat(), density, false)
|
||||
val placed = OnScreenLayout.elements(context, gamepad(), w, h, Rect(), dock = deck)
|
||||
val pad = placed.single { it.control == InputControl.RIGHT_PAD }
|
||||
assertEquals(InputElementKind.TRACKPAD, pad.kind)
|
||||
assertEquals(boxes.getValue(GamepadGeometry.Slot.RIGHT_MAIN).left, pad.x * w, 1f)
|
||||
assertNull(placed.firstOrNull { it.control == InputControl.RS })
|
||||
assertTrue(placed.any { it.control == InputControl.LS })
|
||||
val plain = OnScreenLayout.elements(context, gamepad(), w, h, Rect(), dock = OnScreenLayout.Dock(false, false))
|
||||
assertTrue(plain.any { it.control == InputControl.RS })
|
||||
assertNull(plain.firstOrNull { it.kind == InputElementKind.TRACKPAD })
|
||||
val swapped = stock.copy(elements = stock.elements.map {
|
||||
when (it.control) {
|
||||
InputControl.LS -> it.copy(control = InputControl.LEFT_PAD, id = "legacy:left-pad", kind = InputElementKind.TRACKPAD)
|
||||
InputControl.RIGHT_PAD -> it.copy(control = InputControl.RS, id = "legacy:rs", kind = InputElementKind.STICK)
|
||||
else -> it
|
||||
}
|
||||
})
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LEFT_PAD, InputControl.RS), OnScreenLayout.Dock.deck(false, swapped))
|
||||
}
|
||||
|
||||
private fun deckGamepad(library: InputLibrary) = library.layouts.first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
|
||||
private fun upgradeWithSurfaces(left: String?, right: String?, change: (InputLibrary) -> InputLibrary = { it }): InputLibrary {
|
||||
val stored = change(InputLibraries.read(context))
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().putString("gamepadLeft", left).putString("gamepadRight", right).commit()
|
||||
prefs.edit().putString("library", JSONObject(InputLibraries.encode(stored)).put("version", 13).toString()).commit()
|
||||
return InputLibraries.read(context)
|
||||
}
|
||||
|
||||
@Test fun theSavedDeckGamepadStickOrTrackpadChoiceMovesIntoTheUntouchedDeckGamepadOnce() {
|
||||
val upgraded = upgradeWithSurfaces("trackpad", "stick")
|
||||
val elements = deckGamepad(upgraded).elements
|
||||
assertEquals(InputStockLayouts.gamepad(leftPad = true, rightStick = true), elements)
|
||||
assertEquals(InputElementKind.TRACKPAD, elements.single { it.control == InputControl.LEFT_PAD }.kind)
|
||||
assertEquals(InputElementKind.STICK, elements.single { it.control == InputControl.RS }.kind)
|
||||
assertNull(elements.firstOrNull { it.control == InputControl.LS || it.control == InputControl.RIGHT_PAD })
|
||||
assertEquals(OnScreenLayout.Dock(false, true, InputControl.LEFT_PAD, InputControl.RS), OnScreenLayout.Dock.deck(false, deckGamepad(upgraded)))
|
||||
assertEquals(upgraded, InputLibraries.read(context))
|
||||
}
|
||||
|
||||
@Test fun theDefaultDeckGamepadChoiceOrAnEditedDeckGamepadIsLeftAlone() {
|
||||
val stock = InputStockLayouts.layouts().first { it.id == DeckPanel.GAMEPAD.layout }
|
||||
assertEquals(stock.elements, deckGamepad(upgradeWithSurfaces(null, null)).elements)
|
||||
assertEquals(stock.elements, deckGamepad(upgradeWithSurfaces("stick", "trackpad")).elements)
|
||||
val moved = stock.elements.map { if (it.control == InputControl.A) it.copy(x = .7f) else it }
|
||||
val edited = upgradeWithSurfaces("trackpad", "stick") { it.withLayout(deckGamepad(it).copy(elements = moved)) }
|
||||
assertEquals(moved, deckGamepad(edited).elements)
|
||||
val others = InputLibraries.read(context).layouts.filter { it.id != DeckPanel.GAMEPAD.layout }
|
||||
assertEquals(InputStockLayouts.layouts().filter { it.id != DeckPanel.GAMEPAD.layout }.map { it.elements },
|
||||
others.filter { l -> InputStockLayouts.layouts().any { it.id == l.id } }.map { it.elements })
|
||||
}
|
||||
}
|
||||
@@ -64,4 +64,13 @@ class SecondScreenModeTest {
|
||||
assertEquals(SecondScreenMode.TERMINAL, SecondScreenMode.opening("terminal", deckPad, dashboard, desktopSession = true))
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun theEditorOpensOnTheChosenBottomScreenUnlessAnotherAppHoldsIt() {
|
||||
val free = { _: Int -> false }
|
||||
assertEquals(listOf(2, 3), SecondScreenDisplays.editorCandidates(2, listOf(2, 3), free))
|
||||
assertEquals(listOf(3, 2), SecondScreenDisplays.editorCandidates(3, listOf(2, 3), free))
|
||||
assertEquals(listOf(-1, 2), SecondScreenDisplays.editorCandidates(-1, listOf(2), free))
|
||||
assertEquals(listOf(3), SecondScreenDisplays.editorCandidates(2, listOf(2, 3)) { it == 2 })
|
||||
assertEquals(emptyList<Int>(), SecondScreenDisplays.editorCandidates(2, listOf(2)) { it == 2 })
|
||||
}
|
||||
}
|
||||
@@ -79,4 +79,18 @@ class LegacyNamesTest {
|
||||
assertEquals("Exec=/usr/local/bin/bannerlator-steam-launch\n",
|
||||
File(root, "usr/local/share/applications/firefox.desktop").readText())
|
||||
}
|
||||
|
||||
@Test fun catalogAppImageMenusStartThroughTheSharedLauncher() {
|
||||
write("usr/share/applications/droiddeck-rpcs3.desktop",
|
||||
"[Desktop Entry]\nName=RPCS3\nExec=env APPIMAGE_EXTRACT_AND_RUN=1 /opt/appimages/rpcs3.AppImage\nCategories=Emulator;\n")
|
||||
write("usr/share/applications/droiddeck-steam-x64.desktop", "[Desktop Entry]\nExec=droiddeck-steam-x64\n")
|
||||
write("usr/share/applications/other.desktop", "Exec=env APPIMAGE_EXTRACT_AND_RUN=1 /opt/appimages/other.AppImage\n")
|
||||
LegacyNames.migrateRootfs(root)
|
||||
LegacyNames.migrateRootfs(root)
|
||||
assertEquals("[Desktop Entry]\nName=RPCS3\nExec=/usr/local/bin/droiddeck-appimage-exec /opt/appimages/rpcs3.AppImage\nCategories=Emulator;\n",
|
||||
File(root, "usr/share/applications/droiddeck-rpcs3.desktop").readText())
|
||||
assertEquals("[Desktop Entry]\nExec=droiddeck-steam-x64\n", File(root, "usr/share/applications/droiddeck-steam-x64.desktop").readText())
|
||||
assertEquals("Exec=env APPIMAGE_EXTRACT_AND_RUN=1 /opt/appimages/other.AppImage\n",
|
||||
File(root, "usr/share/applications/other.desktop").readText())
|
||||
}
|
||||
}
|
||||
@@ -64,4 +64,36 @@ class LinuxFexTest {
|
||||
assertEquals("App-x86_64.AppImage", UserApps.pickAsset(listOf("App-x86_64.AppImage", "App-armhf.AppImage", "notes.txt")))
|
||||
assertNull(UserApps.pickAsset(listOf("App-armhf.AppImage", "App-i686.AppImage")))
|
||||
}
|
||||
|
||||
@Test fun aLinkedAssetPicksTheSameVariantInEveryRelease() {
|
||||
val link = "https://github.com/Djihads80/Eglantine/releases/download/build-129/eglantine-a407703f6d29a5ab73328b4ededb12cf29b2758d-linux-aarch64.AppImage"
|
||||
val wanted = UserApps.githubAsset(link)
|
||||
assertEquals("eglantine-a407703f6d29a5ab73328b4ededb12cf29b2758d-linux-aarch64.AppImage", wanted)
|
||||
assertEquals("Djihads80/Eglantine", UserApps.githubRepo(link))
|
||||
val build = listOf("eglantine-a407703f6d29a5ab73328b4ededb12cf29b2758d-linux-aarch64-pgo.AppImage",
|
||||
"eglantine-a407703f6d29a5ab73328b4ededb12cf29b2758d-linux-aarch64.AppImage")
|
||||
assertEquals(build[1], UserApps.pickAsset(build, wanted))
|
||||
val daily = listOf("eglantine-linux-aarch64-pgo.AppImage", "eglantine-linux-aarch64.AppImage", "eglantine-linux-x86_64.AppImage")
|
||||
assertEquals("eglantine-linux-aarch64.AppImage", UserApps.pickAsset(daily, wanted))
|
||||
assertEquals("eglantine-linux-aarch64-pgo.AppImage", UserApps.pickAsset(daily, "eglantine-0f3c2a91-linux-aarch64-pgo.AppImage"))
|
||||
assertEquals("eglantine-linux-aarch64-pgo.AppImage", UserApps.pickAsset(daily, "Other-v2.1-x86_64.AppImage"))
|
||||
assertEquals("eglantine-linux-aarch64-pgo.AppImage", UserApps.pickAsset(daily))
|
||||
}
|
||||
|
||||
@Test fun onlyReleaseDownloadLinksNameAnAsset() {
|
||||
assertNull(UserApps.githubAsset("https://github.com/Djihads80/Eglantine"))
|
||||
assertNull(UserApps.githubAsset("Djihads80/Eglantine"))
|
||||
assertNull(UserApps.githubAsset("https://github.com/o/r/releases/download/v1/notes.txt"))
|
||||
assertEquals("My App-1.0-arm64.AppImage", UserApps.githubAsset("https://github.com/o/r/releases/download/v1.0/My%20App-1.0-arm64.AppImage"))
|
||||
assertEquals("App-arm64.AppImage", UserApps.githubAsset("https://github.com/o/r/releases/latest/download/App-arm64.AppImage"))
|
||||
assertEquals("o/r", UserApps.githubRepo("https://github.com/o/r/releases/latest/download/App-arm64.AppImage"))
|
||||
assertNull(UserApps.githubAsset("https://github.com/o/r/releases/download/v1/bad%ZZ.AppImage"))
|
||||
}
|
||||
|
||||
@Test fun theLinkedArchitectureWidthIsKept() {
|
||||
val release = listOf("App-2.0-x86-32.AppImage", "App-2.0-x86_64.AppImage", "App-2.0-i686.AppImage")
|
||||
assertEquals("App-2.0-x86_64.AppImage", UserApps.pickAsset(release, "App-1.0-x86_64.AppImage"))
|
||||
assertEquals("App-2.0-x86-32.AppImage", UserApps.pickAsset(release, "App-1.0-x86-32.AppImage"))
|
||||
assertEquals("App-2.0-amd64.AppImage", UserApps.pickAsset(listOf("App-2.0-i386.AppImage", "App-2.0-amd64.AppImage"), "App-1.0-x86_64.AppImage"))
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,14 @@ class GameStorageTest {
|
||||
assertFalse(GameStorage.attached(File("/mnt/media_rw/B2BD-BCFC/STEAM GAMES")) { null })
|
||||
}
|
||||
|
||||
@Test fun aSavedFolderThatIsThereIsAttachedWhateverItsVolumeReports() {
|
||||
val folder = tmp.newFolder("card", "STEAM GAMES")
|
||||
listOf(Environment.MEDIA_UNKNOWN, Environment.MEDIA_UNMOUNTED, Environment.MEDIA_REMOVED, null).forEach {
|
||||
assertTrue(GameStorage.attached(folder, it, externalStoragePath = true))
|
||||
}
|
||||
assertTrue(GameStorage.attached(folder) { Environment.MEDIA_UNKNOWN })
|
||||
}
|
||||
|
||||
@Test fun onlyASavedDriveThatIsMissingCountsAsDisconnected() {
|
||||
SessionPrefs.setGameStorage(app, "", "")
|
||||
assertFalse(GameStorage.disconnected(app))
|
||||
|
||||
@@ -20,6 +20,14 @@ class SortCatalogItemsTest {
|
||||
assertEquals(listOf("Zelda", "Beta", "alpha"), sortCatalogItems(items, "titleDesc").map { it.title })
|
||||
}
|
||||
|
||||
@Test fun anEntryTimeFallsBackWhenTheFirstSourceHasNone() {
|
||||
assertEquals(30L, entryTime(30L, 20L))
|
||||
assertEquals(20L, entryTime(0L, 20L))
|
||||
assertEquals(20L, entryTime(null, 20L))
|
||||
assertEquals(0L, entryTime(0L, null))
|
||||
assertEquals(0L, entryTime(null, 0L))
|
||||
}
|
||||
|
||||
@Test fun playedAndInstalledComeFirstWithTitlesBreakingTies() {
|
||||
val items = listOf(item("1", "Zelda"), item("2", "alpha"), item("3", "Beta"))
|
||||
val played = mapOf("gog:3" to 900L, "gog:1" to 900L)
|
||||
|
||||
@@ -383,9 +383,10 @@ class InputSettingsTest {
|
||||
|
||||
@Test fun elementPropertiesEditOpacityLabelsAndOfferImages() {
|
||||
val element = mutableStateOf(InputSurfaceElement(InputControl.A, .1f, .1f, .2f, .2f, "custom", name = "Custom button"))
|
||||
val action = mutableStateOf<InputAction?>(null)
|
||||
compose.setContent { DroidDeckTheme {
|
||||
androidx.compose.foundation.layout.Column(androidx.compose.ui.Modifier.verticalScroll(androidx.compose.foundation.rememberScrollState())) {
|
||||
InputElementProperties(element.value) { element.value = it }
|
||||
InputElementProperties(element.value, action.value) { element.value = it }
|
||||
}
|
||||
} }
|
||||
compose.onNodeWithTag("setting-element-opacity").performScrollTo().performSemanticsAction(androidx.compose.ui.semantics.SemanticsActions.SetProgress) { it(60f) }
|
||||
@@ -399,6 +400,17 @@ class InputSettingsTest {
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(InputControl.LEFT_PAD, .1f, .1f, .4f, .4f, "pad", InputElementKind.TRACKPAD, "Pad") }
|
||||
compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
compose.onNodeWithTag("setting-element-opacity").assertExists()
|
||||
for (kind in listOf(InputElementKind.DPAD, InputElementKind.STICK)) {
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(kind.control, .1f, .1f, .4f, .4f, "imported:0", kind, "WASD"); action.value = InputAction.Directions(17, 32, 31, 30) }
|
||||
compose.onNodeWithTag("setting-element-label").performScrollTo().performClick()
|
||||
compose.runOnIdle { assertFalse(element.value.showLabel); action.value = InputAction.Native() }
|
||||
compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
compose.runOnIdle { action.value = InputAction.Directions(0, 0, 0, 0, listOf(16, 18, 44, 46)) }
|
||||
if (kind == InputElementKind.STICK) compose.onNodeWithTag("setting-element-label").assertExists()
|
||||
else compose.onNodeWithTag("setting-element-label").assertDoesNotExist()
|
||||
}
|
||||
compose.runOnIdle { element.value = InputSurfaceElement(InputControl.LS, .1f, .1f, .4f, .4f, "legacy:left-stick", InputElementKind.STICK, "Move") }
|
||||
compose.onNodeWithTag("setting-element-label").assertExists()
|
||||
}
|
||||
|
||||
@Test fun editorScalesEveryControlAndRestoresStockPositions() {
|
||||
@@ -421,6 +433,23 @@ class InputSettingsTest {
|
||||
compose.runOnIdle { assertEquals(layout.elements, InputLibraries.read(context).layouts.first { it.id == layout.id }.elements) }
|
||||
}
|
||||
|
||||
@Test fun editorSnapChoiceIsRemembered() {
|
||||
com.droiddeck.launcher.input.ControllerPrefs.setSnap(context, true)
|
||||
val library = InputLibraries.read(context)
|
||||
val layout = library.layout("game", InputRole.SECONDARY)!!
|
||||
val round = mutableStateOf(0)
|
||||
compose.setContent { DroidDeckTheme { androidx.compose.runtime.key(round.value) { InputSurfaceDialog("game", {}, {}, role = InputRole.PRIMARY,
|
||||
layoutId = layout.id, bindingsId = library.profile("game").binding(InputRole.SECONDARY)) } } }
|
||||
val snap = isToggleable() and hasAnySibling(hasText(context.getString(R.string.ctrl_editor_snap)))
|
||||
compose.onNodeWithTag("input-editor-tools").performClick()
|
||||
compose.onNode(snap).performScrollTo().assertIsOn().performClick()
|
||||
compose.runOnIdle { assertFalse(com.droiddeck.launcher.input.ControllerPrefs.snap(context)) }
|
||||
compose.runOnIdle { round.value = 1 }
|
||||
compose.onNodeWithTag("input-editor-tools").performClick()
|
||||
compose.onNode(snap).performScrollTo().assertIsOff()
|
||||
com.droiddeck.launcher.input.ControllerPrefs.setSnap(context, true)
|
||||
}
|
||||
|
||||
@Test fun quickButtonTogglesHideBuiltInButtonsWithoutDeletingThem() {
|
||||
compose.setContent { DroidDeckTheme { InputSettingsPage({}) } }
|
||||
compose.onNodeWithTag("input-profile-game").performScrollTo().performClick()
|
||||
|
||||
@@ -58,6 +58,16 @@ class InputSurfaceViewTest {
|
||||
} finally { root.removeAllViews(); bridge.stop(); controller.pause().stop().destroy() }
|
||||
}
|
||||
|
||||
@Test fun keyedDirectionsAreLabelledWithTheirKeysAndOtherActionsAreNot() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val wasd = InputAction.Directions(17, 32, 31, 30, listOf(18, 0, 0, 16))
|
||||
assertEquals(listOf((0f to -1f) to "W", (1f to 0f) to "D", (0f to 1f) to "S", (-1f to 0f) to "A"),
|
||||
InputSurfaceView.directionLabels(context, wasd, cardinalOnly = true))
|
||||
assertEquals(listOf("W", "D", "S", "A", "E", "Q"), InputSurfaceView.directionLabels(context, wasd, cardinalOnly = false).map { it.second })
|
||||
assertEquals(listOf("W", "A"), InputSurfaceView.directionLabels(context, InputAction.Directions(17, 0, 0, 30), cardinalOnly = false).map { it.second })
|
||||
assertTrue(InputSurfaceView.directionLabels(context, null, cardinalOnly = false).isEmpty())
|
||||
}
|
||||
|
||||
@Test fun takesTouchAtMatchesWhatATouchDoes() {
|
||||
val directory = Files.createTempDirectory("deck-panel").toFile()
|
||||
try {
|
||||
|
||||
@@ -99,7 +99,7 @@ class SecondScreenBarTest {
|
||||
} finally { panel.closeControls() }
|
||||
}
|
||||
|
||||
@Test fun deckPanelHasTheBarWithSwapAndTabsAndProfileControlsStayFullScreen() {
|
||||
@Test fun deckPanelHasTheBarWithSwapAndTabsAndProfileControlsKeepTheBar() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
SessionState.deckPad = true
|
||||
var swaps = 0
|
||||
@@ -116,8 +116,14 @@ class SecondScreenBarTest {
|
||||
assertEquals(DeckPanel.entries.map { context.getString(it.label) }, bar.tabs!!.labels)
|
||||
assertEquals(DeckPanel.BOTH.ordinal, bar.tabs!!.selected)
|
||||
panel.showMode(SecondScreenMode.CUSTOM_CONTROLS)
|
||||
assertNull(panel.barModel(SecondScreenMode.CUSTOM_CONTROLS))
|
||||
assertTrue(rootChildren(panel)[0] is InputSurfaceView)
|
||||
val profile = panel.barModel(SecondScreenMode.CUSTOM_CONTROLS)!!
|
||||
assertEquals(context.getString(R.string.input_profile_controls_mode), profile.title)
|
||||
assertEquals(listOf(context.getString(R.string.swap_screens)), profile.extras.map { it.description })
|
||||
assertNotNull(profile.onClose); assertNotNull(profile.onMenu)
|
||||
profile.extras.single().onClick()
|
||||
assertEquals(2, swaps)
|
||||
assertTrue(rootChildren(panel)[0] is ComposeView)
|
||||
assertTrue(rootChildren(panel)[1] is InputSurfaceView)
|
||||
} finally { panel.closeControls() }
|
||||
val noSwap = presentation(swap = null)
|
||||
try {
|
||||
@@ -221,6 +227,47 @@ class SecondScreenBarTest {
|
||||
compose.onNodeWithTag("bottom-bar-close").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun swappedTopScreenProfileControlsHaveTheBarWithSwap() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
ReflectionHelpers.getField<MutableState<SecondScreenMode>>(activity, "secondScreenMode\$delegate").value = SecondScreenMode.CUSTOM_CONTROLS
|
||||
val method = SessionActivity::class.java.getDeclaredMethod("SwappedTopPanel", androidx.compose.runtime.Composer::class.java, Int::class.javaPrimitiveType)
|
||||
method.isAccessible = true
|
||||
compose.setContent { DroidDeckTheme { method.invoke(activity, currentComposer, 0) } }
|
||||
compose.onNodeWithText(context.getString(R.string.input_profile_controls_mode)).assertIsDisplayed()
|
||||
compose.onNodeWithContentDescription(context.getString(R.string.swap_screens)).assertIsDisplayed()
|
||||
compose.onNodeWithTag("bottom-bar-close").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test fun theBottomPanelWaitsUnderAnOpenInputEditorAndReturnsWhenItCloses() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val id = org.robolectric.shadows.ShadowDisplayManager.addDisplay("w1280dp-h960dp")
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
val displays = activity.getSystemService(DisplayManager::class.java)
|
||||
org.robolectric.Shadows.shadowOf(displays.getDisplay(id)).setFlags(Display.FLAG_PRESENTATION)
|
||||
ReflectionHelpers.setField(activity, "displayManager", displays)
|
||||
ReflectionHelpers.setField(activity, "started", true)
|
||||
ReflectionHelpers.getField<MutableState<Int>>(activity, "selectedSecondScreenDisplay\$delegate").value = id
|
||||
val mode = ReflectionHelpers.getField<MutableState<SecondScreenMode>>(activity, "secondScreenMode\$delegate")
|
||||
mode.value = SecondScreenMode.KEYBOARD_TRACKPAD
|
||||
fun panel() = ReflectionHelpers.getField<SecondScreenPresentation?>(activity, "secondScreenPresentation")
|
||||
fun show() = ReflectionHelpers.callInstanceMethod<Unit>(activity, "showSecondScreen",
|
||||
ReflectionHelpers.ClassParameter.from(SecondScreenMode::class.java, SecondScreenMode.KEYBOARD_TRACKPAD))
|
||||
activity.inputWindowShown(Display.DEFAULT_DISPLAY)
|
||||
activity.inputWindowShown(id)
|
||||
activity.inputWindowShown(id)
|
||||
show()
|
||||
assertNull(panel())
|
||||
assertEquals(SecondScreenMode.KEYBOARD_TRACKPAD, mode.value)
|
||||
activity.inputWindowClosed(id)
|
||||
assertNull(panel())
|
||||
activity.inputWindowClosed(id)
|
||||
try {
|
||||
assertEquals(id, panel()!!.display.displayId)
|
||||
assertTrue(panel()!!.isShowing)
|
||||
} finally { panel()?.closeControls() }
|
||||
}
|
||||
|
||||
@Test fun unfoldedControlsGetTheSessionBar() {
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
|
||||
@@ -376,7 +376,7 @@ class SessionSecondScreenTest {
|
||||
isHomeApp = false, androidApps = emptyList(), hudOn = false,
|
||||
micAvailable = micAvailable, micEnabled = micEnabled, onMicEnabled = onMic,
|
||||
frameGen = FrameGen.Mode.OFF, lossless = Lossless.State.NONE, oscMode = "never",
|
||||
onScreenButtonsVisible = false, suspendPolicy = "auto", backActionsInverted = false,
|
||||
suspendPolicy = "auto", backActionsInverted = false,
|
||||
touchMode = "auto", touchAuto = "trackpad", fexPreset = "default",
|
||||
secondScreenMode = SecondScreenMode.NONE, secondScreenDisplays = displays, selectedSecondScreenDisplay = 1,
|
||||
onHud = {}, onFrameGenPick = {}, onImportLossless = {}, onKeyboard = {}, onHardwareKeyboard = {},
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# DroidDeck 0.7.0
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Droid-Deck/DroidDeck/releases/download/0.7.0/DroidDeck-0.7.0.apk"><img src="https://img.shields.io/badge/Download-DroidDeck%200.7.0%20APK-3DDC84?style=for-the-badge&logo=android&logoColor=white" alt="Download DroidDeck 0.7.0"></a>
|
||||
<a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a>
|
||||
</p>
|
||||
|
||||
**DroidDeck is 100% open source. No ads. No telemetry. No proprietary components. No source withheld.**
|
||||
|
||||
Since 0.6.9, **39 people** sent **194 pull requests**, and **20 of them are first-time contributors**. Our community is strongest when we can build together, and with DroidDeck 0.7.0, our community is about to get A LOT bigger!
|
||||
|
||||
0.7.0 introduces preliminary support for **Mali, PowerVR and Xclipse** GPUs! In addition, we now have GameJolt library and store, dual screen game mode + KDE Desktop, Syncthing integration, improvements for devices with low ram, customizable controller profiles and presets, hardware mouse & keyboard support, live wallpapers, and even more. Click the link in the comments below to... I mean scroll down to learn more:
|
||||
|
||||
### Features
|
||||
|
||||
<p align="center"><img src="media/0.7.0/mali-support.jpg" width="50%" alt="A phone showing DroidDeck Setup with GPU via Mali bridge turned on, in front of handhelds, one of them running Steam"></p>
|
||||
|
||||
- Initial **Mali, PowerVR and Xclipse support**. DroidDeck detects the GPU and picks a driver for it, so these devices can run sessions, with MaliBridge and Venus on Mali and open driver pairs elsewhere. (@ASEbrahim, @mr-abccc, @The412Banner, @wallentx, @Leask, @stefanz92, @DavidCimo, @maxjivi05, @K11MCH1, @xXJSONDeruloXx, [#1587](https://github.com/Droid-Deck/DroidDeck/pull/1587), [#2027](https://github.com/Droid-Deck/DroidDeck/pull/2027), [#1950](https://github.com/Droid-Deck/DroidDeck/pull/1950), [#2029](https://github.com/Droid-Deck/DroidDeck/pull/2029), [#2157](https://github.com/Droid-Deck/DroidDeck/pull/2157))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/swap-game-bottom.jpg" width="40%" alt="A Retroid Pocket Duo with the KDE desktop on the top screen and Rayman 2 running on the bottom screen"> <img src="media/0.7.0/kde-desktop.gif" width="40%" alt="The KDE desktop on the bottom screen of an AYN Thor running fastfetch, then opening a file manager, while Steam stays on the top screen"></p>
|
||||
|
||||
|
||||
- Added the **KDE desktop on the bottom screen** beside a Steam session or a game, next to Dashboard, Keyboard + trackpad, Terminal and Deck grips. (@xXJSONDeruloXx, [#1966](https://github.com/Droid-Deck/DroidDeck/pull/1966))
|
||||
- Added **swap screens**: play on the bottom screen while the top screen shows the Dashboard or the desktop. Use the swap button in the panel bar or **Show the game on** in the session menu. (@The412Banner, [#2006](https://github.com/Droid-Deck/DroidDeck/pull/2006))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/steam-libraries.jpg" width="50%" alt="An AYN Thor with Steam's Storage page on the top screen listing internal storage, the SD card and a shared library, and the Storage Manager on the bottom screen"></p>
|
||||
|
||||
- Added **shared Steam libraries with other pc gaming apps**. DroidDeck can now **find Steam libraries that other apps already made** on your internal storage or card and suggests them, each with its game count, so one install of a game can serve both apps. You can add up to four more libraries beside internal storage and the card, rename them, and remove them any time. (@xXJSONDeruloXx, [#1967](https://github.com/Droid-Deck/DroidDeck/pull/1967), [#2023](https://github.com/Droid-Deck/DroidDeck/pull/2023), [#2054](https://github.com/Droid-Deck/DroidDeck/pull/2054))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/game-jolt.jpg" width="50%" alt="The Game Jolt store in the Stores tab with price, platform, maturity and status filters above a grid of games"></p>
|
||||
|
||||
- Added the **Game Jolt store**: browse, search, install, update and keep a library, in the same style as itch.io. (@FX192, [#1986](https://github.com/Droid-Deck/DroidDeck/pull/1986), [#1989](https://github.com/Droid-Deck/DroidDeck/pull/1989))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/syncthing.jpg" width="50%" alt="The Syncthing page in Setup with Open web UI, Pause, Add folder and a Running switch"></p>
|
||||
|
||||
- Added built-in **Syncthing**, so you can sync saves and games from DroidDeck’s own storage without another app. (@superboo07, [#1442](https://github.com/Droid-Deck/DroidDeck/pull/1442))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/dashboard.jpg" width="50%" alt="An AYN Thor: Redout on the top screen and the Dashboard with In-game stats on the bottom screen"></p>
|
||||
|
||||
- Added **bottom-screen Dashboard** for dual-screen handhelds like the Retroid Pocket Duo and AYN Thor: downloads, notifications, screenshots, Steam chat with voice controls, and **In-game stats** that draw a racing game’s own speed, gear, rpm and pedals from its data output. (@The412Banner, [#1920](https://github.com/Droid-Deck/DroidDeck/pull/1920), [#1933](https://github.com/Droid-Deck/DroidDeck/pull/1933), [#1968](https://github.com/Droid-Deck/DroidDeck/pull/1968))
|
||||
|
||||
<p align="center"><img src="media/0.7.0/hud-strip.jpg" width="50%" alt="The Strip HUD across the top of Steam on an AYN Thor: FPS, frame-time graph, lows, GPU, CPU, VRAM, RAM, battery and clock"></p>
|
||||
|
||||
<p align="center"><img src="media/0.7.0/hud-dock.gif" width="50%" alt="Dragging the HUD to the screen edge shrinks it into a small FPS tab, and tapping the tab opens it again"></p>
|
||||
|
||||
- Added a **Horizontal DroidDeck HUD Style**: one line across the top or bottom edge with FPS, frame-time graph, lows, GPU, CPU, VRAM, RAM, battery and the clock. All DD HUDs can now **shrinks into its tab as you drag it off an edge** and snaps to the sides. The Strip is lighter to draw. (@xXJSONDeruloXx, @The412Banner, @KeegantheVeganat0r, [#1935](https://github.com/Droid-Deck/DroidDeck/pull/1935), [#1937](https://github.com/Droid-Deck/DroidDeck/pull/1937), [#1982](https://github.com/Droid-Deck/DroidDeck/pull/1982), [#2194](https://github.com/Droid-Deck/DroidDeck/pull/2194), [#2235](https://github.com/Droid-Deck/DroidDeck/pull/2235))
|
||||
- A **hardware mouse and keyboard work like a PC**: the camera no longer freezes in games that hide the cursor, the cursor shows in Steam sessions, and clicks land where you point. (@maxjivi05, [#2053](https://github.com/Droid-Deck/DroidDeck/pull/2053), [#2133](https://github.com/Droid-Deck/DroidDeck/pull/2133), [#2139](https://github.com/Droid-Deck/DroidDeck/pull/2139))
|
||||
<p align="center"><img src="media/0.7.0/controller-profiles.jpg" width="35%" alt="The layout editor on the second screen of a dual-screen handheld, with the right trackpad selected and its Properties, Click properties and Remove control buttons above the full-size canvas"></p>
|
||||
|
||||
- Added **unified controller profiles** for Game and Desktop sessions: named profiles combine a physical controller layout with main and second screen layouts, with one full-size editor for both screens and a profile picker in the session menu. (@Delete228, @maxjivi05, @xXJSONDeruloXx, [#1439](https://github.com/Droid-Deck/DroidDeck/pull/1439), [#2215](https://github.com/Droid-Deck/DroidDeck/pull/2215), [#2221](https://github.com/Droid-Deck/DroidDeck/pull/2221), [#2238](https://github.com/Droid-Deck/DroidDeck/pull/2238), [#2242](https://github.com/Droid-Deck/DroidDeck/pull/2242))
|
||||
- Added **Startup** choices: Never, Steam or Desktop. (@xXJSONDeruloXx, [#1993](https://github.com/Droid-Deck/DroidDeck/pull/1993))
|
||||
- Added a **low-storage warning** switch, and an **Android keyboard for game text fields** setting for dual-screen sessions, off by default so Steam keeps its own keyboard. (@xXJSONDeruloXx, [#2244](https://github.com/Droid-Deck/DroidDeck/pull/2244), [#2236](https://github.com/Droid-Deck/DroidDeck/pull/2236))
|
||||
- Added **live wallpapers** to Android’s wallpaper picker. (@xXJSONDeruloXx, [#2012](https://github.com/Droid-Deck/DroidDeck/pull/2012))
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Auto picks DD-Turnip on Adreno 7xx**, and the **upstream RADV pair on Xclipse 950 and 960**. Auto follows the published table on Adreno 6xx and 8xx, and a driver source that is down no longer changes the pick. (@maxjivi05, @xXJSONDeruloXx, [#1978](https://github.com/Droid-Deck/DroidDeck/pull/1978), [#2158](https://github.com/Droid-Deck/DroidDeck/pull/2158), [#2151](https://github.com/Droid-Deck/DroidDeck/pull/2151), [#2233](https://github.com/Droid-Deck/DroidDeck/pull/2233), [#2255](https://github.com/Droid-Deck/DroidDeck/pull/2255), [#2226](https://github.com/Droid-Deck/DroidDeck/pull/2226))
|
||||
- A **configurable stick dead zone** for physical controllers, 0 to 25%. (@vitamin663-sys, [#1780](https://github.com/Droid-Deck/DroidDeck/pull/1780))
|
||||
- The drawer’s **frame limit works in desktop mode**, up to 165 fps. (@K11MCH1, [#1885](https://github.com/Droid-Deck/DroidDeck/pull/1885))
|
||||
- **Faster Steam**: compatibility mapping and app info load in batches, and Steam shows cached library art when it starts without a connection. SD and USB libraries answer missing-file checks from a directory cache. (@ddd-ms, @xXJSONDeruloXx, [#1755](https://github.com/Droid-Deck/DroidDeck/pull/1755), [#1756](https://github.com/Droid-Deck/DroidDeck/pull/1756), [#1985](https://github.com/Droid-Deck/DroidDeck/pull/1985), [#2212](https://github.com/Droid-Deck/DroidDeck/pull/2212), [#2203](https://github.com/Droid-Deck/DroidDeck/pull/2203))
|
||||
- Added **Enhanced diagnostics**, a one-session setting that bundles redacted session, graphics and game logs for bug reports, plus **session log management** in Setup. (@wallentx, @xXJSONDeruloXx, [#2129](https://github.com/Droid-Deck/DroidDeck/pull/2129), [#2099](https://github.com/Droid-Deck/DroidDeck/pull/2099), [#2131](https://github.com/Droid-Deck/DroidDeck/pull/2131))
|
||||
- Added **auto updates for FEX, DXVK and VKD3D-Proton in DroidDeck-Proton**, so it gets the newest builds without a new DroidDeck release. Other Protons keep the components they ship with, old DroidDeck-Proton builds are removed, and a game set to a removed build moves to the newest one. (@maxjivi05, @xXJSONDeruloXx, [#2156](https://github.com/Droid-Deck/DroidDeck/pull/2156), [#2190](https://github.com/Droid-Deck/DroidDeck/pull/2190), [#2193](https://github.com/Droid-Deck/DroidDeck/pull/2193), [#2223](https://github.com/Droid-Deck/DroidDeck/pull/2223))
|
||||
- An installed **DroidDeck-Proton becomes the default** for Steam games and added shortcuts when nobody picked Proton Experimental. A Proton you choose in DroidDeck or in Steam is kept, and both stay in sync. (@maxjivi05, [#2266](https://github.com/Droid-Deck/DroidDeck/pull/2266))
|
||||
|
||||
- **Little Nightmares** keeps its render thread, **+12.7% FPS** on the Retroid Pocket Duo, and **Adreno 722** has fewer DXVK stalls. (@maxjivi05, [#1930](https://github.com/Droid-Deck/DroidDeck/pull/1930), [#2066](https://github.com/Droid-Deck/DroidDeck/pull/2066))
|
||||
- A resumed session reads its memory back from zram at once, which ends minutes of stutter in heavy games like Yakuza. DroidDeck also frees launcher memory earlier under pressure, and idle game memory is compressed before low-RAM devices run out. (@l4nr3, @xXJSONDeruloXx, [#1839](https://github.com/Droid-Deck/DroidDeck/pull/1839), [#2138](https://github.com/Droid-Deck/DroidDeck/pull/2138), [#2187](https://github.com/Droid-Deck/DroidDeck/pull/2187))
|
||||
- On-screen sticks are lighter to hold, and **Low latency off** is honored. (@maxjivi05, [#2048](https://github.com/Droid-Deck/DroidDeck/pull/2048))
|
||||
- The HUD cycles colors with the Inner Gamer theme, the docked FPS tab and bottom-screen frame graph look better, and the watermark is gone with the wordmark in headers. (@xXJSONDeruloXx, [#1932](https://github.com/Droid-Deck/DroidDeck/pull/1932), [#1976](https://github.com/Droid-Deck/DroidDeck/pull/1976), [#2013](https://github.com/Droid-Deck/DroidDeck/pull/2013), [#2011](https://github.com/Droid-Deck/DroidDeck/pull/2011), [#2003](https://github.com/Droid-Deck/DroidDeck/pull/2003), [#1983](https://github.com/Droid-Deck/DroidDeck/pull/1983))
|
||||
- Every session log now records **CPU clocks, thermal status and battery temperature**, and `droiddeckctl` gained `thermal` and `wait cool`. (@angelrenefm, [#2060](https://github.com/Droid-Deck/DroidDeck/pull/2060), [#2061](https://github.com/Droid-Deck/DroidDeck/pull/2061), [#2062](https://github.com/Droid-Deck/DroidDeck/pull/2062))
|
||||
- Added games scan **one at a time and keep what they found** across launches, and imported Steam games with no known build update only when launched. (@K11MCH1, [#2067](https://github.com/Droid-Deck/DroidDeck/pull/2067), [#2064](https://github.com/Droid-Deck/DroidDeck/pull/2064))
|
||||
- Steam’s **VRAM budget** is applied through Vulkan without rejecting allocations, and the **download monitor** no longer freezes a session on slow storage. D3D12 support is detected per device. (@xXJSONDeruloXx, [#2014](https://github.com/Droid-Deck/DroidDeck/pull/2014), [#2020](https://github.com/Droid-Deck/DroidDeck/pull/2020), [#2134](https://github.com/Droid-Deck/DroidDeck/pull/2134))
|
||||
- Every bottom-screen panel now has the **same bar**, and X or Back opens the in-session menu there instead of dropping you on the Android launcher. (@xXJSONDeruloXx, [#2084](https://github.com/Droid-Deck/DroidDeck/pull/2084), [#2110](https://github.com/Droid-Deck/DroidDeck/pull/2110))
|
||||
- On the Duo and Thor, DroidDeck switches to **single-screen mode** while another window holds the bottom screen, such as Retroid’s performance panel, and back when it is free, including when docked. The trackpad and Deck panels are back on the second screen, and the bottom-screen menu and layout editor stay open when the screen turns off. (@maxjivi05, [#1979](https://github.com/Droid-Deck/DroidDeck/pull/1979), [#2232](https://github.com/Droid-Deck/DroidDeck/pull/2232), [#2229](https://github.com/Droid-Deck/DroidDeck/pull/2229), [#2247](https://github.com/Droid-Deck/DroidDeck/pull/2247))
|
||||
- The HUD shows **GPU load and clock without root** on phones where Android blocks them, such as the Retroid Pocket Duo and Adreno 8xx, and in Steam’s overlay. (@The412Banner, @angelrenefm, [#1969](https://github.com/Droid-Deck/DroidDeck/pull/1969), [#1981](https://github.com/Droid-Deck/DroidDeck/pull/1981), [#2059](https://github.com/Droid-Deck/DroidDeck/pull/2059))
|
||||
- Added independent **1% and 0.1% low** toggles to the HUD. (@KeegantheVeganat0r, [#2127](https://github.com/Droid-Deck/DroidDeck/pull/2127))
|
||||
- **EA games install the EA app** through Steam, and the EA sign-in stays in front of the launcher’s splash screen. (@K11MCH1, [#2036](https://github.com/Droid-Deck/DroidDeck/pull/2036), [#2037](https://github.com/Droid-Deck/DroidDeck/pull/2037), [#2045](https://github.com/Droid-Deck/DroidDeck/pull/2045))
|
||||
- The **desktop session follows the app language**, so Plasma and its apps are no longer English only. (@xXJSONDeruloXx, [#1971](https://github.com/Droid-Deck/DroidDeck/pull/1971))
|
||||
- The desktop shows **programs installed by Windows installers** in its menu and as icons. (@K11MCH1, [#2146](https://github.com/Droid-Deck/DroidDeck/pull/2146))
|
||||
- **Follow screen** resolution can be scaled down: ÷1.5, ÷2, ÷2.5, ÷3, ÷4 or your own factor. (@xXJSONDeruloXx, [#1964](https://github.com/Droid-Deck/DroidDeck/pull/1964))
|
||||
- Added a **Hold Back for Steam shortcuts** switch, for people who map a held Back with another app. (@xXJSONDeruloXx, [#1955](https://github.com/Droid-Deck/DroidDeck/pull/1955))
|
||||
- Added **size, opacity and hidden keys** settings for the on-screen PC keyboard. (@Dantalian-dev, [#1897](https://github.com/Droid-Deck/DroidDeck/pull/1897))
|
||||
- Added **World of Warcraft addon import** from a zip, plus **voice type and a keyboard chat bar**. Addons also download from WoWInterface, and CurseForge opens in the browser. (@maxjivi05, [#2111](https://github.com/Droid-Deck/DroidDeck/pull/2111), [#2171](https://github.com/Droid-Deck/DroidDeck/pull/2171), [#2175](https://github.com/Droid-Deck/DroidDeck/pull/2175), [#2219](https://github.com/Droid-Deck/DroidDeck/pull/2219))
|
||||
- **UBWC compatibility** moved from Components to Settings, and FEX’s **Use Default** shows which preset it resolves to. (@xXJSONDeruloXx, [#2008](https://github.com/Droid-Deck/DroidDeck/pull/2008), [#2225](https://github.com/Droid-Deck/DroidDeck/pull/2225))
|
||||
- **Steam launch options** show on the Launch tab. (@wallentx, [#2163](https://github.com/Droid-Deck/DroidDeck/pull/2163))
|
||||
- **GOG achievements** are on by default. (@xXJSONDeruloXx, [#2168](https://github.com/Droid-Deck/DroidDeck/pull/2168))
|
||||
- **Translations:** every missing string is translated, with a fresh Russian pass. (@maxjivi05, @flexeykinDev, [#1965](https://github.com/Droid-Deck/DroidDeck/pull/1965), [#2116](https://github.com/Droid-Deck/DroidDeck/pull/2116))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed **“DroidDeck isn’t responding”** on dual-screen handhelds after the bottom screen sat idle, and opening DroidDeck from iisu on the AYN Thor no longer bounces back. (@maxjivi05, @0x1428571429, [#1917](https://github.com/Droid-Deck/DroidDeck/pull/1917), [#1901](https://github.com/Droid-Deck/DroidDeck/pull/1901))
|
||||
- Fixed **lag and heat on the Galaxy S26** from switching between display modes with the same refresh rate, which started in 0.6.7. (@maxjivi05, [#2192](https://github.com/Droid-Deck/DroidDeck/pull/2192))
|
||||
- Fixed **Steam starting on Quest 3**, and the gamescope abort at start on Quest. (@xXJSONDeruloXx, @Mi-Yomi, [#2050](https://github.com/Droid-Deck/DroidDeck/pull/2050), [#1938](https://github.com/Droid-Deck/DroidDeck/pull/1938))
|
||||
- Fixed **Steam libraries** with installs but no app manifest not being found, and **a stale session’s startup failure** stopping the newer session. (@xXJSONDeruloXx, [#2054](https://github.com/Droid-Deck/DroidDeck/pull/2054), [#1984](https://github.com/Droid-Deck/DroidDeck/pull/1984))
|
||||
- Fixed an **unplugged game drive**: installs fall back to internal storage instead of failing. (@maxjivi05, [#1913](https://github.com/Droid-Deck/DroidDeck/pull/1913))
|
||||
- Fixed **GOG**: an install launches the game and not its config tool, cloud saves upload in Galaxy’s format, and a game whose installer cannot run gets its install registry. (@moronigranja, [#1644](https://github.com/Droid-Deck/DroidDeck/pull/1644), [#1952](https://github.com/Droid-Deck/DroidDeck/pull/1952), [#2022](https://github.com/Droid-Deck/DroidDeck/pull/2022))
|
||||
- Fixed **Battle.net**: Flathub installs outside a session, World of Warcraft: Forever in the Library, the right artwork for each game, games installed inside its folder, Chinese servers and installs, and launching store games from any game page tab. Its pages no longer play videos on the CPU. (@maxjivi05, @visail, [#1923](https://github.com/Droid-Deck/DroidDeck/pull/1923), [#2018](https://github.com/Droid-Deck/DroidDeck/pull/2018), [#2092](https://github.com/Droid-Deck/DroidDeck/pull/2092), [#2087](https://github.com/Droid-Deck/DroidDeck/pull/2087), [#1660](https://github.com/Droid-Deck/DroidDeck/pull/1660), [#1662](https://github.com/Droid-Deck/DroidDeck/pull/1662), [#2185](https://github.com/Droid-Deck/DroidDeck/pull/2185), [#2195](https://github.com/Droid-Deck/DroidDeck/pull/2195))
|
||||
- Fixed **game-specific problems**: Codemasters EGO games, DOOM (2016), Project Zomboid, Supergiant games and Ubisoft Connect. (@The412Banner, @xXJSONDeruloXx, [#1936](https://github.com/Droid-Deck/DroidDeck/pull/1936), [#1987](https://github.com/Droid-Deck/DroidDeck/pull/1987), [#2016](https://github.com/Droid-Deck/DroidDeck/pull/2016), [#2143](https://github.com/Droid-Deck/DroidDeck/pull/2143), [#2090](https://github.com/Droid-Deck/DroidDeck/pull/2090))
|
||||
- Fixed the **Linux desktop** disappearing after a runtime update, language packs, a game’s window following its own window mode, and Steam’s own header moving its windows. (@maxjivi05, [#2021](https://github.com/Droid-Deck/DroidDeck/pull/2021), [#2096](https://github.com/Droid-Deck/DroidDeck/pull/2096), [#2094](https://github.com/Droid-Deck/DroidDeck/pull/2094), [#2097](https://github.com/Droid-Deck/DroidDeck/pull/2097))
|
||||
- Fixed **repaired components** not reaching prefixes that already had them, and a native-only override breaking a prefix with no native DLL. (@maxjivi05, [#2121](https://github.com/Droid-Deck/DroidDeck/pull/2121), [#2142](https://github.com/Droid-Deck/DroidDeck/pull/2142))
|
||||
- Fixed **Windows installs on a natively mounted card** asking for an administrator. (@maxjivi05, [#2100](https://github.com/Droid-Deck/DroidDeck/pull/2100))
|
||||
- Fixed the **Home screen setting** so DroidDeck stays in RedMagic Game Space, and the right click acting as Back in RedMagic cast mode. (@maxjivi05, @OmarAlTaki1, [#2083](https://github.com/Droid-Deck/DroidDeck/pull/2083), [#1870](https://github.com/Droid-Deck/DroidDeck/pull/1870))
|
||||
- Fixed **Steam’s UI crashing on PowerVR** when scrolling the Home shelf fast. (@coffeemeist3r, [#2107](https://github.com/Droid-Deck/DroidDeck/pull/2107))
|
||||
- Fixed **the side menu freezing the launcher** while moving through the Stores tab, and **controller focus** not moving into an opened folder. (@kmnkv1, @danielnoam, [#2105](https://github.com/Droid-Deck/DroidDeck/pull/2105), [#1962](https://github.com/Droid-Deck/DroidDeck/pull/1962))
|
||||
- Fixed **VPN and network** problems: links without a gateway, and network queries inside a session. (@uubooster-team, @xXJSONDeruloXx, [#2038](https://github.com/Droid-Deck/DroidDeck/pull/2038), [#2141](https://github.com/Droid-Deck/DroidDeck/pull/2141))
|
||||
- Fixed **picture-in-picture** output being cropped after a resize and not scaling to full size, and the **glasses pointer** not reaching the full screen or drawing in the picture. (@xXJSONDeruloXx, @moronigranja, [#2125](https://github.com/Droid-Deck/DroidDeck/pull/2125), [#2145](https://github.com/Droid-Deck/DroidDeck/pull/2145), [#1925](https://github.com/Droid-Deck/DroidDeck/pull/1925), [#2174](https://github.com/Droid-Deck/DroidDeck/pull/2174))
|
||||
- Fixed **World of Warcraft and other added games sorting last** in the Library. Battle.net and added games now sort by when they were played and installed, like Steam games. (@maxjivi05, [#2256](https://github.com/Droid-Deck/DroidDeck/pull/2256))
|
||||
- Fixed the **Linux runtime showing as not installed after an app update** that brings a new PRoot pack. Only the changed add-on packs download now. (@maxjivi05, [#2270](https://github.com/Droid-Deck/DroidDeck/pull/2270))
|
||||
- Fixed **desktop apps that would not start**: RPCS3 and other catalog AppImages from the desktop menu, D-Bus programs such as melonDS, x86 programs that a native script starts, and Flatpak apps that Flatpak no longer has now uninstall cleanly. Add from GitHub installs **the AppImage you linked** instead of the release’s first one. (@maxjivi05, [#2274](https://github.com/Droid-Deck/DroidDeck/pull/2274), [#2275](https://github.com/Droid-Deck/DroidDeck/pull/2275), [#2217](https://github.com/Droid-Deck/DroidDeck/pull/2217), [#2216](https://github.com/Droid-Deck/DroidDeck/pull/2216), [#2273](https://github.com/Droid-Deck/DroidDeck/pull/2273))
|
||||
- Fixed **controller navigation** across the Controller tab and profile menus. (@xXJSONDeruloXx, [#2231](https://github.com/Droid-Deck/DroidDeck/pull/2231))
|
||||
- Fixed **games listed with a bundled arm64 Linux build** defaulting to the wrong runner. They now start with FEX. (@phj1081, [#2126](https://github.com/Droid-Deck/DroidDeck/pull/2126))
|
||||
- Fixed smaller things: left adaptive stick activation at the screen edge, game buffers upscaled before composition, the Steam applying-update stage, apps pinned when all cores are selected, runtime PATH kept for guest binaries, Xwayland orphans, GPU environment values with spaces, the navigation bar staying dark, the Android pointer showing after hovering session menus, touch sticks clicking without a fast double tap, wall scrims leaving edge pixels unshaded, the PowerVR bundled PPSSPP install, L2 and R2 keys overriding analog triggers, taps outside the PC keyboard not reaching the game, direct-touch drags in games that hide the cursor, library scan and download cancel crashes on Android 10, and a Second library on a raw media path or an unmounted volume state. (@sanogueralorenzo, @cphillam, @HCOOKIEie, @w1novnik, @asdf17128, @LingWindT, @Systina12, @KirkPatrickJunsay, @realRurichan, @maxjivi05, @xXJSONDeruloXx, [#1455](https://github.com/Droid-Deck/DroidDeck/pull/1455), [#1367](https://github.com/Droid-Deck/DroidDeck/pull/1367), [#1614](https://github.com/Droid-Deck/DroidDeck/pull/1614), [#1759](https://github.com/Droid-Deck/DroidDeck/pull/1759), [#1826](https://github.com/Droid-Deck/DroidDeck/pull/1826), [#1716](https://github.com/Droid-Deck/DroidDeck/pull/1716), [#1931](https://github.com/Droid-Deck/DroidDeck/pull/1931), [#1975](https://github.com/Droid-Deck/DroidDeck/pull/1975), [#2113](https://github.com/Droid-Deck/DroidDeck/pull/2113), [#2176](https://github.com/Droid-Deck/DroidDeck/pull/2176), [#2173](https://github.com/Droid-Deck/DroidDeck/pull/2173), [#1994](https://github.com/Droid-Deck/DroidDeck/pull/1994), [#2205](https://github.com/Droid-Deck/DroidDeck/pull/2205), [#2257](https://github.com/Droid-Deck/DroidDeck/pull/2257), [#2220](https://github.com/Droid-Deck/DroidDeck/pull/2220), [#2204](https://github.com/Droid-Deck/DroidDeck/pull/2204), [#2224](https://github.com/Droid-Deck/DroidDeck/pull/2224), [#2245](https://github.com/Droid-Deck/DroidDeck/pull/2245), [#2258](https://github.com/Droid-Deck/DroidDeck/pull/2258))
|
||||
|
||||
### DroidDeck-Proton, DD-Turnip and components
|
||||
|
||||
These ship on their own and update in the app, so 0.7.0 users get them too.
|
||||
|
||||
- **DroidDeck-Proton 11-12** moves to GE-Proton11-12 and Valve’s newer Wine: XInput rescans only when a controller could have changed, DualSense haptics and adaptive triggers together over USB, MSFS 2024 and other intro videos play normally, an ntsync fix stops waiters blocking forever on an abandoned mutex, plus DXVK 3.1.1, VKD3D-Proton 3.1 and newer FEX. (@The412Banner, @maxjivi05, with GloriousEggroll, Valve and the GE contributors upstream)
|
||||
- **Faster loads in 32-bit and ARM64EC games**: FEX keeps its translated code on empty reads and stays off Wine’s memory lock. Entering the world in Dungeons & Dragons Online went from 50 s to 4 s. (@K11MCH1, [DroidDeck-Proton#9](https://github.com/Droid-Deck/DroidDeck-Proton/pull/9))
|
||||
- **32-bit games load at the address they were built for**, so game and mod loaders that patch them at start work again. (@maxjivi05, [DroidDeck-Proton#10](https://github.com/Droid-Deck/DroidDeck-Proton/pull/10))
|
||||
- **DiRT 3 and DiRT Showdown** keep running with their telemetry on, for the In-game stats dash. (@The412Banner)
|
||||
- About 3,000 system functions no longer look **hooked** to x64 games and launchers that check for that, while ARM64EC callers still skip the emulator (11-12.2). Text fields in Wine programs now tell DroidDeck when they have focus, which brings up the keyboard for World of Warcraft chat (11-12.3). (@maxjivi05)
|
||||
- **DD-Turnip 0.4.0**: newer Mesa, and indirect draws no longer read their arguments too early on Adreno 8xx, which fixes the black or over-shadowed ground in **Mortal Shell 2** and other UE5 games through VKD3D-Proton. (@K11MCH1, [Drivers#14](https://github.com/Droid-Deck/Drivers/pull/14))
|
||||
- **Components**: the full Visual C++ 2010 runtime and MFC for 2015–2022 are installed, the DXVK and VKD3D-Proton nightlies are built as ARM64X, and esync packs give Windows users read access to their folders. (@xXJSONDeruloXx, @maxjivi05, [DroidDeck-Components#10](https://github.com/Droid-Deck/DroidDeck-Components/pull/10), [#11](https://github.com/Droid-Deck/DroidDeck-Components/pull/11), [#12](https://github.com/Droid-Deck/DroidDeck-Components/pull/12), [#13](https://github.com/Droid-Deck/DroidDeck-Components/pull/13))
|
||||
|
||||
### Community
|
||||
|
||||
DroidDeck is built in the open by the people who use it. These are everyone whose code is in 0.7.0:
|
||||
|
||||
| Contributor | Pull requests |
|
||||
|---|---|
|
||||
| @xXJSONDeruloXx | 65: KDE desktop on the bottom screen, more Steam libraries, HUD Strip and docking, live wallpapers, startup choice, diagnostics, PowerVR add-on pack and more |
|
||||
| @maxjivi05 | 70: hardware mouse and keyboard, single-screen mode, Battle.net, desktop fixes, performance and more |
|
||||
| @The412Banner | 9: bottom-screen Dashboard, swap screens, bottom-screen HUD, GPU load without root, game fixes |
|
||||
| @K11MCH1 | 6: EA app, installed Windows programs, scan and frame limit fixes |
|
||||
| @moronigranja | 4: GOG saves and installs, glasses cursor |
|
||||
| @ASEbrahim, @mr-abccc, @The412Banner | MaliBridge for Mali GPUs ([#2027](https://github.com/Droid-Deck/DroidDeck/pull/2027), [#1950](https://github.com/Droid-Deck/DroidDeck/pull/1950)) |
|
||||
| @wallentx, @Leask, @stefanz92, @DavidCimo | Non-Adreno graphics stack and diagnostics ([#2029](https://github.com/Droid-Deck/DroidDeck/pull/2029), [#2129](https://github.com/Droid-Deck/DroidDeck/pull/2129)) |
|
||||
| @angelrenefm | 4: session clocks, GPU clock in Steam’s overlay, `droiddeckctl` |
|
||||
| @FX192 | 2: Game Jolt store |
|
||||
| @visail, @ddd-ms, @kmnkv1 | 2 each: Battle.net China, Steam speedups, Stores freeze and build fix |
|
||||
| @superboo07, @vitamin663-sys, @Dantalian-dev, @KeegantheVeganat0r, @l4nr3, @phj1081, @danielnoam, @cphillam, @asdf17128, @Mi-Yomi, @LingWindT, @KirkPatrickJunsay, @0x1428571429, @OmarAlTaki1, @sanogueralorenzo, @HCOOKIEie, @w1novnik, @Systina12, @coffeemeist3r, @realRurichan, @flexeykinDev, @uubooster-team, @Delete228 | Syncthing, stick dead zone, PC keyboard settings, unified controller profiles, HUD lows, zram resume, FEX default, Stores freeze, folder focus, upscaling, PATH, Quest, Xwayland, iisu, VPN, PowerVR crash, translations and more |
|
||||
|
||||
### New contributors
|
||||
|
||||
- @cphillam made their first contribution in [#1367](https://github.com/Droid-Deck/DroidDeck/pull/1367).
|
||||
- @sanogueralorenzo made their first contribution in [#1455](https://github.com/Droid-Deck/DroidDeck/pull/1455).
|
||||
- @HCOOKIEie made their first contribution in [#1614](https://github.com/Droid-Deck/DroidDeck/pull/1614).
|
||||
- @visail made their first contribution in [#1660](https://github.com/Droid-Deck/DroidDeck/pull/1660).
|
||||
- @LingWindT made their first contribution in [#1716](https://github.com/Droid-Deck/DroidDeck/pull/1716).
|
||||
- @w1novnik made their first contribution in [#1759](https://github.com/Droid-Deck/DroidDeck/pull/1759).
|
||||
- @vitamin663-sys made their first contribution in [#1780](https://github.com/Droid-Deck/DroidDeck/pull/1780).
|
||||
- @OmarAlTaki1 made their first contribution in [#1870](https://github.com/Droid-Deck/DroidDeck/pull/1870).
|
||||
- @Systina12 made their first contribution in [#1931](https://github.com/Droid-Deck/DroidDeck/pull/1931).
|
||||
- @Mi-Yomi made their first contribution in [#1938](https://github.com/Droid-Deck/DroidDeck/pull/1938).
|
||||
- @mr-abccc made their first contribution in [#1950](https://github.com/Droid-Deck/DroidDeck/pull/1950).
|
||||
- @danielnoam made their first contribution in [#1962](https://github.com/Droid-Deck/DroidDeck/pull/1962).
|
||||
- @DavidCimo made their first contribution in [#2029](https://github.com/Droid-Deck/DroidDeck/pull/2029).
|
||||
- @Leask made their first contribution in [#2029](https://github.com/Droid-Deck/DroidDeck/pull/2029).
|
||||
- @stefanz92 made their first contribution in [#2029](https://github.com/Droid-Deck/DroidDeck/pull/2029).
|
||||
- @uubooster-team made their first contribution in [#2038](https://github.com/Droid-Deck/DroidDeck/pull/2038).
|
||||
- @realRurichan made their first contribution in [#2113](https://github.com/Droid-Deck/DroidDeck/pull/2113).
|
||||
- @flexeykinDev made their first contribution in [#2116](https://github.com/Droid-Deck/DroidDeck/pull/2116).
|
||||
- @phj1081 made their first contribution in [#2126](https://github.com/Droid-Deck/DroidDeck/pull/2126).
|
||||
- @KeegantheVeganat0r made their first contribution in [#2127](https://github.com/Droid-Deck/DroidDeck/pull/2127).
|
||||
|
||||
[Full changelog](https://github.com/Droid-Deck/DroidDeck/compare/0.6.9...0.7.0)
|
||||
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 173 KiB |