From d8dac065dc7b834c56dda45b7980abeabe56761b Mon Sep 17 00:00:00 2001 From: Terry Cai Date: Thu, 16 May 2024 18:27:57 +0800 Subject: [PATCH 01/31] feat: change ui --- app/build.gradle.kts | 6 +- .../1.json | 16 +- app/src/main/AndroidManifest.xml | 117 +++--- .../voicenotify/AlphabetIndexHelper.kt | 50 +++ .../main/java/com/pilot51/voicenotify/App.kt | 20 + .../com/pilot51/voicenotify/AppListScreen.kt | 260 +++++++++---- .../pilot51/voicenotify/AppListViewModel.kt | 19 +- .../java/com/pilot51/voicenotify/Common.kt | 29 ++ .../com/pilot51/voicenotify/MainScreen.kt | 97 +++-- .../com/pilot51/voicenotify/PreferenceRows.kt | 15 +- .../java/com/pilot51/voicenotify/SearchBar.kt | 90 +++++ .../com/pilot51/voicenotify/SwitchCustom.kt | 68 ++++ .../java/com/pilot51/voicenotify/TextField.kt | 240 ++++++++++++ .../java/com/pilot51/voicenotify/TopAppBar.kt | 67 ++++ .../com/pilot51/voicenotify/ui/theme/Color.kt | 11 + .../com/pilot51/voicenotify/ui/theme/Theme.kt | 76 ++++ .../com/pilot51/voicenotify/ui/theme/Type.kt | 34 ++ app/src/main/res/values/strings.xml | 341 +++++++++--------- app/src/main/res/values/themes.xml | 5 + build.gradle.kts | 3 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 21 files changed, 1205 insertions(+), 361 deletions(-) create mode 100644 app/src/main/java/com/pilot51/voicenotify/AlphabetIndexHelper.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/SearchBar.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/SwitchCustom.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/TextField.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/TopAppBar.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/ui/theme/Color.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/pilot51/voicenotify/ui/theme/Type.kt create mode 100644 app/src/main/res/values/themes.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8f43a08..5f6dd35 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,7 +45,7 @@ android { compileSdk = 34 defaultConfig { applicationId = "com.pilot51.voicenotify" - minSdk = 21 + minSdk = 24 targetSdk = 34 versionName = "1.3.1" versionCode = 29 @@ -105,7 +105,7 @@ android { generateLocaleConfig = true } - applicationVariants.all { + applicationVariants.all { outputs.all { this as BaseVariantOutputImpl outputFileName = "VoiceNotify_v${defaultConfig.versionName}-${name}_$gitCommitHash.apk" @@ -119,7 +119,7 @@ dependencies { implementation("androidx.compose.material3:material3:1.1.2") implementation("androidx.compose.material:material-icons-extended-android:1.5.4") implementation("androidx.compose.ui:ui-tooling-preview:1.5.4") - debugImplementation("androidx.compose.ui:ui-tooling:1.5.4") + debugImplementation("androidx.compose.ui:ui-tooling:1.5.4") implementation("androidx.navigation:navigation-compose:2.7.6") implementation("androidx.glance:glance-appwidget:1.0.0") implementation("androidx.preference:preference-ktx:1.2.1") diff --git a/app/schemas/com.pilot51.voicenotify.AppDatabase/1.json b/app/schemas/com.pilot51.voicenotify.AppDatabase/1.json index 5567fea..011a379 100644 --- a/app/schemas/com.pilot51.voicenotify.AppDatabase/1.json +++ b/app/schemas/com.pilot51.voicenotify.AppDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "1d73ff7d95b81686c7f6de6408254734", + "identityHash": "10896f7f2edd7a5e91670d81d13c009c", "entities": [ { "tableName": "apps", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `package` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `is_enabled` INTEGER)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `package` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `is_enabled` INTEGER, `sortLetter` TEXT NOT NULL)", "fields": [ { "fieldPath": "id", @@ -31,13 +31,19 @@ "columnName": "is_enabled", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "sortLetter", + "columnName": "sortLetter", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { + "autoGenerate": true, "columnNames": [ "_id" - ], - "autoGenerate": true + ] }, "indices": [], "foreignKeys": [] @@ -46,7 +52,7 @@ "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, '1d73ff7d95b81686c7f6de6408254734')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '10896f7f2edd7a5e91670d81d13c009c')" ] } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ce9ecc8..1799577 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,65 +1,60 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/AlphabetIndexHelper.kt b/app/src/main/java/com/pilot51/voicenotify/AlphabetIndexHelper.kt new file mode 100644 index 0000000..2d8074b --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/AlphabetIndexHelper.kt @@ -0,0 +1,50 @@ +package com.pilot51.voicenotify + +import android.icu.text.AlphabeticIndex +import android.os.Build +import android.os.LocaleList +import androidx.annotation.RequiresApi +import java.util.Locale + +/** + * Help to get a alphabetic of a char. + * + * Use: + * ``` + * val sectionName = AlphabeticIndexHelper.computeSectionName(Locale.CHINESE, "神") + * log: sectionName = "S" + * ``` + */ +object AlphabeticIndexHelper { + @JvmStatic + fun computeSectionName(c: CharSequence): String { + return computeSectionName(Locale.getDefault(), c) + } + + @JvmStatic + fun computeCNSectionName(c: CharSequence): String = computeSectionName( + LocaleList(Locale.CHINESE, Locale.SIMPLIFIED_CHINESE), c + ) + + @JvmStatic + fun computeSectionName(locale: Locale, c: CharSequence): String { + return AlphabeticIndex(locale).buildImmutableIndex().let { + it.getBucket(it.getBucketIndex(c)).label + } + } + + @JvmStatic + fun computeSectionName(localeList: LocaleList, c: CharSequence): String { + val primaryLocale = if (localeList.isEmpty) Locale.ENGLISH else localeList[0] + val ai = AlphabeticIndex(primaryLocale) + for (index in 1 until localeList.size()) { + ai.addLabels(localeList[index]) + } + return ai.buildImmutableIndex().let { + it.getBucket(it.getBucketIndex(c)).label + } + } + + @JvmStatic + fun isStartsWithDigit(c: CharSequence): Boolean = Character.isDigit(c[0]) +} diff --git a/app/src/main/java/com/pilot51/voicenotify/App.kt b/app/src/main/java/com/pilot51/voicenotify/App.kt index ba3743f..5dbb891 100644 --- a/app/src/main/java/com/pilot51/voicenotify/App.kt +++ b/app/src/main/java/com/pilot51/voicenotify/App.kt @@ -16,11 +16,15 @@ package com.pilot51.voicenotify import android.content.Context +import android.os.Build import android.provider.BaseColumns import android.widget.Toast +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.ImageBitmap import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore +import androidx.room.MapColumn import androidx.room.PrimaryKey import com.pilot51.voicenotify.AppDatabase.Companion.db import kotlinx.coroutines.CoroutineScope @@ -44,6 +48,22 @@ data class App( get() = isEnabled!! set(value) { isEnabled = value } + @get:Ignore + var iconImage: ImageBitmap + get() = null!! + set(value) {} + + var sortLetter: String = "" + get() { + var rlabel = AlphabeticIndexHelper.computeSectionName(label) + if (rlabel.matches("[A-Z]".toRegex())) { + return rlabel + } else { + return "#" + } + } + + /** * Updates self in database. * @return This instance. diff --git a/app/src/main/java/com/pilot51/voicenotify/AppListScreen.kt b/app/src/main/java/com/pilot51/voicenotify/AppListScreen.kt index 4da4ecc..c59e6b4 100644 --- a/app/src/main/java/com/pilot51/voicenotify/AppListScreen.kt +++ b/app/src/main/java/com/pilot51/voicenotify/AppListScreen.kt @@ -15,11 +15,22 @@ */ package com.pilot51.voicenotify +import android.content.Context +import android.content.pm.PackageManager import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.toggleable @@ -34,12 +45,24 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.res.painterResource +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel @@ -48,101 +71,172 @@ import kotlinx.coroutines.delay private lateinit var vmStoreOwner: ViewModelStoreOwner + @OptIn(ExperimentalComposeUiApi::class) @Composable -fun AppListActions() { +fun AppListActions(modifier: Modifier = Modifier) { val vm: AppListViewModel = viewModel(vmStoreOwner) - var showSearchBar by remember { mutableStateOf(false) } - if (showSearchBar) { - val focusRequester = remember { FocusRequester() } - val keyboard = LocalSoftwareKeyboardController.current - TextField( - value = vm.searchQuery ?: "", - onValueChange = { - vm.searchQuery = it - vm.filterApps(it) - }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp) - .focusRequester(focusRequester), - maxLines = 1, - singleLine = true, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = null - ) - }, - trailingIcon = { - IconButton(onClick = { - showSearchBar = false - vm.searchQuery = null - vm.filterApps(null) - }) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = stringResource(R.string.close) - ) - } - } - ) - LaunchedEffect(focusRequester) { - focusRequester.requestFocus() - delay(100) - keyboard?.show() - } - } else { - IconButton(onClick = { - showSearchBar = true - }) { - Icon( - imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.filter) - ) - } - IconButton(onClick = { - vm.massIgnore(IgnoreType.IGNORE_ALL) - }) { - Icon( - imageVector = Icons.Filled.CheckBoxOutlineBlank, - contentDescription = stringResource(R.string.ignore_all) - ) - } - IconButton(onClick = { - vm.massIgnore(IgnoreType.IGNORE_NONE) - }) { - Icon( - imageVector = Icons.Filled.CheckBox, - contentDescription = stringResource(R.string.ignore_none) - ) - } - } + // var showSearchBar by remember { mutableStateOf(false) } + // val focusRequester = remember { FocusRequester() } + // val keyboard = LocalSoftwareKeyboardController.current +// Row( +// modifier = modifier +// ) { +// TextField( +// value = vm.searchQuery ?: "", +// onValueChange = { +// vm.searchQuery = it +// vm.filterApps(it) +// }, +// modifier = Modifier +// .fillMaxWidth() +// .padding(horizontal = 4.dp) +// .focusRequester(focusRequester), +// maxLines = 1, +// singleLine = true, +// leadingIcon = { +// Icon( +// imageVector = Icons.Filled.Search, +// contentDescription = null +// ) +// }, +// trailingIcon = { +// IconButton(onClick = { +// showSearchBar = false +// vm.searchQuery = null +// vm.filterApps(null) +// }) { +// Icon( +// imageVector = Icons.Filled.Close, +// contentDescription = stringResource(R.string.close) +// ) +// } +// } +// ) + +// } + +// LaunchedEffect(focusRequester) { +// focusRequester.requestFocus() +// delay(100) +// keyboard?.show() +// } + SealSearchBar( + modifier = modifier, + text = vm.searchQuery ?: "", + onValueChange = { + vm.searchQuery = it + vm.filterApps(it) + }, + placeholderText = "Search" + ) + } @Composable fun AppListScreen() { vmStoreOwner = LocalViewModelStoreOwner.current!! val vm: AppListViewModel = viewModel(vmStoreOwner) - AppList(vm.filteredApps, vm.showList) { app -> - vm.setIgnore(app, IgnoreType.IGNORE_TOGGLE) + Column( + modifier = Modifier.fillMaxSize() + ) { + + AppListActions( + modifier = Modifier.fillMaxWidth() + .padding(8.dp, 4.dp) + + ) + Box( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) { + AppList( + filteredApps = vm.filteredApps, + showList = vm.showList, + stickyHeader = { + val modifier = Modifier + .fillMaxWidth() + .background( + color = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.medium + ). + padding(8.dp, 4.dp) + Row( + modifier = modifier, + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.ignore_all), + modifier = Modifier.padding(8.dp) + ) + Switch( + checked = vm.appEnable, + onCheckedChange = { + vm.massIgnore(if (it) IgnoreType.IGNORE_NONE else IgnoreType.IGNORE_ALL) + }, + modifier = Modifier.padding(8.dp) + ) + } + } + ) { app -> + vm.setIgnore(app, IgnoreType.IGNORE_TOGGLE) + } + } } + + } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun AppList( filteredApps: List, showList: Boolean, - toggleIgnore: (app: App) -> Unit + stickyHeader: @Composable () -> Unit = {}, + toggleIgnore: (app: App) -> Unit, ) { if (!showList) return - LazyColumn(modifier = Modifier.fillMaxSize()) { - items(filteredApps) { - AppListItem(it, toggleIgnore) + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.Top + ) { + LazyColumn( + + ) { + stickyHeader { + stickyHeader() + } + items(filteredApps) { + AppListItem(it, toggleIgnore) + } } } } +@Composable +fun PackageImage(context: Context, packageName: String, modifier: Modifier = Modifier) { + val packageManager: PackageManager = context.packageManager + val result = runCatching { + val appInfo = packageManager.getApplicationInfo(packageName, 0) + val icon = appInfo.loadIcon(packageManager).toBitmap().asImageBitmap() + Image( + painter = BitmapPainter(icon), + contentDescription = null, + modifier = modifier + ) + } + result.onFailure { + // If there's an exception, use the default image resource + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = modifier + ) + } +} + @Composable private fun AppListItem(app: App, toggleIgnore: (app: App) -> Unit) { ListItem( @@ -151,21 +245,33 @@ private fun AppListItem(app: App, toggleIgnore: (app: App) -> Unit) { role = Role.Checkbox, onValueChange = { toggleIgnore(app) } ), + leadingContent = { + PackageImage( + context = LocalContext.current, + packageName = app.packageName, + modifier = Modifier.size(48.dp) + ) + }, headlineContent = { Text( text = app.label, - fontSize = 24.sp + fontSize = 18.sp ) }, supportingContent = { Text(app.packageName) }, trailingContent = { - Checkbox( + Switch( checked = app.enabled, - modifier = Modifier.focusable(false), - onCheckedChange = { toggleIgnore(app) } + onCheckedChange = { toggleIgnore(app) }, + modifier = Modifier.focusable(false) ) +// Checkbox( +// checked = app.enabled, +// modifier = Modifier.focusable(false), +// onCheckedChange = { toggleIgnore(app) } +// ) } ) } diff --git a/app/src/main/java/com/pilot51/voicenotify/AppListViewModel.kt b/app/src/main/java/com/pilot51/voicenotify/AppListViewModel.kt index 6ff4f4f..5736ac6 100644 --- a/app/src/main/java/com/pilot51/voicenotify/AppListViewModel.kt +++ b/app/src/main/java/com/pilot51/voicenotify/AppListViewModel.kt @@ -16,13 +16,17 @@ package com.pilot51.voicenotify import android.app.Application +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Build import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.annotation.RequiresApi import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.AndroidViewModel import com.pilot51.voicenotify.AppListViewModel.IgnoreType.* import com.pilot51.voicenotify.PreferenceHelper.KEY_APP_DEFAULT_ENABLE @@ -32,6 +36,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.sync.withLock + class AppListViewModel(application: Application) : AndroidViewModel(application) { private val appContext = application.applicationContext private val apps by Common::apps @@ -40,6 +45,7 @@ class AppListViewModel(application: Application) : AndroidViewModel(application) private val syncAppsMutex by Common::syncAppsMutex var searchQuery by mutableStateOf(null) var showList by mutableStateOf(false) + var appEnable by mutableStateOf(false) init { updateAppsList() @@ -72,11 +78,13 @@ class AppListViewModel(application: Application) : AndroidViewModel(application) } // Add new - val installedApps = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - packMan.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L)) - } else { - packMan.getInstalledApplications(0) - } + // val installedApps = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // packMan.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L)) + // } else { + // packMan.getInstalledApplications(0) + // } + var installedApps = Common.getAppsInfo(appContext) + inst@ for (appInfo in installedApps) { for (app in apps) { if (app.packageName == appInfo.packageName) { @@ -120,6 +128,7 @@ class AppListViewModel(application: Application) : AndroidViewModel(application) fun massIgnore(ignoreType: IgnoreType) { if (ignoreType == IGNORE_ALL) appDefaultEnable = false else if (ignoreType == IGNORE_NONE) appDefaultEnable = true + appEnable = appDefaultEnable CoroutineScope(Dispatchers.IO).launch { syncAppsMutex.withLock { if (apps.isEmpty()) return@launch diff --git a/app/src/main/java/com/pilot51/voicenotify/Common.kt b/app/src/main/java/com/pilot51/voicenotify/Common.kt index 5b7a800..9c66ebe 100644 --- a/app/src/main/java/com/pilot51/voicenotify/Common.kt +++ b/app/src/main/java/com/pilot51/voicenotify/Common.kt @@ -16,20 +16,25 @@ package com.pilot51.voicenotify import android.app.Activity +import android.content.Context import android.content.Intent +import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Build import android.provider.Settings import android.util.Pair +import androidx.activity.ComponentActivity import androidx.compose.runtime.mutableStateListOf import com.pilot51.voicenotify.AppListViewModel.Companion.appDefaultEnable import com.pilot51.voicenotify.PreferenceHelper.getSelectedAudioStream import com.pilot51.voicenotify.VNApplication.Companion.appContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock + object Common { /** Sets the volume control stream defined in preferences. */ fun setVolumeStream(activity: Activity) { @@ -116,4 +121,28 @@ object Common { } return list } + + fun isSystemApp(applicationInfo: ApplicationInfo): Boolean { + return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0 + } + + /** + * get application info use queryIntentActivities method + * @param activity + * @return + */ + fun getAppsInfo(context: Context): List { + val pm = context.packageManager + val intent = Intent(Intent.ACTION_MAIN, null) + intent.addCategory(Intent.CATEGORY_LAUNCHER) + val resolveInfos = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA) + val apps: MutableList = ArrayList(0) + for (resolveInfo in resolveInfos) { + // filer system apps + if (isSystemApp(resolveInfo.activityInfo.applicationInfo)) continue + apps.add(resolveInfo.activityInfo.applicationInfo) + } + return apps + } + } diff --git a/app/src/main/java/com/pilot51/voicenotify/MainScreen.kt b/app/src/main/java/com/pilot51/voicenotify/MainScreen.kt index 3a3c9ec..13122ed 100644 --- a/app/src/main/java/com/pilot51/voicenotify/MainScreen.kt +++ b/app/src/main/java/com/pilot51/voicenotify/MainScreen.kt @@ -25,9 +25,12 @@ import android.content.res.Configuration import android.os.Build import android.widget.Toast import androidx.annotation.StringRes +import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -37,6 +40,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalLifecycleOwner @@ -45,6 +49,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -69,6 +74,7 @@ import com.pilot51.voicenotify.PreferenceHelper.KEY_QUIET_END import com.pilot51.voicenotify.PreferenceHelper.KEY_QUIET_START import kotlinx.coroutines.flow.MutableStateFlow import java.util.* +import com.pilot51.voicenotify.ui.theme.VoicenotifyTheme private enum class Screen(@StringRes val title: Int) { MAIN(R.string.app_name), @@ -78,26 +84,14 @@ private enum class Screen(@StringRes val title: Int) { @Composable fun AppTheme(content: @Composable () -> Unit) { - MaterialTheme( - colorScheme = if (isSystemInDarkTheme()) { - darkColorScheme(primary = Color(0xFF1CB7D5), primaryContainer = Color(0xFF1E4696)) - } else { - lightColorScheme(primary = Color(0xFF2A54A5), primaryContainer = Color(0xFF64F0FF)) - }, - typography = MaterialTheme.typography.copy( - // Increased font size for dialog buttons - labelLarge = TextStyle( - fontFamily = FontFamily.SansSerif, - fontWeight = FontWeight.Medium, - fontSize = 20.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ) - ), + VoicenotifyTheme( + darkTheme = isSystemInDarkTheme(), + dynamicColor = true, content = content ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun AppMain() { val navController = rememberNavController() @@ -105,12 +99,20 @@ fun AppMain() { val currentScreen = Screen.valueOf( backStackEntry?.destination?.route ?: Screen.MAIN.name ) + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + canScroll = { true }) Scaffold( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { AppBar( currentScreen = currentScreen, canNavigateBack = navController.previousBackStackEntry != null, - navigateUp = { navController.navigateUp() } + navigateUp = { navController.navigateUp() }, + scrollBehavior = scrollBehavior ) } ) { innerPadding -> @@ -118,6 +120,7 @@ fun AppMain() { navController = navController, startDestination = Screen.MAIN.name, modifier = Modifier.padding(innerPadding) + .background(MaterialTheme.colorScheme.background) ) { composable(route = Screen.MAIN.name) { MainScreen( @@ -141,30 +144,48 @@ private fun AppBar( currentScreen: Screen, canNavigateBack: Boolean, navigateUp: () -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() ) { - TopAppBar( - title = { Text(stringResource(currentScreen.title)) }, - modifier = modifier, - navigationIcon = { - if (canNavigateBack) { - IconButton(onClick = navigateUp) { - Icon( - imageVector = Icons.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) + if (currentScreen !== Screen.valueOf(Screen.MAIN.name)) { + SmallTopAppBar( + title = { Text(stringResource(currentScreen.title)) }, + modifier = modifier, + navigationIcon = { + if (canNavigateBack) { + IconButton(onClick = navigateUp) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } } + }, + actions = { + } - }, - actions = { - if (currentScreen == Screen.APP_LIST) { - AppListActions() + ) + } else { + LargeTopAppBar( + title = { Text(stringResource(currentScreen.title)) }, + modifier = modifier + .background(MaterialTheme.colorScheme.background), + navigationIcon = { + if (canNavigateBack) { + IconButton(onClick = navigateUp) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + }, + scrollBehavior = scrollBehavior, + actions = { + } - }, - colors = TopAppBarDefaults.mediumTopAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer ) - ) + } } @OptIn(ExperimentalPermissionsApi::class) @@ -238,7 +259,9 @@ private fun MainScreen( } Column( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() +// .background(MaterialTheme.colorScheme.background) + .background(Color(0xfff2f3f9)) .verticalScroll(rememberScrollState()) ) { PreferenceRowLink( diff --git a/app/src/main/java/com/pilot51/voicenotify/PreferenceRows.kt b/app/src/main/java/com/pilot51/voicenotify/PreferenceRows.kt index bb7cb25..2c69f9a 100644 --- a/app/src/main/java/com/pilot51/voicenotify/PreferenceRows.kt +++ b/app/src/main/java/com/pilot51/voicenotify/PreferenceRows.kt @@ -18,12 +18,15 @@ package com.pilot51.voicenotify import android.content.res.Configuration import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Checkbox import androidx.compose.material3.ListItem import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -33,6 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -62,6 +66,7 @@ fun PreferenceRowLink( Row( modifier = Modifier .fillMaxWidth() + .padding(8.dp) .height(IntrinsicSize.Min) .combinedClickable( enabled = enabled, @@ -92,6 +97,7 @@ fun PreferenceRowCheckbox( Row( modifier = Modifier .fillMaxWidth() + .padding(8.dp) .height(IntrinsicSize.Min) .toggleable( value = prefValue, @@ -104,10 +110,14 @@ fun PreferenceRowCheckbox( title = stringResource(titleRes), subtitle = stringResource(subtitleRes), action = { - Checkbox( + Switch( checked = prefValue, onCheckedChange = { prefValue = it } ) +// Checkbox( +// checked = prefValue, +// onCheckedChange = { prefValue = it } +// ) } ) } @@ -121,7 +131,8 @@ private fun PreferenceRowScaffold( action: (@Composable (Boolean) -> Unit)? = null ) { ListItem( - modifier = Modifier.defaultMinSize(minHeight = 88.dp), + modifier = Modifier.defaultMinSize(minHeight = 88.dp) + .background(MaterialTheme.colorScheme.background), headlineContent = { ColorWrap(enabled) { Text(title) diff --git a/app/src/main/java/com/pilot51/voicenotify/SearchBar.kt b/app/src/main/java/com/pilot51/voicenotify/SearchBar.kt new file mode 100644 index 0000000..92d5795 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/SearchBar.kt @@ -0,0 +1,90 @@ +package com.pilot51.voicenotify + + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Clear +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.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.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun SealSearchBar( + modifier: Modifier = Modifier, + text: String, + placeholderText: String, + onValueChange: (String) -> Unit, +) { + val view = LocalView.current + + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceVariant, + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.width(16.dp)) + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + SealAutoFocusTextField( + value = text, + onValueChange = onValueChange, + placeholder = { Text(text = placeholderText) }, + modifier = Modifier.weight(1f), + contentDescription = stringResource(id = R.string.test), + trailingIcon = { + if (text.isNotEmpty()) { + IconButton(onClick = { + onValueChange("") + view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) + }) { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Outlined.Clear, + contentDescription = stringResource(id = R.string.close), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + ) + } + } +} + +@Preview +@Composable +private fun SearchBarPreview() { + var text by remember { mutableStateOf("") } + AppTheme { + Surface { + SealSearchBar( + text = text, + placeholderText = "test", + ) { text = it } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/SwitchCustom.kt b/app/src/main/java/com/pilot51/voicenotify/SwitchCustom.kt new file mode 100644 index 0000000..84744da --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/SwitchCustom.kt @@ -0,0 +1,68 @@ +package com.pilot51.voicenotify +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Switch +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SwitchDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + + +@Composable +fun SwitchCustom( + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + switchColor: Color = MaterialTheme.colorScheme.primary, + backgroundColor: Color = MaterialTheme.colorScheme.surface, + shape: Shape = CircleShape +) { + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = switchColor, + uncheckedThumbColor = backgroundColor, + checkedTrackColor = switchColor.copy(alpha = 0.5f), + uncheckedTrackColor = backgroundColor.copy(alpha = 0.5f) + ), + modifier = modifier ?: Modifier + ) +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun SwitchCustomPreview() { + AppTheme { + var isChecked by remember { mutableStateOf(false) } + Switch( + checked = isChecked, + onCheckedChange = { isChecked = it }, + modifier = Modifier.padding(16.dp) + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Composable +private fun PreviewSwitchCustomChecked() { + AppTheme { + var isChecked by remember { mutableStateOf(true) } + Switch( + checked = isChecked, + onCheckedChange = { isChecked = it }, + modifier = Modifier.padding(16.dp) + ) + } +} diff --git a/app/src/main/java/com/pilot51/voicenotify/TextField.kt b/app/src/main/java/com/pilot51/voicenotify/TextField.kt new file mode 100644 index 0000000..900ba07 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/TextField.kt @@ -0,0 +1,240 @@ +package com.pilot51.voicenotify + + +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.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.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay + +/** + * @param contentDescription Text label of the `TextField` for the accessibility service + */ +@Composable +fun SealTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + contentDescription: String? = null, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @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.shape, + colors: TextFieldColors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ) +) { + TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier.then(Modifier.semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }), + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + interactionSource = interactionSource, + shape = shape, + colors = colors + ) +} + + +@Composable +fun SealAutoFocusTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + contentDescription: String? = null, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + visualTransformation: VisualTransformation = VisualTransformation.None, + singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + shape: Shape = TextFieldDefaults.shape, + colors: TextFieldColors = TextFieldDefaults.colors( + unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + focusRequester: FocusRequester = remember { FocusRequester() }, +) { + val focusManager = LocalFocusManager.current + + LaunchedEffect(Unit) { + delay(200) + focusRequester.requestFocus() + } + + TextField( + value = value, + onValueChange = onValueChange, + modifier = modifier + .then(Modifier.semantics { + if (contentDescription != null) { + this.contentDescription = contentDescription + } + }) + .focusRequester(focusRequester = focusRequester), + enabled = enabled, + readOnly = readOnly, + textStyle = textStyle, + label = label, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + prefix = prefix, + suffix = suffix, + supportingText = supportingText, + isError = isError, + visualTransformation = visualTransformation, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { + focusManager.clearFocus() + }), + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + interactionSource = interactionSource, + shape = shape, + colors = colors + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SealTextField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + label: @Composable (() -> Unit)? = null, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + prefix: @Composable (() -> Unit)? = null, + suffix: @Composable (() -> Unit)? = null, + supportingText: @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.shape, + colors: TextFieldColors = TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ) +) { + TextField( + value, + onValueChange, + modifier, + enabled, + readOnly, + textStyle, + label, + placeholder, + leadingIcon, + trailingIcon, + prefix, + suffix, + supportingText, + 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/TopAppBar.kt b/app/src/main/java/com/pilot51/voicenotify/TopAppBar.kt new file mode 100644 index 0000000..e85f221 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/TopAppBar.kt @@ -0,0 +1,67 @@ +package com.pilot51.voicenotify + +import android.graphics.Path +import android.view.animation.PathInterpolator +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LargeTopAppBar( + title: @Composable () -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null, +) { + androidx.compose.material3.LargeTopAppBar( + modifier = modifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior, + ) + +} + +private val path = Path().apply { + moveTo(0f,0f) + lineTo(0.7f, 0.1f) + cubicTo(0.7f, 0.1f, .95F, .5F, 1F, 1F) + moveTo(1f,1f) +} + +val fraction: (Float) -> Float = { PathInterpolator(path).getInterpolation(it) } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SmallTopAppBar( + modifier: Modifier = Modifier, + titleText: String = "", + scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + title: @Composable () -> Unit = { + Text( + text = titleText, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = fraction(scrollBehavior.state.overlappedFraction)), + maxLines = 1 + ) + }, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, +) { + TopAppBar( + modifier = modifier, + title = title, + navigationIcon = navigationIcon, + actions = actions, + scrollBehavior = scrollBehavior + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/ui/theme/Color.kt b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Color.kt new file mode 100644 index 0000000..5499a95 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.pilot51.voicenotify.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/ui/theme/Theme.kt b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Theme.kt new file mode 100644 index 0000000..0d42708 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Theme.kt @@ -0,0 +1,76 @@ +package com.pilot51.voicenotify.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// colorScheme = if (isSystemInDarkTheme()) { +// darkColorScheme(primary = Color(0xFF1CB7D5), primaryContainer = Color(0xFF1E4696)) +// } else { +// lightColorScheme(primary = Color(0xFF2A54A5), primaryContainer = Color(0xFF64F0FF)) +// }, + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80, + // background color + background = Color(0xFF010101) +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + + + // background color + background = Color(0xFFf2f3f8) + + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun VoicenotifyTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pilot51/voicenotify/ui/theme/Type.kt b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Type.kt new file mode 100644 index 0000000..03a7d99 --- /dev/null +++ b/app/src/main/java/com/pilot51/voicenotify/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.pilot51.voicenotify.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0be43cb..cb0241d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - - - Voice Notify + Voice Notify - - Back + + Back - - Voice Notify service is running - Voice Notify service is suspended + + Voice Notify service is running + Voice Notify service is suspended - - The phone calls permission is required to prevent Voice Notify from speaking during a call. - The post notifications permission is required for Voice Notify to post a test notification. - Voice Notify service is disabled - Tap to suspend Voice Notify.\nLong press to open the Android notification access settings to disable the service. - Tap to open the Android notification access settings to enable the service. - Tap to activate Voice Notify.\nLong press to open the Android notification access settings to disable the service. - App List - List installed apps which can be ignored - Text-To-Speech - Configure Text-To-Speech behavior - Pause/dim media - Request other media to pause/dim when speaking - Shake-To-Silence - Adjust the sensitivity threshold of Shake-To-Silence - Lower values are more sensitive.\n\nDefault: %d\nBlank to disable. - - Media - Notification - Voice - Ring - Alarm - - Require Text - Only speak notifications that contain certain text in the message - Ignore Text - Ignore notifications that contain certain text in the message - This is matched against the message sent to TTS, after TTS Message formatting and Text Replacement, including punctuation.\n\nSeparate individual entries with a new line.\nThe message needs to contain any entry, not all, to be a match.\nCase insensitive. - Ignore Empty - Notifications without a message will not be spoken - Notifications without a message will be spoken as \"Notification from [app name].\" - Ignore Group Messages - Notifications containing multiple messages will not be spoken - Notifications containing multiple messages will be spoken - Ignore Repeats - Ignore subsequent identical notifications within a set time - Set number of seconds that must pass since last notification before a repeat can be spoken.\nIgnored repeats reset the count and different notifications clear it.\nBlank = infinite. - Device States - Select whether to speak during certain device states - Speak during these device states - - Screen Off - Screen On - Headset Off - Headset On - Silent/Vibrate - - Quiet Time Start - Don\'t speak after this time.\nLeave the same as end to disable. - Quiet Time End - Don\'t speak before this time.\nLeave the same as start to disable. - Test - Post a notification (delayed 5 seconds) to test current settings - Voice Notify is ignored in App List.\nTest will run, but notification will not be spoken. - Used for test notification - This is the ticker message - The Subtext - Content Title - This is the content message. - Content Info - Big Content Title - This is the big content summary - This is the big content text. - This is a line of text.\nThis is another line of text. - Notification Log - List last %d notifications.\nDisplays time, app name, message, and ignore reasons. - Notification Details - Enable - Ticker - Subtext - Title - Content Text - Content Info Text - Big Content Summary - Big Content Title - Big Content Text - Text Lines - Ignored Reasons - Interrupted Reason - Metadata - ID: %d - Category: %s - Progress: %1$d / %2$d - Progress: Indeterminate - Ignore %s? - Unignore %s? - Yes - Help & Support - Rate & review, email the developer, community chat, translations, source code, issue tracker, privacy policy - Rate & Review - Email Developer - General feedback - Discord - Matrix - User chat, fastest developer response - Translations - GitHub - Bug reports, feature requests, source code - Privacy Policy - Voice Notify does not collect or transmit data off of the device. However, it does use third party apps or services that may transmit data, as outlined below under \"Third party apps and services\". + + The phone calls permission is required to prevent Voice Notify from speaking during a call. + The post notifications permission is required for Voice Notify to post a test notification. + Voice Notify service is disabled + Tap to suspend Voice Notify.\nLong press to open the Android notification access settings to disable the service. + Tap to open the Android notification access settings to enable the service. + Tap to activate Voice Notify.\nLong press to open the Android notification access settings to disable the service. + App List + List installed apps which can be ignored + Text-To-Speech + Configure Text-To-Speech behavior + Pause/dim media + Request other media to pause/dim when speaking + Shake-To-Silence + Adjust the sensitivity threshold of Shake-To-Silence + Lower values are more sensitive.\n\nDefault: %d\nBlank to disable. + Require Text + Only speak notifications that contain certain text in the message + Ignore Text + Ignore notifications that contain certain text in the message + This is matched against the message sent to TTS, after TTS Message formatting and Text Replacement, including punctuation.\n\nSeparate individual entries with a new line.\nThe message needs to contain any entry, not all, to be a match.\nCase insensitive. + Ignore Empty + Notifications without a message will not be spoken + Notifications without a message will be spoken as \"Notification from [app name].\" + Ignore Group Messages + Notifications containing multiple messages will not be spoken + Notifications containing multiple messages will be spoken + Ignore Repeats + Ignore subsequent identical notifications within a set time + Set number of seconds that must pass since last notification before a repeat can be spoken.\nIgnored repeats reset the count and different notifications clear it.\nBlank = infinite. + Device States + Select whether to speak during certain device states + Speak during these device states + Quiet Time Start + Don\'t speak after this time.\nLeave the same as end to disable. + Quiet Time End + Don\'t speak before this time.\nLeave the same as start to disable. + Test + Post a notification (delayed 5 seconds) to test current settings + Voice Notify is ignored in App List.\nTest will run, but notification will not be spoken. + Used for test notification + This is the ticker message + The Subtext + Content Title + This is the content message. + Content Info + Big Content Title + This is the big content summary + This is the big content text. + This is a line of text.\nThis is another line of text. + Notification Log + List last %d notifications.\nDisplays time, app name, message, and ignore reasons. + Notification Details + Enable + Ticker + Subtext + Title + Content Text + Content Info Text + Big Content Summary + Big Content Title + Big Content Text + Text Lines + Ignored Reasons + Interrupted Reason + Metadata + ID: %d + Category: %s + Progress: %1$d / %2$d + Progress: Indeterminate + Ignore %s? + Unignore %s? + Yes + Help & Support + Rate & review, email the developer, community chat, translations, source code, issue tracker, privacy policy + Rate & Review + Email Developer + General feedback + Discord + Matrix + User chat, fastest developer response + Translations + GitHub + Bug reports, feature requests, source code + Privacy Policy + Voice Notify does not collect or transmit data off of the device. However, it does use third party apps or services that may transmit data, as outlined below under \"Third party apps and services\". \n\nIn the unlikely event that sensitive or personal information is received by the developer, it will not be sold, shared, copied, or used without the express consent of who the information belongs to. In the absence of consent, the information will be deleted if possible. \n\nThe purpose of Voice Notify is to speak notifications, and as such it is likely that spoken notifications may be heard by people or microphones in the vicinity. It is recommended to configure Voice Notify and the device to prevent undesired spoken notifications. Use at your own risk. \n\nThe notifications received by Voice Notify are only held in memory to be displayed in the Notification Log (up to the most recent 20) and are not written to storage. This prevents other apps from accessing the data, especially if the device is rooted. As a result, if the Voice Notify process is terminated, the notification log is cleared. @@ -137,71 +122,89 @@ \n• Vibrate - Required for Test feature while phone is in vibrate mode. \n• Modify Audio Settings - Required for improved wired headset detection. \n• Read Phone State - Required to interrupt TTS if a phone call becomes active. - Error: Unable to find Google Play Store installed. - Error: Unable to find an installed email app. - Error: Unable to find an installed browser app. + Error: Unable to find Google Play Store installed. + Error: Unable to find an installed email app. + Error: Unable to find an installed browser app. + Close + Ignore All - - Close - Ignore All - Ignore None - Filter + + Ignore None + Filter + TTS Settings + Open the Android Text-To-Speech settings - - TTS Settings - Open the Android Text-To-Speech settings - Unable to find TTS settings! If it exists on your device (it should), please contact the developer. - TTS Message - Customize which parts of notifications are spoken - #A = App title\n#T = Ticker\n#S = Subtext\n#C = Content title\n#M = Content message\n#I = Content info\n#H = Big content title\n#Y = Big content summary\n#B = Big content text\n#L = Text lines\nCase insensitive\n\nDefault:\n%1$s\n\nOld default (v1.1.0 - v1.3.0):\n#A. #C. #M.\n\nOld default (v1.0.x):\n#A: #T - TTS Text Replacement - Substitute text to be spoken, such as to fix pronunciation - Substitute text to be spoken, allowing you to customize how Text-To-Speech pronounces words or replace text for any other reason.\n\nText to replace is case insensitive and applies after the formatting set in TTS Message, including punctuation. - Duplicate! Will not be saved. - Text to replace - Replacement text - Remove - Error - Maximum Message - Maximum message length to speak. Larger messages will be truncated. - Maximum message length to speak\nLarger messages will be truncated. - TTS Audio Stream - Choose the audio stream that Text-To-Speech plays through - TTS Delay - Delay TTS a set number of seconds after notification - Number of seconds to wait after notification before speaking. - TTS Repeat - Continually repeat notifications over TTS until screen turns on - Notifications are repeated at the defined interval until the screen is turned on. Notifications created while the screen is on are not repeated.\n\nValue is in minutes. Blank or 0 to disable. + + Unable to find TTS settings! If it exists on your device (it should), please contact the developer. + TTS Message + Customize which parts of notifications are spoken + #A = App title\n#T = Ticker\n#S = Subtext\n#C = Content title\n#M = Content message\n#I = Content info\n#H = Big content title\n#Y = Big content summary\n#B = Big content text\n#L = Text lines\nCase insensitive\n\nDefault:\n%1$s\n\nOld default (v1.1.0 - v1.3.0):\n#A. #C. #M.\n\nOld default (v1.0.x):\n#A: #T + TTS Text Replacement + Substitute text to be spoken, such as to fix pronunciation + Substitute text to be spoken, allowing you to customize how Text-To-Speech pronounces words or replace text for any other reason.\n\nText to replace is case insensitive and applies after the formatting set in TTS Message, including punctuation. + Duplicate! Will not be saved. + Text to replace + Replacement text + Remove + Error + Maximum Message + Maximum message length to speak. Larger messages will be truncated. + Maximum message length to speak\nLarger messages will be truncated. + TTS Audio Stream + Choose the audio stream that Text-To-Speech plays through + TTS Delay + Delay TTS a set number of seconds after notification + Number of seconds to wait after notification before speaking. + TTS Repeat + Continually repeat notifications over TTS until screen turns on + Notifications are repeated at the defined interval until the screen is turned on. Notifications created while the screen is on are not repeated.\n\nValue is in minutes. Blank or 0 to disable. + %s is ignored + %s is not ignored - - %s is ignored - %s is not ignored + + Notification from %s. + Error: Could not initialize TTS! Status code %d - - Notification from %s. + + Ignored app - - Error: Could not initialize TTS! Status code %d + + Ignored text - - Ignored app - Ignored text - Required text missing - Empty message - Identical message within {0,choice,-1#infinite seconds|1#{0} second|1<{0} seconds} - Quiet time - Silent or vibrate mode - Active phone call - Screen off - Screen on - Headset off - Headset on - Silenced by shake - Voice Notify suspended by widget - Service stopped - TTS failed. Restart and retry attempted.\nThis text should be yellow if retry successful or red if not.\nPlease try restarting the Voice Notify service if notifications fail to be spoken. - TTS restarted while message in queue or speaking. Re-queued. - TTS interrupted for unknown reason - Message exceeded TTS length limit + + Required text missing + Empty message + Identical message within {0,choice,-1#infinite seconds|1#{0} second|1<{0} seconds} + Quiet time + Silent or vibrate mode + Active phone call + Screen off + Screen on + Headset off + Headset on + Silenced by shake + Voice Notify suspended by widget + Service stopped + TTS failed. Restart and retry attempted.\nThis text should be yellow if retry successful or red if not.\nPlease try restarting the Voice Notify service if notifications fail to be spoken. + TTS restarted while message in queue or speaking. Re-queued. + TTS interrupted for unknown reason + Message exceeded TTS length limit + AppListDemo.kt + ListDemo + Alphabet + AlphabetIndexHelper + + Media + Notification + Voice + Ring + Alarm + + + Screen Off + Screen On + Headset Off + Headset On + Silent/Vibrate + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..2d378eb --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + + -