Feature: Custom shortcuts for building command templates (WIP)

This commit is contained in:
junkfood
2023-01-13 23:29:51 +08:00
parent 845aa043db
commit 1c91c94d89
25 changed files with 916 additions and 175 deletions

View File

@ -185,7 +185,7 @@ dependencies {
implementation(libs.accompanist.webview)
implementation(libs.accompanist.pager.layouts)
implementation(libs.accompanist.pager.indicators)
implementation(libs.accompanist.flowlayout)
implementation(libs.coil.kt.compose)

View File

@ -0,0 +1,161 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "5eab3a1c93713521f1197fa2e2903231",
"entities": [
{
"tableName": "DownloadedVideoInfo",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `videoTitle` TEXT NOT NULL, `videoAuthor` TEXT NOT NULL, `videoUrl` TEXT NOT NULL, `thumbnailUrl` TEXT NOT NULL, `videoPath` TEXT NOT NULL, `extractor` TEXT NOT NULL DEFAULT 'Unknown')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "videoTitle",
"columnName": "videoTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoAuthor",
"columnName": "videoAuthor",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoUrl",
"columnName": "videoUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoPath",
"columnName": "videoPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "extractor",
"columnName": "extractor",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Unknown'"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "CommandTemplate",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `template` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "template",
"columnName": "template",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "CookieProfile",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "OptionShortcut",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `option` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "option",
"columnName": "option",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5eab3a1c93713521f1197fa2e2903231')"
]
}
}

View File

