Skip to content

Commit a60b9be

Browse files
committed
polish scene editor state and destructive affordances
1 parent 86e129c commit a60b9be

6 files changed

Lines changed: 112 additions & 33 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OpenTasker
22

3-
[![Version](https://img.shields.io/badge/version-0.2.64-blue.svg)](https://github.com/SysAdminDoc/OpenTasker/releases)
3+
[![Version](https://img.shields.io/badge/version-0.2.65-blue.svg)](https://github.com/SysAdminDoc/OpenTasker/releases)
44
[![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
55
[![Platform](https://img.shields.io/badge/platform-Android%208.0%2B-brightgreen.svg)](https://developer.android.com)
66
[![Kotlin](https://img.shields.io/badge/kotlin-2.3.21-7f52ff.svg)](https://kotlinlang.org)
@@ -20,7 +20,7 @@
2020

2121
**Planned:** broad device-verified background geofence reliability, elevated (Shizuku) execution, Termux script dispatch, a visual flow authoring editor, and richer plugin UX. See [ROADMAP.md](ROADMAP.md).
2222

23-
> **Status:** the current source version is `0.2.64`. Device-evidence claims (location/calendar/sun) are single-device API 36 data points on `SM-S938B`, not broad background-geofence reliability guarantees. The latest polish pass improves IME handling, compact small-screen bottom navigation with a More menu, variable deletion safety, saveable editor/context/template state, numeric form keyboards, larger day-token touch targets, and accessibility roles for widget/flow/form switch targets.
23+
> **Status:** the current source version is `0.2.65`. Device-evidence claims (location/calendar/sun) are single-device API 36 data points on `SM-S938B`, not broad background-geofence reliability guarantees. The latest polish pass improves IME handling, compact small-screen bottom navigation with a More menu, variable and scene-element deletion safety, saveable editor/context/template/scene-element state, numeric form keyboards, larger day-token touch targets, and accessibility roles for widget/flow/form switch targets.
2424
2525
---
2626

RESEARCH.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Updated: 2026-06-15 (stricter release-polish pass; replaces previous research co
44

55
## Executive Summary
66

7-
OpenTasker is a Kotlin/Compose Android automation app at source version 0.2.64 with a Room-backed profile -> context -> task -> action engine, 49 registered runtime actions, engine-handled flow control, FOSS LocationManager geofencing, Locale/Tasker plugin hosting, Tasker XML import, OpenTasker JSON transfer, Quick Settings/widget triggers, OEM battery guidance, and F-Droid/Play release checks. The repo's best trait is explicit failure behavior: unsupported privileged actions fail honestly, imports are review-gated, sensitive run-log data is redacted, destructive variable deletion is now confirmed, and prior high-risk guards remain in place.
7+
OpenTasker is a Kotlin/Compose Android automation app at source version 0.2.65 with a Room-backed profile -> context -> task -> action engine, 49 registered runtime actions, engine-handled flow control, FOSS LocationManager geofencing, Locale/Tasker plugin hosting, Tasker XML import, OpenTasker JSON transfer, Quick Settings/widget triggers, OEM battery guidance, and F-Droid/Play release checks. The repo's best trait is explicit failure behavior: unsupported privileged actions fail honestly, imports are review-gated, sensitive run-log data is redacted, destructive variable and scene-element deletion are visually confirmed, and prior high-risk guards remain in place.
88

99
The highest-value direction is trust and scale before more action breadth. Existing roadmap items already cover target-SDK readiness, Android 17 audio gating, work/private-profile app automation, TalkBack accessibility, profile/task organization, action implementation tests, WorkManager resilience, diagnostics, Tasker XML export, scoped LAN cleartext, date/time templates, camera/mic privacy triggers, Wake-on-LAN, and optional BYO-key AI authoring.
1010

@@ -60,7 +60,7 @@ Recent continuation passes found three additional non-duplicate gaps; v0.2.62 cl
6060
- Target-SDK wording risk remains: live Google target-API pages checked on 2026-06-15 still publish the API 35 / August 31, 2025 requirement, not a public API 36 Play deadline. Target-36 code has shipped, but docs must not mark an unpublished deadline as verified.
6161
- Android 17 audio risk remains: `MediaActions.kt` and TTS/sound code can run from background profiles while `AutomationService` uses `specialUse|location`; background audio hardening can fail playback or volume changes silently unless a valid foreground-service/while-in-use path exists.
6262
- Work profile / Private Space risk remains: app-open contexts, package broadcasts, and app launch actions are package-name/current-profile based; no model currently records `UserHandle`, profile labels, hidden-profile state, or profile-local disclosure.
63-
- Accessibility gap is narrowed but remains: action checkbox rows, profile/context form switches, widget task rows, and flow graph targets now expose better roles/state; numeric fields request numeric keyboards and small day-token controls meet 48 dp minimum height. A full TalkBack sweep and accessibility-checks instrumentation gate are still missing.
63+
- Accessibility gap is narrowed but remains: action checkbox rows, profile/context form switches, widget task rows, and flow graph targets now expose better roles/state; numeric fields request numeric keyboards, small day-token controls meet 48 dp minimum height, and scene element edit/delete dialog state is restored from stable scene id/index state. A full TalkBack sweep and accessibility-checks instrumentation gate are still missing.
6464
- Action test gap is narrowed but remains: `core/actions/ActionGuardsTest.kt` now covers key network, URL, wait, ping, and Wake-on-LAN guards, while file, media, settings, app, notification, and download edge cases still need coverage.
6565
- Network hardening gap closed in v0.2.62: `HttpPostAction` caps request bodies at 1 MB, uses fixed-length streaming, and keeps legacy `body` compatibility under the same cap.
6666
- New supply-chain gap: `.github/workflows/build.yml` and `release.yml` consume tag-pinned actions (`@v4`), the build workflow has no explicit `permissions`, `gradle/verification-metadata.xml` is absent, and no Dependabot config is present.

ROADMAP.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# OpenTasker Roadmap
22

3-
Source-backed product roadmap for OpenTasker v0.2.64 → v0.3.x. Reconciles current repo state with competitive research across Android automation apps, adjacent workflow engines, platform constraints (API 35–36), distribution policy, and dependency changelogs.
3+
Source-backed product roadmap for OpenTasker v0.2.65 → v0.3.x. Reconciles current repo state with competitive research across Android automation apps, adjacent workflow engines, platform constraints (API 35–36), distribution policy, and dependency changelogs.
44

55
**Last updated:** 2026-06-15
6-
**Roadmap version:** 2026.06.15 stricter release-polish pass (v0.2.64 UI/accessibility/form-state reconciliation)
7-
**Current app version:** 0.2.64
6+
**Roadmap version:** 2026.06.15 stricter release-polish pass (v0.2.65 scene-state/accessibility reconciliation)
7+
**Current app version:** 0.2.65
88
**Planning rule:** items marked "Now" must ship before the v0.3.0 public beta claim. Items already completed in 0.2.x are retained in the **Completed Backlog** appendix for traceability and explicitly removed from active tiers.
99

1010
## Reconciliation note (2026-05-06 pass)
@@ -133,7 +133,7 @@ Key local constraints:
133133

134134
### N9 (2026.05.06) - Documentation truth pass post-v0.2.59
135135

136-
**Status:** Partially refreshed through v0.2.64: README, ROADMAP.md, RESEARCH.md, Gradle version values, F-Droid metadata gates, compact More-menu phone navigation, saveable editor/context/template state, numeric keyboard hints, and row-level switch semantics now reflect the target-SDK-36/action-metadata and stricter UI polish passes. Older architecture and readiness docs still need a current-version truth pass before this item closes. [L14]
136+
**Status:** Partially refreshed through v0.2.65: README, ROADMAP.md, RESEARCH.md, Gradle version values, F-Droid metadata gates, compact More-menu phone navigation, saveable editor/context/template/scene-element state, numeric keyboard hints, row-level switch semantics, and destructive variable/scene-element affordances now reflect the target-SDK-36/action-metadata and stricter UI polish passes. Older architecture and readiness docs still need a current-version truth pass before this item closes. [L14]
137137
**Description:** Refresh README, CLAUDE.md, ARCHITECTURE.md, IMPROVEMENT_PLAN.md, and FDROID_READINESS.md to reflect the current release. Restructure long status copy into skimmable feature lists where needed. Sync version strings, toolchain versions, workflow filenames, fdroid metadata, Gradle properties, ROADMAP, and CHANGELOG.
138138
**Sources:** Local repo state [L1][L5], existing improvement plan [L7], dependency/release audit [L14].
139139
**Category:** docs, dev-experience.

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ val releaseKeystorePath = System.getenv("OPEN_TASKER_RELEASE_KEYSTORE")
1010
val releaseKeystorePassword = System.getenv("OPEN_TASKER_RELEASE_KEYSTORE_PASSWORD")
1111
val releaseKeyAlias = System.getenv("OPEN_TASKER_RELEASE_KEY_ALIAS")
1212
val releaseKeyPassword = System.getenv("OPEN_TASKER_RELEASE_KEY_PASSWORD")
13-
val appVersionCode = 66
14-
val appVersionName = "0.2.64"
13+
val appVersionCode = 67
14+
val appVersionName = "0.2.65"
1515
val allowedDistributions = setOf("standard", "fdroid", "play")
1616
val selectedDistribution = providers.gradleProperty("openTaskerDistribution")
1717
.orElse("standard")

app/src/main/java/com/opentasker/ui/screens/SceneLibraryScreen.kt

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.compose.material.icons.filled.Delete
2929
import androidx.compose.material.icons.filled.Edit
3030
import androidx.compose.material3.AlertDialog
3131
import androidx.compose.material3.Button
32+
import androidx.compose.material3.ButtonDefaults
3233
import androidx.compose.material3.Card
3334
import androidx.compose.material3.CardDefaults
3435
import androidx.compose.material3.DropdownMenu
@@ -43,6 +44,7 @@ import androidx.compose.material3.Surface
4344
import androidx.compose.material3.Text
4445
import androidx.compose.material3.TextButton
4546
import androidx.compose.runtime.Composable
47+
import androidx.compose.runtime.LaunchedEffect
4648
import androidx.compose.runtime.getValue
4749
import androidx.compose.runtime.mutableStateOf
4850
import androidx.compose.runtime.remember
@@ -78,9 +80,30 @@ fun SceneLibraryScreen(
7880
contentPadding: PaddingValues,
7981
) {
8082
var showCreateDialog by rememberSaveable { mutableStateOf(false) }
81-
var elementEditor by remember { mutableStateOf<SceneElementEditorState?>(null) }
82-
var pendingElementDelete by remember { mutableStateOf<SceneElementEditorState?>(null) }
83+
var elementEditorSceneId by rememberSaveable { mutableStateOf<Long?>(null) }
84+
var elementEditorIndex by rememberSaveable { mutableStateOf<Int?>(null) }
85+
var pendingElementDeleteSceneId by rememberSaveable { mutableStateOf<Long?>(null) }
86+
var pendingElementDeleteIndex by rememberSaveable { mutableStateOf<Int?>(null) }
8387
val sortedScenes = remember(scenes) { scenes.sortedBy { it.name.lowercase() } }
88+
val elementEditor = remember(scenes, elementEditorSceneId, elementEditorIndex) {
89+
sceneElementEditorState(scenes, elementEditorSceneId, elementEditorIndex, allowNew = true)
90+
}
91+
val pendingElementDelete = remember(scenes, pendingElementDeleteSceneId, pendingElementDeleteIndex) {
92+
sceneElementEditorState(scenes, pendingElementDeleteSceneId, pendingElementDeleteIndex, allowNew = false)
93+
}
94+
95+
LaunchedEffect(elementEditorSceneId, elementEditor) {
96+
if (elementEditorSceneId != null && elementEditor == null) {
97+
elementEditorSceneId = null
98+
elementEditorIndex = null
99+
}
100+
}
101+
LaunchedEffect(pendingElementDeleteSceneId, pendingElementDelete) {
102+
if (pendingElementDeleteSceneId != null && pendingElementDelete == null) {
103+
pendingElementDeleteSceneId = null
104+
pendingElementDeleteIndex = null
105+
}
106+
}
84107

85108
if (showCreateDialog) {
86109
SceneEditorDialog(
@@ -96,7 +119,10 @@ fun SceneLibraryScreen(
96119
SceneElementEditorDialog(
97120
state = state,
98121
tasks = tasks,
99-
onDismiss = { elementEditor = null },
122+
onDismiss = {
123+
elementEditorSceneId = null
124+
elementEditorIndex = null
125+
},
100126
onSave = { element ->
101127
val updatedScene = if (state.index == null) {
102128
state.scene.copy(elements = state.scene.elements + element)
@@ -108,15 +134,19 @@ fun SceneLibraryScreen(
108134
)
109135
}
110136
onUpdateScene(updatedScene, if (state.index == null) "Element added" else "Element updated")
111-
elementEditor = null
137+
elementEditorSceneId = null
138+
elementEditorIndex = null
112139
},
113140
)
114141
}
115142

116143
pendingElementDelete?.let { state ->
117144
SceneElementDeleteDialog(
118145
state = state,
119-
onDismiss = { pendingElementDelete = null },
146+
onDismiss = {
147+
pendingElementDeleteSceneId = null
148+
pendingElementDeleteIndex = null
149+
},
120150
onConfirm = {
121151
val index = state.index
122152
if (index != null) {
@@ -125,7 +155,8 @@ fun SceneLibraryScreen(
125155
"Element removed",
126156
)
127157
}
128-
pendingElementDelete = null
158+
pendingElementDeleteSceneId = null
159+
pendingElementDeleteIndex = null
129160
},
130161
)
131162
}
@@ -156,9 +187,18 @@ fun SceneLibraryScreen(
156187
SceneCard(
157188
scene = scene,
158189
tasks = tasks,
159-
onAddElement = { elementEditor = SceneElementEditorState(scene = scene) },
160-
onEditElement = { index, element -> elementEditor = SceneElementEditorState(scene, index, element) },
161-
onDeleteElement = { index, element -> pendingElementDelete = SceneElementEditorState(scene, index, element) },
190+
onAddElement = {
191+
elementEditorSceneId = scene.id
192+
elementEditorIndex = null
193+
},
194+
onEditElement = { index, _ ->
195+
elementEditorSceneId = scene.id
196+
elementEditorIndex = index
197+
},
198+
onDeleteElement = { index, _ ->
199+
pendingElementDeleteSceneId = scene.id
200+
pendingElementDeleteIndex = index
201+
},
162202
onMoveElement = { index, element ->
163203
onUpdateScene(
164204
scene.copy(
@@ -175,6 +215,24 @@ fun SceneLibraryScreen(
175215
}
176216
}
177217

218+
private fun sceneElementEditorState(
219+
scenes: List<Scene>,
220+
sceneId: Long?,
221+
index: Int?,
222+
allowNew: Boolean,
223+
): SceneElementEditorState? {
224+
val scene = scenes.firstOrNull { it.id == sceneId } ?: return null
225+
return if (index == null) {
226+
if (allowNew) SceneElementEditorState(scene = scene) else null
227+
} else {
228+
SceneElementEditorState(
229+
scene = scene,
230+
index = index,
231+
element = scene.elements.getOrNull(index) ?: return null,
232+
)
233+
}
234+
}
235+
178236
private data class SceneElementEditorState(
179237
val scene: Scene,
180238
val index: Int? = null,
@@ -508,6 +566,13 @@ private fun SceneElementDeleteDialog(
508566
val element = state.element
509567
AlertDialog(
510568
onDismissRequest = onDismiss,
569+
icon = {
570+
Icon(
571+
Icons.Filled.Delete,
572+
contentDescription = "Remove element",
573+
tint = MaterialTheme.colorScheme.error,
574+
)
575+
},
511576
title = { Text("Remove element?") },
512577
text = {
513578
Text(
@@ -516,7 +581,15 @@ private fun SceneElementDeleteDialog(
516581
)
517582
},
518583
confirmButton = {
519-
Button(onClick = onConfirm) { Text("Remove") }
584+
Button(
585+
onClick = onConfirm,
586+
colors = ButtonDefaults.buttonColors(
587+
containerColor = MaterialTheme.colorScheme.error,
588+
contentColor = MaterialTheme.colorScheme.onError,
589+
),
590+
) {
591+
Text("Remove Element")
592+
}
520593
},
521594
dismissButton = {
522595
TextButton(onClick = onDismiss) { Text("Cancel") }
@@ -534,20 +607,22 @@ private fun SceneElementEditorDialog(
534607
val initial = remember(state) {
535608
state.element ?: SceneElementDrafts.defaultElement(state.scene, SceneElementType.BUTTON)
536609
}
537-
var type by remember(state) { mutableStateOf(initial.type.takeIf { it in SceneElementDrafts.editableTypes } ?: SceneElementType.BUTTON) }
538-
var x by remember(state) { mutableStateOf(initial.xDp.toString()) }
539-
var y by remember(state) { mutableStateOf(initial.yDp.toString()) }
540-
var width by remember(state) { mutableStateOf(initial.widthDp.toString()) }
541-
var height by remember(state) { mutableStateOf(initial.heightDp.toString()) }
542-
var label by remember(state) {
610+
var type by rememberSaveable(state.scene.id, state.index) {
611+
mutableStateOf(initial.type.takeIf { it in SceneElementDrafts.editableTypes } ?: SceneElementType.BUTTON)
612+
}
613+
var x by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.xDp.toString()) }
614+
var y by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.yDp.toString()) }
615+
var width by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.widthDp.toString()) }
616+
var height by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.heightDp.toString()) }
617+
var label by rememberSaveable(state.scene.id, state.index) {
543618
mutableStateOf(initial.config["label"] ?: initial.config["text"] ?: "")
544619
}
545-
var sliderMin by remember(state) { mutableStateOf(initial.config["min"] ?: "0") }
546-
var sliderMax by remember(state) { mutableStateOf(initial.config["max"] ?: "100") }
547-
var sliderValue by remember(state) { mutableStateOf(initial.config["value"] ?: "50") }
548-
var imageSource by remember(state) { mutableStateOf(initial.config["source"] ?: "") }
549-
var tapTaskId by remember(state) { mutableStateOf(initial.tapTaskId) }
550-
var longPressTaskId by remember(state) { mutableStateOf(initial.longPressTaskId) }
620+
var sliderMin by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.config["min"] ?: "0") }
621+
var sliderMax by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.config["max"] ?: "100") }
622+
var sliderValue by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.config["value"] ?: "50") }
623+
var imageSource by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.config["source"] ?: "") }
624+
var tapTaskId by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.tapTaskId) }
625+
var longPressTaskId by rememberSaveable(state.scene.id, state.index) { mutableStateOf(initial.longPressTaskId) }
551626

552627
val parsedX = x.toIntOrNull()
553628
val parsedY = y.toIntOrNull()

app/src/main/java/com/opentasker/ui/screens/VariablesScreen.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@ private fun VariableRow(
168168
)
169169
}
170170
IconButton(onClick = onDelete) {
171-
Icon(Icons.Filled.Delete, contentDescription = "Delete variable")
171+
Icon(
172+
Icons.Filled.Delete,
173+
contentDescription = "Delete variable",
174+
tint = MaterialTheme.colorScheme.error,
175+
)
172176
}
173177
}
174178
}

0 commit comments

Comments
 (0)