Skip to content

Commit

Permalink
Added "Skip to Time" and "Play Next In Queue" commands to "Control Pl…
Browse files Browse the repository at this point in the history
…ayback" Tasker action and cleaned up some code

- Added "Skip to Time" and "Play Next In Queue" commands to "Control Playback" Tasker action
- Updated Changelog
- In the Tasker action configuration screen:
    - Allowed only either the variable selection or the item selection dropdown to show at a time
    - Don't show duplicate Tasker variables and sort them alphabetically
    - Allow user to dismiss dropdown menus by clicking away from them
    - Changed keyboard IME action to "Done"
    - Changed Variable Select icon to be the same as in Tasker itself
    - Made dropdown size limited so it shows correctly in all circumstances
- Added auxiliary "OptionalField" class in the "ViewModelConfigControlPlayback" class so that optional input fields that depend on the type of command being used can very easily be added with minimal code
  • Loading branch information
joaomgcd committed Oct 24, 2022
1 parent 993d7ca commit f0f1f42
Show file tree
Hide file tree
Showing 15 changed files with 165 additions and 346 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* New Features:
* Added a Halloween icon.
([#415](https://github.com/Automattic/pocket-casts-android/pull/415)).
* Added Tasker integration with filter and basic playback controls.
([#415](https://github.com/Automattic/pocket-casts-android/pull/431)).

7.25
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSizeIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Button
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
Expand All @@ -25,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
Expand All @@ -38,19 +42,29 @@ class TaskerInputFieldState<T>(val content: Content<T>) {
val value: String?,
@StringRes val labelResId: Int,
val onTextChange: (String) -> Unit,
val taskerVariables: Array<String>,
val taskerVariables: List<String>,
val possibleItems: List<T>? = null,
val itemToString: (T?) -> String = { it?.toString() ?: "" },
val itemContent: @Composable (T) -> Unit = { Text(text = itemToString(it)) }
)
}

private enum class TaskerInputFieldSelectMode { Variable, ItemList }

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun <T> ComposableTaskerInputField(content: TaskerInputFieldState.Content<T>) {
var isSearching by remember { mutableStateOf(false) }
var isSelectingTaskerVariable by remember { mutableStateOf(false) }
var selectionMode by remember { mutableStateOf(null as TaskerInputFieldSelectMode?) }
val keyboardController = LocalSoftwareKeyboardController.current

/**
* @param selection if null, just hide dropdown and don't signal text change
*/
fun finishSelecting(selection: String? = null) {
selectionMode = null
keyboardController?.hide()
selection?.let { content.onTextChange(it) }
}
Box {

Row {
Expand All @@ -59,24 +73,29 @@ fun <T> ComposableTaskerInputField(content: TaskerInputFieldState.Content<T>) {
val hasSuggestedItems = !possibleItems.isNullOrEmpty()
val hasTaskerVariables = content.taskerVariables.isNotEmpty()
OutlinedTextField(
value = content.value ?: "", label = { Text(text = stringResource(id = content.labelResId)) }, onValueChange = {
modifier = Modifier.weight(1f),
value = content.value ?: "",
label = { Text(text = stringResource(id = content.labelResId)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(onDone = { finishSelecting() }),
onValueChange = {
content.onTextChange(it)
}, modifier = Modifier.weight(1f),
},
trailingIcon = if (!hasSuggestedItems && !hasTaskerVariables) null else {
{
Row {
if (hasTaskerVariables) {
IconButton(onClick = { isSelectingTaskerVariable = true }) {
IconButton(onClick = { selectionMode = TaskerInputFieldSelectMode.Variable }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.images.R.drawable.ic_filters),
painter = painterResource(au.com.shiftyjelly.pocketcasts.taskerplugin.R.drawable.label_outline),
contentDescription = stringResource(R.string.tasker_variables),
tint = MaterialTheme.theme.colors.primaryIcon01,
modifier = Modifier.padding(end = 16.dp, start = 16.dp)
)
}
}
if (hasSuggestedItems) {
IconButton(onClick = { isSearching = true }) {
IconButton(onClick = { selectionMode = TaskerInputFieldSelectMode.ItemList }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.images.R.drawable.ic_search),
contentDescription = stringResource(R.string.search),
Expand All @@ -89,36 +108,35 @@ fun <T> ComposableTaskerInputField(content: TaskerInputFieldState.Content<T>) {
}
}
)
if (possibleItems != null && !possibleItems.isEmpty()) {
val dropdownMaxHeight = screenSize.height / 6 * 2 //at most dropdown can be 2/6 of the screen size so it doesn't draw over its parent
if (hasTaskerVariables) {
DropdownMenu(
expanded = isSearching,
onDismissRequest = { },
modifier = Modifier.requiredSizeIn(maxHeight = dropdownMaxHeight),
expanded = selectionMode == TaskerInputFieldSelectMode.Variable,
onDismissRequest = { finishSelecting() },
properties = PopupProperties(focusable = false)
) {
possibleItems.forEach {
content.taskerVariables.forEach {
DropdownMenuItem(onClick = {
isSearching = false
keyboardController?.hide()
content.onTextChange(content.itemToString(it))
finishSelecting(it)
}) {
content.itemContent(it)
Text(it)
}
}
}
}
if (hasTaskerVariables) {
if (possibleItems != null && possibleItems.isNotEmpty()) {
DropdownMenu(
expanded = isSelectingTaskerVariable,
onDismissRequest = { },
modifier = Modifier.requiredSizeIn(maxHeight = dropdownMaxHeight),
expanded = selectionMode == TaskerInputFieldSelectMode.ItemList,
onDismissRequest = { finishSelecting() },
properties = PopupProperties(focusable = false)
) {
content.taskerVariables.forEach {
possibleItems.forEach {
DropdownMenuItem(onClick = {
isSelectingTaskerVariable = false
keyboardController?.hide()
content.onTextChange(it)
finishSelecting(content.itemToString(it))
}) {
Text(it)
content.itemContent(it)
}
}
}
Expand All @@ -134,7 +152,7 @@ private fun ComposableTaskerInputFieldPreview() {
ComposableTaskerInputField(
TaskerInputFieldState.Content(
"some value", R.string.archive, {},
arrayOf("%test"),
listOf("%test"),
listOf("Hi", "Hello")
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp

val String?.nullIfEmpty get() = if (isNullOrEmpty()) null else this
fun <T> tryOrNull(handleError: ((Throwable) -> T?)? = null, block: () -> T?): T? = try {
block()
} catch (t: Throwable) {
handleError?.invoke(t)
}

val screenSize
@Composable
get() :DpSize {
val configuration = LocalConfiguration.current

val screenHeight = configuration.screenHeightDp.dp
val screenWidth = configuration.screenWidthDp.dp
return DpSize(width = screenWidth, height = screenHeight)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import kotlinx.coroutines.flow.MutableStateFlow

abstract class ViewModelBase<TInput : Any, THelper : TaskerPluginConfigHelperNoOutput<TInput, out TaskerPluginRunnerActionNoOutput<TInput>>>(application: Application) : AndroidViewModel(application), TaskerPluginConfig<TInput> {
override val context get() = getApplication<Application>()
Expand Down Expand Up @@ -47,5 +48,5 @@ abstract class ViewModelBase<TInput : Any, THelper : TaskerPluginConfigHelperNoO

fun finishForTasker() = taskerHelper.finishForTasker()

val taskerVariables get() = taskerHelper.relevantVariables
val taskerVariables by lazy { taskerHelper.relevantVariables.distinct().sortedBy { it } }
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ActionHelperControlPlayback(config: TaskerPluginConfig<InputControlPlaybac
blurbBuilder.append("${context.getString(au.com.shiftyjelly.pocketcasts.localization.R.string.playback_command)}: ${commandEnum?.getDescription(context)}")
when (commandEnum) {
InputControlPlayback.PlaybackCommand.SkipToChapter -> blurbBuilder.append("\n${context.getString(au.com.shiftyjelly.pocketcasts.localization.R.string.chapter_to_skip_to)}: ${inputControlPlayback.chapterToSkipTo}")
InputControlPlayback.PlaybackCommand.SkipToTime -> blurbBuilder.append("\n${context.getString(au.com.shiftyjelly.pocketcasts.localization.R.string.time_to_skip_to_seconds)}: ${inputControlPlayback.timeToSkipToSeconds}")
else -> {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback

import android.content.Context
import au.com.shiftyjelly.pocketcasts.localization.R
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.nullIfEmpty
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.hilt.playbackManager
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.nullIfEmpty
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
Expand All @@ -12,7 +13,8 @@ import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultSucess

private const val ERROR_NO_COMMAND_PROVIDED = 1
private const val ERROR_INVALIUD_COMMAND_PROVIDED = 2
private const val ERROR_INVALIUD_CHAPTER_TO_SKIP_TO_PROVIDED = 3
private const val ERROR_INVALID_CHAPTER_TO_SKIP_TO_PROVIDED = 3
private const val ERROR_INVALID_TIME_TO_SKIP_TO_PROVIDED = 4

class ActionRunnerControlPlayback : TaskerPluginRunnerActionNoOutput<InputControlPlayback>() {

Expand All @@ -26,9 +28,11 @@ class ActionRunnerControlPlayback : TaskerPluginRunnerActionNoOutput<InputContro
InputControlPlayback.PlaybackCommand.SkipToNextChapter -> playbackManager.skipToNextChapter()
InputControlPlayback.PlaybackCommand.SkipToChapter -> {
val chapterToSkipTo = input.regular.chapterToSkipTo
playbackManager.skipToChapter(chapterToSkipTo?.toIntOrNull() ?: return TaskerPluginResultError(ERROR_INVALIUD_CHAPTER_TO_SKIP_TO_PROVIDED, context.getString(R.string.chapter_to_skip_to_not_valid, input.regular.chapterToSkipTo)))
playbackManager.skipToChapter(chapterToSkipTo?.toIntOrNull() ?: return TaskerPluginResultError(ERROR_INVALID_CHAPTER_TO_SKIP_TO_PROVIDED, context.getString(R.string.chapter_to_skip_to_not_valid, input.regular.chapterToSkipTo)))
}
InputControlPlayback.PlaybackCommand.SkipToPrevious -> playbackManager.skipToPreviousChapter()
InputControlPlayback.PlaybackCommand.SkipToTime -> playbackManager.seekToTimeMs(input.regular.timeToSkipToSeconds?.toIntOrNull()?.let { it * 1000 } ?: return TaskerPluginResultError(ERROR_INVALID_TIME_TO_SKIP_TO_PROVIDED, context.getString(R.string.time_to_skip_to_not_valid, input.regular.timeToSkipToSeconds)))
InputControlPlayback.PlaybackCommand.PlayNextInQueue -> playbackManager.playNextInQueue(PlaybackManager.PlaybackSource.TASKER)
}

return TaskerPluginResultSucess()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ import com.joaomgcd.taskerpluginlibrary.input.TaskerInputRoot
@TaskerInputRoot
class InputControlPlayback @JvmOverloads constructor(
@field:TaskerInputField("command") var command: String? = null,
@field:TaskerInputField("skipToChapter") var chapterToSkipTo: String? = null
@field:TaskerInputField("skipToChapter") var chapterToSkipTo: String? = null,
@field:TaskerInputField("timeToSkipToSeconds") var timeToSkipToSeconds: String? = null
) {

val commandEnum get() = tryOrNull { command?.let { PlaybackCommand.valueOf(it) } }

enum class PlaybackCommand(@StringRes val descriptionResId: Int) {
SkipToNextChapter(R.string.skip_to_next_chapter), SkipToPrevious(R.string.skip_to_previous_chapter), SkipToChapter(R.string.skip_to_chapter);
PlayNextInQueue(R.string.play_next_in_queue),
SkipToNextChapter(R.string.skip_to_next_chapter),
SkipToPrevious(R.string.skip_to_previous_chapter),
SkipToChapter(R.string.skip_to_chapter),
SkipToTime(R.string.skip_to_time);

fun getDescription(context: Context) = context.getString(descriptionResId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,30 @@ class ActivityConfigControlPlayback : ActivityConfigBase<ViewModelConfigControlP
) {
Text(viewModel.getDescription(it))
}
val skipToChapterContent = if (viewModel.askForChapterToSkipToState.collectAsState().value) {
val chapterToSkipToContent = if (viewModel.shouldAskForChapter.collectAsState().value) {
TaskerInputFieldState.Content<String>(
viewModel.chapterToSkipToState.collectAsState().value,
viewModel.chapterToSkipTo.collectAsState().value,
au.com.shiftyjelly.pocketcasts.localization.R.string.chapter_to_skip_to,
{ viewModel.chapterToSkipTo = it },
{ viewModel.setChapterToSkipTo(it) },
viewModel.taskerVariables
)
} else {
null
}
val timeToSkipToContent = if (viewModel.showAskForTime.collectAsState().value) {
TaskerInputFieldState.Content<String>(
viewModel.timeToSkipTo.collectAsState().value,
au.com.shiftyjelly.pocketcasts.localization.R.string.time_to_skip_to_seconds,
{ viewModel.setTimeToSkipTo(it) },
viewModel.taskerVariables
)
} else {
null
}
ComposableConfigControlPlayback(
commandContent,
skipToChapterContent
chapterToSkipToContent,
timeToSkipToContent
) { viewModel.finishForTasker() }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
@Composable
fun ComposableConfigControlPlayback(
inputCommandContent: TaskerInputFieldState.Content<InputControlPlayback.PlaybackCommand>,
inputSkipToChapterContent: TaskerInputFieldState.Content<String>?,
inputChapterToSkipToContent: TaskerInputFieldState.Content<String>?,
inputTimeToSkipToContent: TaskerInputFieldState.Content<String>?,
onFinish: () -> Unit
) {
val inputList = mutableListOf<TaskerInputFieldState.Content<*>>(inputCommandContent)
inputSkipToChapterContent?.let { inputList.add(it) }
inputChapterToSkipToContent?.let { inputList.add(it) }
inputTimeToSkipToContent?.let { inputList.add(it) }
ComposableTaskerInputFieldList(inputList, onFinish)
}

Expand All @@ -25,15 +27,20 @@ private fun ComposableConfigControlPlaybackPreview() {
AppTheme(Theme.ThemeType.CLASSIC_LIGHT) {
ComposableConfigControlPlayback(
TaskerInputFieldState.Content(
InputControlPlayback.PlaybackCommand.SkipToChapter.name,
InputControlPlayback.PlaybackCommand.SkipToTime.name,
au.com.shiftyjelly.pocketcasts.localization.R.string.playback_command,
{}, arrayOf("%test"),
{}, listOf("%test"),
InputControlPlayback.PlaybackCommand.values().toList()
),
TaskerInputFieldState.Content(
"1",
au.com.shiftyjelly.pocketcasts.localization.R.string.chapter_to_skip_to,
{}, arrayOf("%test")
{}, listOf("%test")
),
TaskerInputFieldState.Content(
"60",
au.com.shiftyjelly.pocketcasts.localization.R.string.time_to_skip_to_seconds,
{}, listOf("%test")
)
) {}
}
Expand Down
Loading

0 comments on commit f0f1f42

Please sign in to comment.