@ -5,12 +5,13 @@ import androidx.room.Database
import androidx.room.RoomDatabase
@Database(
entities = [DownloadedVideoInfo::class, CommandTemplate::class, CookieProfile::class],
version = 4,
entities = [DownloadedVideoInfo::class, CommandTemplate::class, CookieProfile::class, OptionShortcut::class],
version = 5,
autoMigrations = [
AutoMigration(from = 1, to = 2),
AutoMigration(from = 2, to = 3),
AutoMigration(from = 3, to = 4)
AutoMigration(from = 3, to = 4),
AutoMigration(from = 4, to = 5),
]
)
abstract class AppDatabase : RoomDatabase() {

View File

@ -2,9 +2,10 @@ package com.junkfood.seal.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity
@kotlinx.serialization.Serializable
@Serializable
data class CommandTemplate(
@PrimaryKey(autoGenerate = true) val id: Int,
val name: String,

View File

@ -0,0 +1,12 @@
package com.junkfood.seal.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.Serializable
@Entity
@Serializable
data class OptionShortcut(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val option: String
)

View File

@ -87,4 +87,13 @@ interface VideoInfoDao {
@Query("delete from CommandTemplate where id=:id")
suspend fun deleteTemplateById(id: Int)
@Query("select * from OptionShortcut")
fun getOptionShortcuts(): Flow<List<OptionShortcut>>
@Delete
suspend fun deleteShortcut(optionShortcut: OptionShortcut)
@Insert
suspend fun insertShortcut(optionShortcut: OptionShortcut): Long
}

View File

@ -16,6 +16,7 @@ object Route {
const val CREDITS = "credits"
const val LANGUAGES = "languages"
const val TEMPLATE = "template"
const val TEMPLATE_EDIT = "template_edit"
const val DARK_THEME = "dark_theme"
const val DOWNLOAD_QUEUE = "queue"
const val DOWNLOAD_FORMAT = "download_format"
@ -23,5 +24,7 @@ object Route {
const val COOKIE_PROFILE = "cookie_profile"
const val COOKIE_GENERATOR_WEBVIEW = "cookie_webview"
const val SUBTITLE_PREFERENCES = "subtitle_preferences"
}
}
fun String.toId(id: Int) = "$this/$id"
fun String.withArg(arg: String) = "$this/{$arg}"

View File

@ -1,25 +1,14 @@
package com.junkfood.seal.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.ClearAll
import androidx.compose.material.icons.outlined.ContentPaste
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ElevatedAssistChip
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
@ -27,111 +16,14 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.junkfood.seal.R
import com.junkfood.seal.ui.page.settings.general.ytdlpReference
@Composable
fun BackButton(onClick: () -> Unit) {
IconButton(modifier = Modifier, onClick = onClick) {
Icon(
painter = painterResource(R.drawable.outline_arrow_back_24),
contentDescription = stringResource(R.string.back),
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ButtonChip(
modifier: Modifier = Modifier,
onClick: () -> Unit,
label: String,
enabled: Boolean = true,
icon: ImageVector? = null
) {
ElevatedAssistChip(
modifier = modifier.padding(horizontal = 4.dp),
onClick = onClick,
label = { Text(label) },
colors = AssistChipDefaults.elevatedAssistChipColors(),
enabled = enabled,
leadingIcon = {
if (icon != null) Icon(
imageVector = icon, null, modifier = Modifier.size(18.dp)
)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterChipWithIcons(
modifier: Modifier = Modifier,
selected: Boolean,
onClick: () -> Unit,
label: String,
leadingIcon: ImageVector = Icons.Outlined.Check
) {
FilterChip(
modifier = modifier.padding(horizontal = 4.dp),
selected = selected,
onClick = onClick,
label = {
Text(text = label)
},
leadingIcon = {
Row {
AnimatedVisibility(visible = selected) {
Icon(
imageVector = leadingIcon,
contentDescription = null,
modifier = Modifier.requiredSize(18.dp)
)
}
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterChip(
modifier: Modifier = Modifier,
selected: Boolean,
enabled: Boolean = true,
onClick: () -> Unit,
label: String,
animated: Boolean = false
) {
FilterChip(
modifier = modifier.padding(horizontal = 4.dp),
selected = selected, enabled = enabled,
onClick = onClick,
label = {
Text(text = label)
},
trailingIcon = {
Row {
if (animated)
AnimatedVisibility(visible = selected) {
Icon(
Icons.Outlined.Check,
stringResource(R.string.checked),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp)
)
}
}
}
)
}
@Composable
fun OutlinedButtonWithIcon(
@ -163,12 +55,14 @@ fun TextButtonWithIcon(
modifier: Modifier = Modifier,
onClick: () -> Unit,
icon: ImageVector,
text: String
text: String,
contentColor: Color = MaterialTheme.colorScheme.primary
) {
TextButton(
modifier = modifier,
onClick = onClick,
contentPadding = ButtonDefaults.ButtonWithIconContentPadding
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
colors = ButtonDefaults.textButtonColors(contentColor = contentColor)
)
{
Row(verticalAlignment = Alignment.CenterVertically) {
@ -269,32 +163,4 @@ fun LinkButton(
)
}
@Composable
fun PasteButton(onPaste: (String) -> Unit = {}) {
val clipboardManager = LocalClipboardManager.current
PasteUrlButton(onClick = {
clipboardManager.getText().toString().let { onPaste(it) }
})
}
@Composable
fun PasteUrlButton(onClick: () -> Unit = {}) {
IconButton(onClick = onClick) {
Icon(
Icons.Outlined.ContentPaste,
stringResource(R.string.paste)
)
}
}
@Composable
fun ClearButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
modifier = Modifier.size(18.dp),
imageVector = Icons.Outlined.Clear,
contentDescription = stringResource(id = R.string.clear),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

View File

@ -0,0 +1,142 @@
package com.junkfood.seal.ui.component
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Check
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.AssistChip
import androidx.compose.material3.AssistChipDefaults
import androidx.compose.material3.ElevatedAssistChip
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.InputChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.junkfood.seal.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ButtonChip(
modifier: Modifier = Modifier,
onClick: () -> Unit,
label: String,
enabled: Boolean = true,
icon: ImageVector? = null
) {
ElevatedAssistChip(
modifier = modifier.padding(horizontal = 4.dp),
onClick = onClick,
label = { Text(label) },
colors = AssistChipDefaults.elevatedAssistChipColors(),
enabled = enabled,
leadingIcon = {
if (icon != null) Icon(
imageVector = icon, null, modifier = Modifier.size(AssistChipDefaults.IconSize)
)
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FilterChipWithIcons(
modifier: Modifier = Modifier,
selected: Boolean,
onClick: () -> Unit,
label: String,
leadingIcon: ImageVector = Icons.Outlined.Check
) {
FilterChip(
modifier = modifier.padding(horizontal = 4.dp),
selected = selected,
onClick = onClick,
label = {
Text(text = label)
},
leadingIcon = {
Row {
AnimatedVisibility(visible = selected) {
Icon(
imageVector = leadingIcon,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VideoFilterChip(
modifier: Modifier = Modifier,
selected: Boolean,
enabled: Boolean = true,
onClick: () -> Unit,
label: String,
animated: Boolean = false
) {
FilterChip(
modifier = modifier.padding(horizontal = 4.dp),
selected = selected, enabled = enabled,
onClick = onClick,
label = {
Text(text = label)
},
trailingIcon = {
Row {
if (animated)
AnimatedVisibility(visible = selected) {
Icon(
Icons.Outlined.Check,
stringResource(R.string.checked),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShortcutChip(
modifier: Modifier = Modifier,
text: String,
onClick: (() -> Unit)? = null,
onRemove: (() -> Unit)? = null,
) {
AssistChip(
modifier = modifier.padding(horizontal = 4.dp),
onClick = { onClick?.invoke() },
label = { Text(text = text) },
trailingIcon = {
onRemove?.let {
IconButton(
onClick = onRemove,
modifier = Modifier.size(InputChipDefaults.IconSize)
) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = stringResource(id = R.string.remove),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
})
}

View File

@ -0,0 +1,73 @@
package com.junkfood.seal.ui.component
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.ContentPaste
import androidx.compose.material.icons.outlined.HighlightOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.junkfood.seal.R
@Composable
fun PasteFromClipBoardButton(onPaste: (String) -> Unit = {}) {
val clipboardManager = LocalClipboardManager.current
PasteButton(onClick = {
clipboardManager.getText().toString().let { onPaste(it) }
})
}
@Composable
fun PasteButton(onClick: () -> Unit = {}) {
IconButton(onClick = onClick) {
Icon(
Icons.Outlined.ContentPaste,
stringResource(R.string.paste)
)
}
}
@Composable
fun AddButton(onClick: () -> Unit, enabled: Boolean = true) {
IconButton(
onClick = onClick, enabled = enabled
) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = stringResource(
R.string.add
)
)
}
}
@Composable
fun ClearButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.Outlined.Cancel,
contentDescription = stringResource(id = R.string.clear),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@Composable
fun BackButton(onClick: () -> Unit) {
IconButton(modifier = Modifier, onClick = onClick) {
Icon(
painter = painterResource(R.drawable.outline_arrow_back_24),
contentDescription = stringResource(R.string.back),
)
}
}

View File

@ -0,0 +1,109 @@
package com.junkfood.seal.ui.component
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SealTextField(
modifier: Modifier = Modifier,
trailingIcon: @Composable (() -> Unit)? = null,
minLines: Int = 1,
maxLines: Int = 1,
value: String, onValueChange: (String) -> Unit,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions(),
) {
TextField(
modifier = modifier,
value = value,
colors = TextFieldDefaults.textFieldColors(containerColor = Color.Transparent),
onValueChange = onValueChange,
trailingIcon = trailingIcon,
minLines = minLines,
maxLines = maxLines,
keyboardActions = keyboardActions,
keyboardOptions = keyboardOptions
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccessibleOutlinedTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
readOnly: Boolean = false,
textStyle: TextStyle = LocalTextStyle.current,
labelText: String,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
isError: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
minLines: Int = 1,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = TextFieldDefaults.outlinedShape,
colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
) {
OutlinedTextField(
value,
onValueChange,
modifier,
enabled,
readOnly,
textStyle,
label,
placeholder,
leadingIcon,
trailingIcon,
supportingText = {
Text(text = labelText, color = Color.Transparent)
},
isError,
visualTransformation,
keyboardOptions,
keyboardActions,
singleLine,
maxLines,
minLines,
interactionSource,
shape, colors
)
}
@Composable
fun AdjacentLabel(modifier: Modifier = Modifier, text: String) {
Text(
text = text,
modifier = modifier
.padding(bottom = 12.dp, start = 4.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}

View File

@ -28,6 +28,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.navigation
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
@ -36,6 +38,8 @@ import com.junkfood.seal.ui.common.LocalWindowWidthState
import com.junkfood.seal.ui.common.Route
import com.junkfood.seal.ui.common.animatedComposable
import com.junkfood.seal.ui.common.slideInVerticallyComposable
import com.junkfood.seal.ui.common.toId
import com.junkfood.seal.ui.common.withArg
import com.junkfood.seal.ui.page.download.DownloadPage
import com.junkfood.seal.ui.page.download.DownloadViewModel
import com.junkfood.seal.ui.page.download.FormatPage
@ -48,6 +52,7 @@ import com.junkfood.seal.ui.page.settings.about.kotlin
import com.junkfood.seal.ui.page.settings.appearance.AppearancePreferences
import com.junkfood.seal.ui.page.settings.appearance.DarkThemePreferences
import com.junkfood.seal.ui.page.settings.appearance.LanguagePage
import com.junkfood.seal.ui.page.settings.command.TemplateEditPage
import com.junkfood.seal.ui.page.settings.command.TemplateListPage
import com.junkfood.seal.ui.page.settings.directory.DownloadDirectoryPreferences
import com.junkfood.seal.ui.page.settings.format.DownloadFormatPreferences
@ -143,7 +148,7 @@ fun HomeEntry(
)
}
animatedComposable(Route.DOWNLOADS) { VideoListPage { onBackPressed() } }
animatedComposable(Route.DOWNLOAD_QUEUE) { DownloadQueuePage { onBackPressed() } }
// animatedComposable(Route.DOWNLOAD_QUEUE) { DownloadQueuePage { onBackPressed() } }
slideInVerticallyComposable(Route.PLAYLIST) { PlaylistSelectionPage { onBackPressed() } }
slideInVerticallyComposable(Route.FORMAT_SELECTION) { FormatPage(downloadViewModel) { onBackPressed() } }
settingsGraph(navController, cookiesViewModel)
@ -238,7 +243,17 @@ fun NavGraphBuilder.settingsGraph(
animatedComposable(Route.DOWNLOAD_DIRECTORY) {
DownloadDirectoryPreferences { onBackPressed() }
}
animatedComposable(Route.TEMPLATE) { TemplateListPage { onBackPressed() } }
animatedComposable(Route.TEMPLATE) {
TemplateListPage(onBackPressed = onBackPressed) {
navController.navigate(Route.TEMPLATE_EDIT.toId(it))
}
}
animatedComposable(
Route.TEMPLATE_EDIT.withArg("templateId"),
arguments = listOf(navArgument("templateId") { type = NavType.IntType })
) {
TemplateEditPage(onBackPressed, it.arguments?.getInt("templateId") ?: -1)
}
animatedComposable(Route.DARK_THEME) { DarkThemePreferences { onBackPressed() } }
animatedComposable(Route.NETWORK_PREFERENCES) {
NetworkPreferences(navigateToCookieProfilePage = {

View File

@ -82,8 +82,6 @@ import com.junkfood.seal.R
import com.junkfood.seal.ui.common.LocalWindowWidthState
import com.junkfood.seal.ui.component.ClearButton
import com.junkfood.seal.ui.component.NavigationBarSpacer
import com.junkfood.seal.ui.component.PasteButton
import com.junkfood.seal.ui.component.PasteUrlButton
import com.junkfood.seal.ui.component.VideoCard
import com.junkfood.seal.ui.theme.PreviewThemeLight
import com.junkfood.seal.util.CONFIGURE

View File

@ -46,7 +46,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.android.material.badge.BadgeUtils
import com.junkfood.seal.R
import com.junkfood.seal.database.CommandTemplate
import com.junkfood.seal.ui.common.booleanState
@ -56,7 +55,7 @@ import com.junkfood.seal.ui.component.ButtonChip
import com.junkfood.seal.ui.component.DismissButton
import com.junkfood.seal.ui.component.DrawerSheetSubtitle
import com.junkfood.seal.ui.component.FilledButtonWithIcon
import com.junkfood.seal.ui.component.FilterChip
import com.junkfood.seal.ui.component.VideoFilterChip
import com.junkfood.seal.ui.component.FilterChipWithIcons
import com.junkfood.seal.ui.component.OutlinedButtonWithIcon
import com.junkfood.seal.ui.page.settings.format.AudioFormatDialog
@ -144,7 +143,7 @@ fun DownloadSettingDialog(
Row(
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
FilterChip(
VideoFilterChip(
selected = audio,
enabled = !customCommand,
onClick = {
@ -154,7 +153,7 @@ fun DownloadSettingDialog(
label = stringResource(R.string.extract_audio)
)
if (!isShareActivity) {
FilterChip(
VideoFilterChip(
selected = playlist,
enabled = !customCommand,
onClick = {
@ -163,7 +162,7 @@ fun DownloadSettingDialog(
},
label = stringResource(R.string.download_playlist)
)
FilterChip(
VideoFilterChip(
selected = formatSelection,
enabled = !customCommand && !playlist,
onClick = {
@ -173,7 +172,7 @@ fun DownloadSettingDialog(
label = stringResource(R.string.format_selection)
)
}
FilterChip(
VideoFilterChip(
selected = subtitle,
enabled = !customCommand && !audio,
onClick = {
@ -182,7 +181,7 @@ fun DownloadSettingDialog(
},
label = stringResource(id = R.string.download_subtitles)
)
FilterChip(
VideoFilterChip(
selected = thumbnail,
enabled = !customCommand,
onClick = {
@ -194,7 +193,7 @@ fun DownloadSettingDialog(
}
DrawerSheetSubtitle(text = stringResource(id = R.string.advanced_settings))
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
FilterChip(
VideoFilterChip(
selected = customCommand,
onClick = {
customCommand = !customCommand

View File

@ -76,7 +76,6 @@ fun PlaylistSelectionPage(onBackPressed: () -> Unit = {}) {
},
navigationIcon = {
IconButton(
// modifier = Modifier.padding(start = 8.dp),
onClick = { onDismissRequest() }) {
Icon(Icons.Outlined.Close, stringResource(R.string.close))
}

View File

@ -1,23 +1,28 @@
package com.junkfood.seal.ui.page.settings.command
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.ContentPaste
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material.icons.outlined.EditNote
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -27,17 +32,24 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
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.DialogProperties
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import com.google.accompanist.flowlayout.FlowRow
import com.junkfood.seal.R
import com.junkfood.seal.database.CommandTemplate
import com.junkfood.seal.ui.component.ButtonChip
import com.junkfood.seal.database.OptionShortcut
import com.junkfood.seal.ui.component.AddButton
import com.junkfood.seal.ui.component.ClearButton
import com.junkfood.seal.ui.component.ConfirmButton
import com.junkfood.seal.ui.component.LinkButton
import com.junkfood.seal.ui.component.PasteButton
import com.junkfood.seal.ui.component.PasteFromClipBoardButton
import com.junkfood.seal.ui.component.ShortcutChip
import com.junkfood.seal.ui.component.SealTextField
import com.junkfood.seal.util.DatabaseUtil
import com.kyant.monet.a3
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@ -125,7 +137,7 @@ fun CommandTemplateDialog(
onValueChange = { templateText = it },
trailingIcon = {
if (templateText.isEmpty())
PasteButton { templateText = it }
PasteFromClipBoardButton { templateText = it }
else ClearButton { templateText = "" }
},
label = { Text(stringResource(R.string.custom_command_template)) },
@ -135,4 +147,59 @@ fun CommandTemplateDialog(
LinkButton()
}
})
}
@Composable
fun OptionChipsDialog(onDismissRequest: () -> Unit) {
val scope = rememberCoroutineScope()
val shortcuts by DatabaseUtil.getShortcuts().collectAsState(emptyList())
var text by remember { mutableStateOf("") }
val addShortCuts = {
scope.launch {
if (shortcuts.find { it.option == text } == null)
DatabaseUtil.insertShortcut(OptionShortcut(option = text))
text = ""
}
}
AlertDialog(
onDismissRequest = onDismissRequest,
title = { Text(text = stringResource(id = R.string.edit_option_chips)) },
icon = { Icon(Icons.Outlined.Edit, null) }, text = {
Column {
Column(
modifier = Modifier
.requiredHeight(400.dp)
.horizontalScroll(rememberScrollState())
.verticalScroll(rememberScrollState())
) {
FlowRow(modifier = Modifier.width(400.dp)) {
shortcuts.forEach { item ->
ShortcutChip(
text = item.option,
onRemove = {
scope.launch {
DatabaseUtil.deleteShortcut(item)
}
})
}
}
}
SealTextField(
modifier = Modifier.padding(top = 12.dp),
value = text,
onValueChange = { text = it },
trailingIcon = {
AddButton(onClick = { addShortCuts() }, enabled = text.isNotEmpty())
},
keyboardActions = KeyboardActions(onDone = { addShortCuts() }),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
maxLines = 2,
)
}
}, confirmButton = {
ConfirmButton { onDismissRequest() }
})
}

View File

@ -0,0 +1,229 @@
package com.junkfood.seal.ui.page.settings.command
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.Divider
import androidx.compose.material3.DividerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
import com.google.accompanist.flowlayout.MainAxisAlignment
import com.google.accompanist.flowlayout.SizeMode
import com.junkfood.seal.R
import com.junkfood.seal.database.CommandTemplate
import com.junkfood.seal.database.OptionShortcut
import com.junkfood.seal.ui.component.AccessibleOutlinedTextField
import com.junkfood.seal.ui.component.ClearButton
import com.junkfood.seal.ui.component.AdjacentLabel
import com.junkfood.seal.ui.component.PasteFromClipBoardButton
import com.junkfood.seal.ui.component.ShortcutChip
import com.junkfood.seal.ui.component.TextButtonWithIcon
import com.junkfood.seal.util.DatabaseUtil
import com.junkfood.seal.util.PreferenceUtil
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TemplateEditPage(onDismissRequest: () -> Unit, templateId: Int) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val commandTemplate =
if (templateId > 0) PreferenceUtil.templateStateFlow.collectAsState().value[templateId] else
CommandTemplate(0, "", "")
var templateText by remember { mutableStateOf(commandTemplate.template) }
var templateName by remember { mutableStateOf(commandTemplate.name) }
var isEditingShortcuts by remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
TopAppBar(title = {
Text(
text = stringResource(R.string.new_template),
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp)
)
},
navigationIcon = {
IconButton(
onClick = { onDismissRequest() }) {
Icon(Icons.Outlined.Close, stringResource(R.string.close))
}
}, actions = {
TextButton(
modifier = Modifier.padding(end = 8.dp),
onClick = {
onDismissRequest()
}
) {
Text(text = stringResource(androidx.appcompat.R.string.abc_action_mode_done))
}
}, scrollBehavior = scrollBehavior
)
}) { paddings ->
LazyColumn(
modifier = Modifier.padding(paddings),
contentPadding = PaddingValues()
) {
item {
Column(androidx.compose.ui.Modifier.padding(horizontal = 24.dp)) {
AdjacentLabel(
text = stringResource(R.string.template_label),
modifier = Modifier
.padding(top = 12.dp)
.clearAndSetSemantics { }
)
AccessibleOutlinedTextField(
labelText = stringResource(R.string.template_label),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
value = templateName,
onValueChange = { templateName = it },
)
}
}
item {
Column(Modifier.padding(horizontal = 24.dp)) {
AdjacentLabel(text = stringResource(R.string.custom_command_template))
OutlinedTextField(
supportingText = { Text(text = stringResource(id = R.string.edit_template_desc)) },
modifier = Modifier.fillMaxWidth(),
value = templateText,
onValueChange = { templateText = it },
trailingIcon = {
if (templateText.isEmpty())
PasteFromClipBoardButton { templateText = it }
else ClearButton { templateText = "" }
},
maxLines = 6,
minLines = 6
)
Divider(
Modifier
.fillMaxWidth()
.padding(top = 48.dp, bottom = 24.dp)
.size(DividerDefaults.Thickness)
.clip(CircleShape),
color = MaterialTheme.colorScheme.outlineVariant,
)
}
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 16.dp)
) {
Text(
text = stringResource(R.string.shortcuts),
modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.tertiary
)
TextButtonWithIcon(
modifier = Modifier,
onClick = { isEditingShortcuts = true },
icon = Icons.Outlined.Edit,
text = stringResource(id = R.string.edit),
contentColor = MaterialTheme.colorScheme.tertiary
)
}
}
item {
val scope = rememberCoroutineScope()
val shortcuts by DatabaseUtil.getShortcuts().collectAsState(emptyList())
var text by remember { mutableStateOf("") }
val addShortCuts = {
scope.launch {
if (shortcuts.find { it.option == text } == null)
DatabaseUtil.insertShortcut(OptionShortcut(option = text))
text = ""
}
}
Column(
modifier = Modifier
.fillParentMaxWidth()
.horizontalScroll(rememberScrollState())
) {
FlowRow(
modifier = Modifier
.padding(horizontal = 8.dp)
.width(500.dp),
mainAxisSize = SizeMode.Expand,
crossAxisSpacing = 2.dp,
) {
shortcuts.forEach { item ->
ShortcutChip(
text = item.option,
onClick = {
templateText.run {
if (isEmpty()) "$this${item.option}"
else this.removeSuffix(" ") + " ${item.option}"
}
}
)
}
}
}
}
}
}
if (isEditingShortcuts)
OptionChipsDialog { isEditingShortcuts = false }
}

View File

@ -9,6 +9,8 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.AssignmentReturn
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkAdd
import androidx.compose.material.icons.outlined.ContentPasteGo
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.MoreVert
@ -63,7 +65,7 @@ private const val TAG = "TemplateListPage"
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLifecycleComposeApi::class)
@Composable
fun TemplateListPage(onBackPressed: () -> Unit) {
fun TemplateListPage(onBackPressed: () -> Unit, onNavigateToEditPage: (Int) -> Unit) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
rememberTopAppBarState(),
canScroll = { true })
@ -74,7 +76,7 @@ fun TemplateListPage(onBackPressed: () -> Unit) {
val context = LocalContext.current
var showEditDialog by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) }
var showShortcutsDialog by remember { mutableStateOf(false) }
var isCustomCommandEnabled by remember {
mutableStateOf(PreferenceUtil.getValue(CUSTOM_COMMAND))
}
@ -200,8 +202,45 @@ fun TemplateListPage(onBackPressed: () -> Unit) {
title = stringResource(id = R.string.new_template),
icon = Icons.Outlined.Add
) {
editingTemplateId = -1
showEditDialog = true
onNavigateToEditPage(-1)
// editingTemplateId = -1
// showEditDialog = true
}
}
item {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize(Alignment.TopEnd)
) {
DropdownMenu(
modifier = Modifier,
expanded = expanded,
onDismissRequest = { expanded = false }) {
DropdownMenuItem(
leadingIcon = { Icon(Icons.Outlined.ContentPasteGo, null) },
text = {
Text(stringResource(R.string.export_to_clipboard))
},
onClick = {})
DropdownMenuItem(
leadingIcon = { Icon(Icons.Outlined.AssignmentReturn, null) },
text = {
Text(stringResource(R.string.import_from_clipboard))
},
onClick = {})
}
PreferenceItemVariant(
title = stringResource(id = R.string.edit_option_chips),
icon = Icons.Outlined.BookmarkAdd,
onLongClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
expanded = true
}, onLongClickLabel = stringResource(id = R.string.show_more_actions)
) {
showShortcutsDialog = true
}
}
}
}
@ -236,6 +275,11 @@ fun TemplateListPage(onBackPressed: () -> Unit) {
}
})
}
if (showShortcutsDialog) {
OptionChipsDialog {
showShortcutsDialog = false
}
}
LaunchedEffect(templates.size) {
if (templates.isNotEmpty() && templates.find { it.id == selectedTemplateId } == null) {
selectedTemplateId = templates.first().id

View File

@ -49,7 +49,7 @@ import com.junkfood.seal.ui.component.ConfirmButton
import com.junkfood.seal.ui.component.DismissButton
import com.junkfood.seal.ui.component.HelpDialog
import com.junkfood.seal.ui.component.LargeTopAppBar
import com.junkfood.seal.ui.component.PasteButton
import com.junkfood.seal.ui.component.PasteFromClipBoardButton
import com.junkfood.seal.ui.component.PreferenceItemVariant
import com.junkfood.seal.ui.component.PreferenceSwitchWithContainer
import com.junkfood.seal.ui.component.TextButtonWithIcon
@ -187,7 +187,7 @@ fun CookieGeneratorDialog(
.padding(top = 16.dp),
value = url, label = { Text("URL") },
onValueChange = { cookiesViewModel.updateUrl(it) }, trailingIcon = {
PasteButton { cookiesViewModel.updateUrl(TextUtil.matchUrlFromClipboard(it)) }
PasteFromClipBoardButton { cookiesViewModel.updateUrl(TextUtil.matchUrlFromClipboard(it)) }
}, maxLines = 1
)

View File

@ -62,7 +62,7 @@ import com.junkfood.seal.ui.common.LocalWindowWidthState
import com.junkfood.seal.ui.component.BackButton
import com.junkfood.seal.ui.component.ConfirmButton
import com.junkfood.seal.ui.component.DismissButton
import com.junkfood.seal.ui.component.FilterChip
import com.junkfood.seal.ui.component.VideoFilterChip
import com.junkfood.seal.ui.component.LargeTopAppBar
import com.junkfood.seal.ui.component.MediaListItem
import com.junkfood.seal.ui.component.MultiChoiceItem
@ -134,13 +134,13 @@ fun VideoListPage(
.padding(8.dp)
.selectableGroup()
) {
FilterChip(
VideoFilterChip(
selected = viewState.audioFilter,
onClick = { videoListViewModel.clickAudioFilter() },
label = stringResource(id = R.string.audio),
)
FilterChip(
VideoFilterChip(
selected = viewState.videoFilter,
onClick = { videoListViewModel.clickVideoFilter() },
label = stringResource(id = R.string.video),
@ -156,7 +156,7 @@ fun VideoListPage(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
)
for (i in 0 until filterSet.size) {
FilterChip(
VideoFilterChip(
selected = viewState.activeFilterIndex == i,
onClick = { videoListViewModel.clickExtractorFilter(i) },
label = filterSet.elementAt(i)

View File

@ -7,6 +7,7 @@ import com.junkfood.seal.database.AppDatabase
import com.junkfood.seal.database.CommandTemplate
import com.junkfood.seal.database.CookieProfile
import com.junkfood.seal.database.DownloadedVideoInfo
import com.junkfood.seal.database.OptionShortcut
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
@ -33,6 +34,11 @@ object DatabaseUtil {
fun getCookiesFlow() = dao.getCookieProfileFlow()
fun getShortcuts() = dao.getOptionShortcuts()
suspend fun deleteShortcut(shortcut: OptionShortcut) = dao.deleteShortcut(shortcut)
suspend fun insertShortcut(shortcut: OptionShortcut) = dao.insertShortcut(shortcut)
suspend fun getCookieById(id: Int) = dao.getCookieById(id)
suspend fun deleteCookieProfile(profile: CookieProfile) = dao.deleteCookieProfile(profile)

View File

@ -81,6 +81,8 @@ const val SYSTEM_DEFAULT = 0
const val TEMPLATE_EXAMPLE =
"""--no-mtime -f "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4] / bv*+ba/b""""
const val TEMPLATE_SHORTCUTS = "template_shortcuts"
val palettesMap = mapOf(
0 to PaletteStyle.TonalSpot,
1 to PaletteStyle.Spritz,
@ -150,6 +152,7 @@ object PreferenceUtil {
}
}
fun getVideoResolution(): Int = VIDEO_QUALITY.getInt()
fun getVideoResolutionDesc(videoQualityCode: Int = getVideoResolution()): String {

View File

@ -50,7 +50,7 @@
<string name="custom_command">自定义命令</string>
<string name="custom_command_desc">使用自定义的命令模板运行 yt-dlp</string>
<string name="custom_command_template">命令模板</string>
<string name="edit">编辑模板</string>
<string name="edit">编辑</string>
<string name="start_execute">开始执行命令</string>
<string name="advanced_settings">高级</string>
<string name="print_details">显示详细信息</string>

View File

@ -285,4 +285,8 @@
<string name="subtitle_desc">Languages, embed subtitles, auto captions</string>
<string name="copy_log">Copy log</string>
<string name="clear">Clear</string>
<string name="edit_option_chips">Edit shortcuts</string>
<string name="add">Add</string>
<string name="command_shortcut">Commands shortcut</string>
<string name="shortcuts">Shortcuts</string>
</resources>

View File

@ -53,7 +53,7 @@ accompanist-systemuicontroller = { group = "com.google.accompanist", name = "acc
accompanist-webview = { group = "com.google.accompanist", name = "accompanist-webview", version.ref = "accompanist" }
accompanist-pager-layouts = { group = "com.google.accompanist", name = "accompanist-pager", version.ref = "accompanist" }
accompanist-pager-indicators = { group = "com.google.accompanist", name = "accompanist-pager-indicators", version.ref = "accompanist" }
accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }