diff --git a/app/build.gradle b/app/build.gradle index 92fd2a7e9..8f7036e03 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,6 +10,7 @@ plugins { id "checkstyle" id "org.sonarqube" version "4.0.0.2929" id "org.jetbrains.kotlin.plugin.compose" version "${kotlin_version}" + id 'com.google.dagger.hilt.android' } android { @@ -190,6 +191,10 @@ sonar { } } +kapt { + correctErrorTypes true +} + dependencies { /** Desugaring **/ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' @@ -200,7 +205,7 @@ dependencies { // name and the commit hash with the commit hash of the (pushed) commit you want to test // This works thanks to JitPack: https://jitpack.io/ implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' - implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.2' + implementation 'com.github.teamnewpipe:newpipeextractor:v0.24.2' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' /** Checkstyle **/ @@ -285,20 +290,29 @@ dependencies { // Date and time formatting implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" - // Jetpack Compose - implementation(platform('androidx.compose:compose-bom:2024.06.00')) - implementation 'androidx.compose.material3:material3:1.3.0-beta05' - implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0-beta04' - implementation 'androidx.activity:activity-compose' + // Jetpack Compose BOM group + implementation(platform('androidx.compose:compose-bom:2024.09.03')) + implementation 'androidx.compose.material3:material3' implementation 'androidx.compose.ui:ui-tooling-preview' - implementation 'androidx.compose.ui:ui-text:1.7.0-beta07' // Needed for parsing HTML to AnnotatedString - implementation 'androidx.lifecycle:lifecycle-viewmodel-compose' + implementation 'androidx.compose.ui:ui-text' // Needed for parsing HTML to AnnotatedString + + // Jetpack Compose related dependencies + implementation 'androidx.compose.material3.adaptive:adaptive:1.0.0' + implementation 'androidx.activity:activity-compose:1.9.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6' implementation 'androidx.paging:paging-compose:3.3.2' - implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' + implementation "androidx.navigation:navigation-compose:2.8.2" // Coroutines interop implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1' + // Hilt + implementation("com.google.dagger:hilt-android:2.51.1") + kapt("com.google.dagger:hilt-compiler:2.51.1") + + // Scroll + implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' + /** Debugging **/ // Memory leak detection debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d11de9f47..c44f8bf2c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -77,6 +77,11 @@ android:exported="false" android:label="@string/settings" /> + + . */ +@HiltAndroidApp public class App extends Application implements ImageLoaderFactory { public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; private static final String TAG = App.class.toString(); diff --git a/app/src/main/java/org/schabi/newpipe/AppModule.kt b/app/src/main/java/org/schabi/newpipe/AppModule.kt new file mode 100644 index 000000000..0aaf2f72b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/AppModule.kt @@ -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) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt new file mode 100644 index 000000000..ac08dd36b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugScreen.kt @@ -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 + ) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt new file mode 100644 index 000000000..5bd8f2b08 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsScreen.kt @@ -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) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt new file mode 100644 index 000000000..821ff0187 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsV2Activity.kt @@ -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) +} diff --git a/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt new file mode 100644 index 000000000..ae3520c94 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/settings/viewmodel/SettingsViewModel.kt @@ -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 = + MutableStateFlow(_settingsLayoutRedesignPref) + val settingsLayoutRedesign = _settingsLayoutRedesign.asStateFlow() + + fun toggleSettingsLayoutRedesign(newState: Boolean) { + _settingsLayoutRedesign.value = newState + _settingsLayoutRedesignPref = newState + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt new file mode 100644 index 000000000..d479343f5 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/SwitchPreference.kt @@ -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) + } +} diff --git a/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt new file mode 100644 index 000000000..f58f2f305 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/ui/TextPreference.kt @@ -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, + ) + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index 534d7085b..219328875 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -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; @@ -648,7 +650,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); } diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index e31cebb92..b0fceb89b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -246,6 +246,7 @@ crash_the_app_key show_error_snackbar_key create_error_notification_key + settings_layout_redesign_key theme diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 938a2497d..eecce7988 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -492,6 +492,7 @@ Crash the app Show an error snackbar Create an error notification + Enable the Redesigned Settings page Import Import from diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml index d97c5aa1a..5b6909892 100644 --- a/app/src/main/res/xml/debug_settings.xml +++ b/app/src/main/res/xml/debug_settings.xml @@ -64,4 +64,11 @@ android:title="@string/create_error_notification" app:singleLineTitle="false" app:iconSpaceReserved="false" /> + + diff --git a/build.gradle b/build.gradle index 1acfb6f4a..7249be503 100644 --- a/build.gradle +++ b/build.gradle @@ -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