Initial Work for Settings Page with Jetpack Compose

- Implemented a new settings page using Jetpack Compose.
- Added a new settings option to enable the redesigned settings page.
- This option allows for gradual integration and testing of the new
  settings page, minimizing disruptions to current functionality.

Plan for Settings Items:
- Jetpack Compose does not have a direct equivalent to the
  Preference/settings library.
- We could consider using third-party libraries that offer preference
  items as composables.
- However, these libraries may be incomplete or lack active development.
- Given our specific needs for only a subset of preference types,
  creating custom composables would be beneficial.
- This approach allows for fine-tuning the components to our specific
  use case.
This commit is contained in:
Siddhesh Naik 2024-02-27 03:03:48 +05:30
parent d479f29e9b
commit b1d8f59ee5
15 changed files with 351 additions and 1 deletions

View File

@ -9,6 +9,7 @@ plugins {
id "kotlin-parcelize"
id "checkstyle"
id "org.sonarqube" version "4.0.0.2929"
id 'com.google.dagger.hilt.android'
}
android {
@ -193,6 +194,10 @@ sonar {
}
}
kapt {
correctErrorTypes true
}
dependencies {
/** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4'
@ -294,6 +299,11 @@ dependencies {
implementation 'androidx.compose.material3:material3'
implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation "androidx.navigation:navigation-compose:2.7.7"
// Hilt
implementation("com.google.dagger:hilt-android:2.51.1")
kapt("com.google.dagger:hilt-compiler:2.51.1")
/** Debugging **/
// Memory leak detection

View File

@ -77,6 +77,11 @@
android:exported="false"
android:label="@string/settings" />
<activity
android:name=".settings.SettingsV2Activity"
android:exported="true"
android:label="@string/settings" />
<activity
android:name=".about.AboutActivity"
android:exported="false"

View File

@ -32,6 +32,7 @@ import java.net.SocketException;
import java.util.List;
import java.util.Objects;
import dagger.hilt.android.HiltAndroidApp;
import io.reactivex.rxjava3.exceptions.CompositeException;
import io.reactivex.rxjava3.exceptions.MissingBackpressureException;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
@ -57,6 +58,7 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
* along with NewPipe. If not, see <http://www.gnu.org/licenses/>.
*/
@HiltAndroidApp
public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString();

View File

@ -0,0 +1,22 @@
package org.schabi.newpipe
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Provides
@Singleton
fun providesSharedPreference(@ApplicationContext context: Context): SharedPreferences {
return PreferenceManager.getDefaultSharedPreferences(context)
}
}

View File

@ -0,0 +1,27 @@
package org.schabi.newpipe.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import org.schabi.newpipe.R
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
import org.schabi.newpipe.ui.SwitchPreference
import org.schabi.newpipe.ui.theme.SizeTokens
@Composable
fun DebugScreen(viewModel: SettingsViewModel, modifier: Modifier = Modifier) {
val settingsLayoutRedesign by viewModel.settingsLayoutRedesign.collectAsState()
Column(modifier = modifier) {
SwitchPreference(
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
R.string.settings_layout_redesign,
settingsLayoutRedesign,
viewModel::toggleSettingsLayoutRedesign
)
}
}

View File

@ -0,0 +1,23 @@
package org.schabi.newpipe.settings
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.schabi.newpipe.R
import org.schabi.newpipe.ui.TextPreference
@Composable
fun SettingsScreen(
onSelectSettingOption: (SettingsScreenKey) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
TextPreference(
title = R.string.settings_category_debug_title,
onClick = { onSelectSettingOption(SettingsScreenKey.DEBUG) }
)
HorizontalDivider(color = Color.Black)
}
}

View File

@ -0,0 +1,85 @@
package org.schabi.newpipe.settings
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import dagger.hilt.android.AndroidEntryPoint
import org.schabi.newpipe.R
import org.schabi.newpipe.settings.viewmodel.SettingsViewModel
import org.schabi.newpipe.ui.Toolbar
import org.schabi.newpipe.ui.theme.AppTheme
const val SCREEN_TITLE_KEY = "SCREEN_TITLE_KEY"
@AndroidEntryPoint
class SettingsV2Activity : ComponentActivity() {
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
var screenTitle by remember { mutableIntStateOf(SettingsScreenKey.ROOT.screenTitle) }
navController.addOnDestinationChangedListener { _, _, arguments ->
screenTitle =
arguments?.getInt(SCREEN_TITLE_KEY) ?: SettingsScreenKey.ROOT.screenTitle
}
AppTheme {
Scaffold(topBar = {
Toolbar(
title = stringResource(id = screenTitle),
hasSearch = true,
onSearchQueryChange = null // TODO: Add suggestions logic
)
}) { padding ->
NavHost(
navController = navController,
startDestination = SettingsScreenKey.ROOT.name,
modifier = Modifier.padding(padding)
) {
composable(
SettingsScreenKey.ROOT.name,
listOf(createScreenTitleArg(SettingsScreenKey.ROOT.screenTitle))
) {
SettingsScreen(onSelectSettingOption = { screen ->
navController.navigate(screen.name)
})
}
composable(
SettingsScreenKey.DEBUG.name,
listOf(createScreenTitleArg(SettingsScreenKey.DEBUG.screenTitle))
) {
DebugScreen(settingsViewModel)
}
}
}
}
}
}
}
fun createScreenTitleArg(@StringRes screenTitle: Int) = navArgument(SCREEN_TITLE_KEY) {
defaultValue = screenTitle
}
enum class SettingsScreenKey(@StringRes val screenTitle: Int) {
ROOT(R.string.settings),
DEBUG(R.string.settings_category_debug_title)
}

