Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0c88cd6
bump version to 4.2.2
sds100 Jun 24, 2026
bc11b09
update foss paywall text
sds100 Jun 24, 2026
389a17d
#2163 feat: add ringer mode constraints
claude Jun 27, 2026
106416a
#2174 feat: add default do not remap preference
claude Jun 27, 2026
f3398a6
#2163 refactor: replace three RingerMode data objects with single dat…
claude Jun 29, 2026
30e2f1a
#2163 refactor: use single RingerMode constraint data class with ring…
claude Jun 29, 2026
f08ac66
#2163 style: fix import ordering in ConstraintUtils
claude Jun 29, 2026
56899ee
#2163 fix: use data.ringerMode not constraint.ringerMode in when bran…
claude Jun 29, 2026
e8292f8
fix bugs with do not remap default setting
sds100 Jul 9, 2026
1a325a7
Merge pull request #2177 from keymapperorg/claude/issue-2163-ringer-m…
sds100 Jul 9, 2026
7ad0623
bump version to 4.3.0
sds100 Jul 9, 2026
dd4c15c
fix changelog
sds100 Jul 9, 2026
ee72868
Add two new constraints — "Notification panel is showing" and "Notifi…
sds100 Jul 9, 2026
446de53
#1980 style: fix import ordering in ConstraintUtils.kt
claude Jun 15, 2026
dad0126
#1980 fix notification panel constraint detection
sds100 Jul 9, 2026
34af09d
Merge branch 'feature/1980-notification-panel-constraint' into develop
sds100 Jul 9, 2026
d75a3ca
Merge branch 'develop' into claude/issue-2174-do-not-remap-default
sds100 Jul 9, 2026
3409a69
fix changelog
sds100 Jul 9, 2026
434ddc4
fix test
sds100 Jul 9, 2026
bd5db65
Merge pull request #2178 from keymapperorg/claude/issue-2174-do-not-r…
sds100 Jul 9, 2026
1da4938
#2184 feat: add display resolution constraint
claude Jul 10, 2026
c587e6a
fix changelog
sds100 Jul 10, 2026
044b7ea
#2184 refactor: move display resolution constraint state into view model
claude Jul 10, 2026
2514706
#2184 listen to changes in display resolutions
sds100 Jul 12, 2026
2d8af3b
Merge branch 'claude/elegant-gates-ikjymp' into develop
sds100 Jul 12, 2026
9bb6200
update whats new
sds100 Jul 12, 2026
ae9bba1
remove accidental accessibility event logging
sds100 Jul 13, 2026
c7623f2
#2187 App constraints do not work with floating buttons
sds100 Jul 13, 2026
ec7dbf8
fastlane: check version code is incremented before building for produ…
sds100 Jul 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
937 changes: 628 additions & 309 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions app/version.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VERSION_NAME=4.2.1
VERSION_CODE=256
VERSION_NAME=4.3.0
VERSION_CODE=257
8 changes: 4 additions & 4 deletions base/src/main/assets/whats-new.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Talkback actions
Monochrome app icon
"Any input device" is the default for triggers
• Bug fixes and accessibility improvements
Display resolution constraint
Ringer mode constraint
Set "do not remap" by default in settings
• Bug fixes

📖 View the complete changelog at: http://keymapper.app/changelog
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fun ChooseConstraintScreen(modifier: Modifier = Modifier, viewModel: ChooseConst
val query by viewModel.searchQuery.collectAsStateWithLifecycle()

TimeConstraintBottomSheet(viewModel)
DisplayResolutionConstraintBottomSheet(viewModel)

ChooseConstraintScreen(
modifier = modifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import io.github.sds100.keymapper.base.utils.ui.compose.SimpleListItemModel
import io.github.sds100.keymapper.base.utils.ui.showDialog
import io.github.sds100.keymapper.common.utils.Orientation
import io.github.sds100.keymapper.common.utils.PhysicalOrientation
import io.github.sds100.keymapper.common.utils.SizeKM
import io.github.sds100.keymapper.common.utils.State
import io.github.sds100.keymapper.system.camera.CameraLens
import io.github.sds100.keymapper.system.volume.RingerMode
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -94,6 +96,9 @@ class ChooseConstraintViewModel @Inject constructor(

var timeConstraintState: ConstraintData.Time? by mutableStateOf(null)

var displayResolutionState: DisplayResolutionSheetState? by mutableStateOf(null)
private set

init {
viewModelScope.launch {
returnResult.collect { constraintData ->
Expand All @@ -111,6 +116,49 @@ class ChooseConstraintViewModel @Inject constructor(
}
}

fun onSelectDisplayResolution(resolution: SizeKM) {
displayResolutionState = displayResolutionState?.copy(
isCustom = false,
selectedResolution = resolution,
)
}

fun onSelectCustomDisplayResolution() {
displayResolutionState = displayResolutionState?.copy(isCustom = true)
}

fun onDisplayResolutionWidthChange(width: String) {
displayResolutionState = displayResolutionState?.copy(
widthText = width.filter(Char::isDigit),
)
}

fun onDisplayResolutionHeightChange(height: String) {
displayResolutionState = displayResolutionState?.copy(
heightText = height.filter(Char::isDigit),
)
}

fun onDismissDisplayResolution() {
displayResolutionState = null
}

fun onDoneConfigDisplayResolutionClick() {
val state = displayResolutionState ?: return

val resolution = state.resolvedResolution ?: return

viewModelScope.launch {
returnResult.emit(
ConstraintData.DisplayResolution(
width = resolution.width,
height = resolution.height,
),
)
displayResolutionState = null
}
}

fun onNavigateBack() {
viewModelScope.launch {
popBackStack()
Expand All @@ -125,6 +173,7 @@ class ChooseConstraintViewModel @Inject constructor(
onSelectDisplayOrientationConstraint()
return@launch
}

PHYSICAL_ORIENTATION_LIST_ITEM_ID -> {
onSelectPhysicalOrientationConstraint()
return@launch
Expand All @@ -139,6 +188,7 @@ class ChooseConstraintViewModel @Inject constructor(
-> onSelectAppConstraint(constraintType)

ConstraintId.MEDIA_PLAYING -> returnResult.emit(ConstraintData.MediaPlaying)

ConstraintId.MEDIA_NOT_PLAYING -> returnResult.emit(ConstraintData.NoMediaPlaying)

ConstraintId.BT_DEVICE_CONNECTED,
Expand All @@ -151,6 +201,10 @@ class ChooseConstraintViewModel @Inject constructor(

ConstraintId.SCREEN_OFF -> returnResult.emit(ConstraintData.ScreenOff)

ConstraintId.DISPLAY_RESOLUTION -> {
displayResolutionState = buildDisplayResolutionState()
}

ConstraintId.DISPLAY_ORIENTATION_PORTRAIT ->
returnResult.emit(ConstraintData.OrientationPortrait)

Expand Down Expand Up @@ -216,6 +270,7 @@ class ChooseConstraintViewModel @Inject constructor(
}

ConstraintId.WIFI_ON -> returnResult.emit(ConstraintData.WifiOn)

ConstraintId.WIFI_OFF -> returnResult.emit(ConstraintData.WifiOff)

ConstraintId.WIFI_CONNECTED,
Expand Down Expand Up @@ -249,6 +304,15 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.PHONE_RINGING ->
returnResult.emit(ConstraintData.PhoneRinging)

ConstraintId.RINGER_MODE_NORMAL ->
returnResult.emit(ConstraintData.RingerMode(RingerMode.NORMAL))

ConstraintId.RINGER_MODE_VIBRATE ->
returnResult.emit(ConstraintData.RingerMode(RingerMode.VIBRATE))

ConstraintId.RINGER_MODE_SILENT ->
returnResult.emit(ConstraintData.RingerMode(RingerMode.SILENT))

ConstraintId.CHARGING ->
returnResult.emit(ConstraintData.Charging)

Expand All @@ -267,6 +331,12 @@ class ChooseConstraintViewModel @Inject constructor(
ConstraintId.LOCK_SCREEN_NOT_SHOWING ->
returnResult.emit(ConstraintData.LockScreenNotShowing)

ConstraintId.NOTIFICATION_PANEL_SHOWING ->
returnResult.emit(ConstraintData.NotificationPanelShowing)

ConstraintId.NOTIFICATION_PANEL_NOT_SHOWING ->
returnResult.emit(ConstraintData.NotificationPanelNotShowing)

ConstraintId.TIME -> {
timeConstraintState = ConstraintData.Time(
startHour = 0,
Expand All @@ -279,6 +349,25 @@ class ChooseConstraintViewModel @Inject constructor(
}
}

private fun buildDisplayResolutionState(): DisplayResolutionSheetState {
val supportedResolutions = useCase.getSupportedResolutions()
val currentResolution = useCase.getCurrentResolution()

val matchingResolution = supportedResolutions.firstOrNull {
it.matchesIgnoringOrientation(currentResolution)
}

return DisplayResolutionSheetState(
supportedResolutions = supportedResolutions.sortedBy { it.width },
// Show the text fields immediately when there is nothing meaningful to pick
// from or when the current resolution isn't one of the supported modes.
isCustom = supportedResolutions.size <= 1 || matchingResolution == null,
selectedResolution = matchingResolution ?: supportedResolutions.firstOrNull(),
widthText = currentResolution.width.toString(),
heightText = currentResolution.height.toString(),
)
}

private suspend fun chooseFlashlightLens(): CameraLens? {
val items = useCase.getFlashlightLenses().map { lens ->
val label = when (lens) {
Expand Down Expand Up @@ -320,15 +409,21 @@ class ChooseConstraintViewModel @Inject constructor(

val constraintData = when (selectedOrientation) {
ConstraintId.DISPLAY_ORIENTATION_PORTRAIT -> ConstraintData.OrientationPortrait

ConstraintId.DISPLAY_ORIENTATION_LANDSCAPE -> ConstraintData.OrientationLandscape

ConstraintId.DISPLAY_ORIENTATION_0 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_0)

ConstraintId.DISPLAY_ORIENTATION_90 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_90)

ConstraintId.DISPLAY_ORIENTATION_180 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_180)

ConstraintId.DISPLAY_ORIENTATION_270 ->
ConstraintData.OrientationCustom(orientation = Orientation.ORIENTATION_270)

else -> return
}

Expand Down Expand Up @@ -582,3 +677,51 @@ class ChooseConstraintViewModel @Inject constructor(
returnResult.emit(constraintData)
}
}

/**
* State for the display resolution bottom sheet.
*
* @param supportedResolutions the resolutions the display reports as supported.
* @param isCustom whether the user is entering a custom resolution instead of picking a chip.
* @param selectedResolution the currently selected supported resolution, if any.
* @param widthText the custom width input.
* @param heightText the custom height input.
*/
data class DisplayResolutionSheetState(
val supportedResolutions: List<SizeKM>,
val isCustom: Boolean,
val selectedResolution: SizeKM?,
val widthText: String,
val heightText: String,
) {
private val customWidth: Int? get() = widthText.toIntOrNull()
private val customHeight: Int? get() = heightText.toIntOrNull()

/**
* The resolution that will be saved, or null when the current input is not valid.
*/
val resolvedResolution: SizeKM?
get() = if (isCustom) {
val width = customWidth
val height = customHeight

if (width != null && width > 0 && height != null && height > 0) {
SizeKM(width, height)
} else {
null
}
} else {
selectedResolution
}

val isValid: Boolean get() = resolvedResolution != null
}

/**
* Compares two resolutions ignoring orientation so that e.g. 1080x1920 and 1920x1080
* are treated as the same resolution.
*/
private fun SizeKM.matchesIgnoringOrientation(other: SizeKM): Boolean {
return (width == other.width && height == other.height) ||
(width == other.height && height == other.width)
}
Loading
Loading