Skip to content

Commit

Permalink
Added new "Control Playback" Tasker action and added way to directly …
Browse files Browse the repository at this point in the history
…insert Tasker variables in inputs of both actions
  • Loading branch information
joaomgcd committed Oct 21, 2022
1 parent 8084bef commit 993d7ca
Show file tree
Hide file tree
Showing 20 changed files with 431 additions and 94 deletions.
9 changes: 9 additions & 0 deletions modules/features/taskerplugin/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,14 @@
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
<activity
android:name=".controlplayback.config.ActivityConfigControlPlayback"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:label="Pocket Casts Control Playback">
<intent-filter>
<action android:name="com.twofortyfouram.locale.intent.action.EDIT_SETTING" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.taskerplugin.hilt.appTheme
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.hilt.appTheme

abstract class ActivityConfigBase<TViewModel : ViewModelBase<*, *>> : ComponentActivity() {
protected abstract val viewModel: TViewModel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

import androidx.annotation.StringRes
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.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.PopupProperties
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.localization.R
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme

class TaskerInputFieldState<T>(val content: Content<T>) {
data class Content<T> constructor(
val value: String?,
@StringRes val labelResId: Int,
val onTextChange: (String) -> Unit,
val taskerVariables: Array<String>,
val possibleItems: List<T>? = null,
val itemToString: (T?) -> String = { it?.toString() ?: "" },
val itemContent: @Composable (T) -> Unit = { Text(text = itemToString(it)) }
)
}

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

Row {
val possibleItems = content.possibleItems

val hasSuggestedItems = !possibleItems.isNullOrEmpty()
val hasTaskerVariables = content.taskerVariables.isNotEmpty()
OutlinedTextField(
value = content.value ?: "", label = { Text(text = stringResource(id = content.labelResId)) }, onValueChange = {
content.onTextChange(it)
}, modifier = Modifier.weight(1f),
trailingIcon = if (!hasSuggestedItems && !hasTaskerVariables) null else {
{
Row {
if (hasTaskerVariables) {
IconButton(onClick = { isSelectingTaskerVariable = true }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.images.R.drawable.ic_filters),
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 }) {
Icon(
painter = painterResource(au.com.shiftyjelly.pocketcasts.images.R.drawable.ic_search),
contentDescription = stringResource(R.string.search),
tint = MaterialTheme.theme.colors.primaryIcon01,
modifier = Modifier.padding(end = 16.dp, start = 16.dp)
)
}
}
}
}
}
)
if (possibleItems != null && !possibleItems.isEmpty()) {
DropdownMenu(
expanded = isSearching,
onDismissRequest = { },
properties = PopupProperties(focusable = false)
) {
possibleItems.forEach {
DropdownMenuItem(onClick = {
isSearching = false
keyboardController?.hide()
content.onTextChange(content.itemToString(it))
}) {
content.itemContent(it)
}
}
}
}
if (hasTaskerVariables) {
DropdownMenu(
expanded = isSelectingTaskerVariable,
onDismissRequest = { },
properties = PopupProperties(focusable = false)
) {
content.taskerVariables.forEach {
DropdownMenuItem(onClick = {
isSelectingTaskerVariable = false
keyboardController?.hide()
content.onTextChange(it)
}) {
Text(it)
}
}
}
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun ComposableTaskerInputFieldPreview() {
AppTheme(Theme.ThemeType.CLASSIC_LIGHT) {
ComposableTaskerInputField(
TaskerInputFieldState.Content(
"some value", R.string.archive, {},
arrayOf("%test"),
listOf("Hi", "Hello")
)
)
}
}

@Composable
fun ComposableTaskerInputFieldList(
fieldContents: List<TaskerInputFieldState.Content<*>>,
onFinish: () -> Unit
) {
Box(modifier = Modifier.fillMaxHeight()) {
LazyColumn {
fieldContents.forEach { content ->
item {
ComposableTaskerInputField(content)
}
}
}
Button(onClick = onFinish, modifier = Modifier.align(Alignment.BottomEnd)) {
Text(stringResource(R.string.ok))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.base

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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package au.com.shiftyjelly.pocketcasts.taskerplugin.base
import android.app.Application
import android.content.Intent
import androidx.lifecycle.AndroidViewModel
import au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback.InputControlPlayback
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutput
Expand All @@ -23,6 +24,7 @@ abstract class ViewModelBase<TInput : Any, THelper : TaskerPluginConfigHelperNoO
override val inputForTasker: TaskerInput<TInput>
get() = taskerInput

fun getDescription(command: InputControlPlayback.PlaybackCommand) = command.getDescription(context)
override fun assignFromInput(input: TaskerInput<TInput>) {
taskerInput = input
}
Expand All @@ -44,4 +46,6 @@ abstract class ViewModelBase<TInput : Any, THelper : TaskerPluginConfigHelperNoO
override fun setResult(resultCode: Int, data: Intent) = setResultFunc?.invoke(resultCode, data) ?: Unit

fun finishForTasker() = taskerHelper.finishForTasker()

val taskerVariables get() = taskerHelper.relevantVariables
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.hilt
package au.com.shiftyjelly.pocketcasts.taskerplugin.base.hilt

import android.content.Context
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback

import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfig
import com.joaomgcd.taskerpluginlibrary.config.TaskerPluginConfigHelperNoOutput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput

class ActionHelperControlPlayback(config: TaskerPluginConfig<InputControlPlayback>) : TaskerPluginConfigHelperNoOutput<InputControlPlayback, ActionRunnerControlPlayback>(config) {
override val runnerClass: Class<ActionRunnerControlPlayback> get() = ActionRunnerControlPlayback::class.java
override fun addToStringBlurb(input: TaskerInput<InputControlPlayback>, blurbBuilder: StringBuilder) {
val inputControlPlayback = input.regular
val commandEnum = inputControlPlayback.commandEnum
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}")
else -> {}
}
}

override val addDefaultStringBlurb: Boolean
get() = false
override val inputClass: Class<InputControlPlayback>
get() = InputControlPlayback::class.java
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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.taskerplugin.base.hilt.playbackManager
import com.joaomgcd.taskerpluginlibrary.action.TaskerPluginRunnerActionNoOutput
import com.joaomgcd.taskerpluginlibrary.input.TaskerInput
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResult
import com.joaomgcd.taskerpluginlibrary.runner.TaskerPluginResultError
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

class ActionRunnerControlPlayback : TaskerPluginRunnerActionNoOutput<InputControlPlayback>() {

override fun run(context: Context, input: TaskerInput<InputControlPlayback>): TaskerPluginResult<Unit> {
val command = input.regular.command.nullIfEmpty ?: return TaskerPluginResultError(ERROR_NO_COMMAND_PROVIDED, context.getString(au.com.shiftyjelly.pocketcasts.localization.R.string.must_provide_command_name))

val playbackManager = context.playbackManager
val commandEnum = input.regular.commandEnum ?: return TaskerPluginResultError(ERROR_INVALIUD_COMMAND_PROVIDED, context.getString(au.com.shiftyjelly.pocketcasts.localization.R.string.command_x_not_valid, command))

when (commandEnum) {
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)))
}
InputControlPlayback.PlaybackCommand.SkipToPrevious -> playbackManager.skipToPreviousChapter()
}

return TaskerPluginResultSucess()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback

import android.content.Context
import androidx.annotation.StringRes
import au.com.shiftyjelly.pocketcasts.localization.R
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.tryOrNull
import com.joaomgcd.taskerpluginlibrary.input.TaskerInputField
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
) {

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);