View File

@ -0,0 +1,39 @@
package org.schabi.newpipe.settings.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.schabi.newpipe.R
import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
@ApplicationContext context: Context,
private val preferenceManager: SharedPreferences
) : AndroidViewModel(context.applicationContext as Application) {
private var _settingsLayoutRedesignPref: Boolean
get() = preferenceManager.getBoolean(
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key), false
)
set(value) {
preferenceManager.edit().putBoolean(
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key),
value
).apply()
}
private val _settingsLayoutRedesign: MutableStateFlow<Boolean> =
MutableStateFlow(_settingsLayoutRedesignPref)
val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow()
fun toggleSettingsLayoutRedesign(newState: Boolean) {
_settingsLayoutRedesign.value = newState
_settingsLayoutRedesignPref = newState
}
}

View File

@ -0,0 +1,53 @@
package org.schabi.newpipe.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import org.schabi.newpipe.ui.theme.SizeTokens
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
@StringRes title: Int,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
@StringRes summary: Int? = null
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier.fillMaxWidth()
) {
Column {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Start,
)
summary?.let {
Text(
text = stringResource(id = summary),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Start,
)
}
}
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
}
}

View File

@ -0,0 +1,66 @@
package org.schabi.newpipe.ui
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import org.schabi.newpipe.ui.theme.SizeTokens
@Composable
fun TextPreference(
modifier: Modifier = Modifier,
@StringRes title: Int,
@DrawableRes icon: Int? = null,
@StringRes summary: Int? = null,
onClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = modifier
.fillMaxWidth()
.padding(SizeTokens.SpacingSmall)
.defaultMinSize(minHeight = SizeTokens.SpaceMinSize)
.clickable { onClick() }
) {
icon?.let {
Icon(
painter = painterResource(id = icon),
contentDescription = "icon for $title preference"
)
Spacer(modifier = Modifier.width(SizeTokens.SpacingSmall))
}
Column {
Text(
text = stringResource(id = title),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.titleSmall,
textAlign = TextAlign.Start,
)
summary?.let {
Text(
text = stringResource(id = summary),
modifier = Modifier.padding(SizeTokens.SpacingExtraSmall),
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Start,
)
}
}
}
}

View File

@ -21,6 +21,7 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import com.jakewharton.processphoenix.ProcessPhoenix;
@ -64,6 +65,7 @@ import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.settings.SettingsV2Activity;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.List;
@ -649,7 +651,13 @@ public final class NavigationHelper {
}
public static void openSettings(final Context context) {
final Intent intent = new Intent(context, SettingsActivity.class);
final Class<?> settingsClass = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean(
ContextCompat.getString(context, R.string.settings_layout_redesign_key),
false
) ? SettingsV2Activity.class : SettingsActivity.class;
final Intent intent = new Intent(context, settingsClass);
context.startActivity(intent);
}

View File

@ -247,6 +247,7 @@
<string name="crash_the_app_key">crash_the_app_key</string>
<string name="show_error_snackbar_key">show_error_snackbar_key</string>
<string name="create_error_notification_key">create_error_notification_key</string>
<string name="settings_layout_redesign_key">settings_layout_redesign_key</string>
<!-- THEMES -->
<string name="theme_key">theme</string>

View File

@ -494,6 +494,7 @@
<string name="crash_the_app">Crash the app</string>
<string name="show_error_snackbar">Show an error snackbar</string>
<string name="create_error_notification">Create an error notification</string>
<string name="settings_layout_redesign">Enable the Redesigned Settings page</string>
<!-- Subscriptions import/export -->
<string name="import_title">Import</string>
<string name="import_from">Import from</string>

View File

@ -71,4 +71,11 @@
android:title="@string/create_error_notification"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/settings_layout_redesign_key"
android:title="@string/settings_layout_redesign"
app:iconSpaceReserved="false"
app:singleLineTitle="false" />
</PreferenceScreen>

View File

@ -9,6 +9,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.51.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files