fun getDescription(context: Context) = context.getString(descriptionResId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback.config

import androidx.activity.viewModels
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.ActivityConfigBase
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.TaskerInputFieldState
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class ActivityConfigControlPlayback : ActivityConfigBase<ViewModelConfigControlPlayback>() {
override val viewModel: ViewModelConfigControlPlayback by viewModels()

@Composable
override fun Content() {
val commandContent = TaskerInputFieldState.Content(
viewModel.commandState.collectAsState().value,
au.com.shiftyjelly.pocketcasts.localization.R.string.playback_command,
{ viewModel.command = it },
viewModel.taskerVariables,
viewModel.availableCommands.toList()
) {
Text(viewModel.getDescription(it))
}
val skipToChapterContent = if (viewModel.askForChapterToSkipToState.collectAsState().value) {
TaskerInputFieldState.Content<String>(
viewModel.chapterToSkipToState.collectAsState().value,
au.com.shiftyjelly.pocketcasts.localization.R.string.chapter_to_skip_to,
{ viewModel.chapterToSkipTo = it },
viewModel.taskerVariables
)
} else {
null
}
ComposableConfigControlPlayback(
commandContent,
skipToChapterContent
) { viewModel.finishForTasker() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback.config

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import au.com.shiftyjelly.pocketcasts.compose.AppTheme
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.ComposableTaskerInputFieldList
import au.com.shiftyjelly.pocketcasts.taskerplugin.base.TaskerInputFieldState
import au.com.shiftyjelly.pocketcasts.taskerplugin.controlplayback.InputControlPlayback
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme

@Composable
fun ComposableConfigControlPlayback(
inputCommandContent: TaskerInputFieldState.Content<InputControlPlayback.PlaybackCommand>,
inputSkipToChapterContent: TaskerInputFieldState.Content<String>?,
onFinish: () -> Unit
) {
val inputList = mutableListOf<TaskerInputFieldState.Content<*>>(inputCommandContent)
inputSkipToChapterContent?.let { inputList.add(it) }
ComposableTaskerInputFieldList(inputList, onFinish)
}

@Preview(showBackground = true)
@Composable
private fun ComposableConfigControlPlaybackPreview() {
AppTheme(Theme.ThemeType.CLASSIC_LIGHT) {
ComposableConfigControlPlayback(
TaskerInputFieldState.Content(
InputControlPlayback.PlaybackCommand.SkipToChapter.name,
au.com.shiftyjelly.pocketcasts.localization.R.string.playback_command,
{}, arrayOf("%test"),
InputControlPlayback.PlaybackCommand.values().toList()
),
TaskerInputFieldState.Content(
"1",
au.com.shiftyjelly.pocketcasts.localization.R.string.chapter_to_skip_to,
{}, arrayOf("%test")
)
) {}
}
}
Loading

0 comments on commit 993d7ca

Please sign in to comment.