mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-10-31 15:23:00 +00:00 
			
		
		
		
	Added the new compose screen with its components and events
This commit is contained in:
		| @@ -310,9 +310,11 @@ dependencies { | |||||||
|     // Coroutines interop |     // Coroutines interop | ||||||
|     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1' |     implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1' | ||||||
|  |  | ||||||
|     // Hilt |     // Hilt & Dagger | ||||||
|     implementation("com.google.dagger:hilt-android:2.51.1") |     implementation("com.google.dagger:hilt-android:2.51.1") | ||||||
|     kapt("com.google.dagger:hilt-compiler:2.51.1") |     kapt("com.google.dagger:hilt-compiler:2.51.1") | ||||||
|  |     implementation("androidx.hilt:hilt-navigation-compose:1.2.0") | ||||||
|  |     kapt("androidx.hilt:hilt-compiler:1.2.0") | ||||||
|  |  | ||||||
|     // Scroll |     // Scroll | ||||||
|     implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' |     implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0' | ||||||
|   | |||||||
| @@ -94,6 +94,9 @@ import java.util.ArrayList; | |||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
|  |  | ||||||
|  | import dagger.hilt.android.AndroidEntryPoint; | ||||||
|  |  | ||||||
|  | @AndroidEntryPoint | ||||||
| public class MainActivity extends AppCompatActivity { | public class MainActivity extends AppCompatActivity { | ||||||
|     private static final String TAG = "MainActivity"; |     private static final String TAG = "MainActivity"; | ||||||
|     @SuppressWarnings("ConstantConditions") |     @SuppressWarnings("ConstantConditions") | ||||||
|   | |||||||
| @@ -0,0 +1,27 @@ | |||||||
|  | package org.schabi.newpipe.dependency_injection | ||||||
|  |  | ||||||
|  | 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 org.schabi.newpipe.error.usecases.OpenErrorActivity | ||||||
|  | import javax.inject.Singleton | ||||||
|  |  | ||||||
|  | @Module | ||||||
|  | @InstallIn(SingletonComponent::class) | ||||||
|  | object AppModule { | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideSharedPreferences(@ApplicationContext context: Context): SharedPreferences = | ||||||
|  |         PreferenceManager.getDefaultSharedPreferences(context) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideOpenActivity( | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): OpenErrorActivity = OpenErrorActivity(context) | ||||||
|  | } | ||||||
| @@ -0,0 +1,56 @@ | |||||||
|  | package org.schabi.newpipe.dependency_injection | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import androidx.room.Room | ||||||
|  | import dagger.Module | ||||||
|  | import dagger.Provides | ||||||
|  | import dagger.hilt.InstallIn | ||||||
|  | import dagger.hilt.android.qualifiers.ApplicationContext | ||||||
|  | import dagger.hilt.components.SingletonComponent | ||||||
|  | import org.schabi.newpipe.database.AppDatabase | ||||||
|  | import org.schabi.newpipe.database.AppDatabase.DATABASE_NAME | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_1_2 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_2_3 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_3_4 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_4_5 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_5_6 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 | ||||||
|  | import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 | ||||||
|  | import org.schabi.newpipe.database.history.dao.SearchHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.history.dao.StreamHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamStateDAO | ||||||
|  | import javax.inject.Singleton | ||||||
|  |  | ||||||
|  | @InstallIn(SingletonComponent::class) | ||||||
|  | @Module | ||||||
|  | class DatabaseModule { | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideAppDatabase(@ApplicationContext appContext: Context): AppDatabase = | ||||||
|  |         Room.databaseBuilder( | ||||||
|  |             appContext, | ||||||
|  |             AppDatabase::class.java, | ||||||
|  |             DATABASE_NAME | ||||||
|  |         ).addMigrations( | ||||||
|  |             MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, | ||||||
|  |             MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9 | ||||||
|  |         ).build() | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     fun provideStreamStateDao(appDatabase: AppDatabase): StreamStateDAO = | ||||||
|  |         appDatabase.streamStateDAO() | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     fun providesStreamDao(appDatabase: AppDatabase): StreamDAO = appDatabase.streamDAO() | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     fun provideStreamHistoryDao(appDatabase: AppDatabase): StreamHistoryDAO = | ||||||
|  |         appDatabase.streamHistoryDAO() | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     fun provideSearchHistoryDao(appDatabase: AppDatabase): SearchHistoryDAO = | ||||||
|  |         appDatabase.searchHistoryDAO() | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | package org.schabi.newpipe.error.usecases | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.Intent | ||||||
|  | import org.schabi.newpipe.error.ErrorActivity | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo | ||||||
|  |  | ||||||
|  | class OpenErrorActivity( | ||||||
|  |     private val context: Context, | ||||||
|  | ) { | ||||||
|  |     operator fun invoke(errorInfo: ErrorInfo) { | ||||||
|  |         val intent = Intent(context, ErrorActivity::class.java) | ||||||
|  |         intent.putExtra(ErrorActivity.ERROR_INFO, errorInfo) | ||||||
|  |         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | ||||||
|  |  | ||||||
|  |         context.startActivity(intent) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -13,6 +13,7 @@ import android.widget.Toast; | |||||||
|  |  | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
| import androidx.viewbinding.ViewBinding; | import androidx.viewbinding.ViewBinding; | ||||||
|  |  | ||||||
| import com.evernote.android.state.State; | import com.evernote.android.state.State; | ||||||
| @@ -27,6 +28,7 @@ import org.schabi.newpipe.database.stream.model.StreamEntity; | |||||||
| import org.schabi.newpipe.databinding.PlaylistControlBinding; | import org.schabi.newpipe.databinding.PlaylistControlBinding; | ||||||
| import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; | import org.schabi.newpipe.databinding.StatisticPlaylistControlBinding; | ||||||
| import org.schabi.newpipe.error.ErrorInfo; | import org.schabi.newpipe.error.ErrorInfo; | ||||||
|  | import org.schabi.newpipe.error.ErrorUtil; | ||||||
| import org.schabi.newpipe.error.UserAction; | import org.schabi.newpipe.error.UserAction; | ||||||
| import org.schabi.newpipe.extractor.stream.StreamInfoItem; | import org.schabi.newpipe.extractor.stream.StreamInfoItem; | ||||||
| import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; | import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder; | ||||||
| @@ -35,7 +37,6 @@ import org.schabi.newpipe.info_list.dialog.StreamDialogDefaultEntry; | |||||||
| import org.schabi.newpipe.local.BaseLocalListFragment; | import org.schabi.newpipe.local.BaseLocalListFragment; | ||||||
| import org.schabi.newpipe.player.playqueue.PlayQueue; | import org.schabi.newpipe.player.playqueue.PlayQueue; | ||||||
| import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | import org.schabi.newpipe.player.playqueue.SinglePlayQueue; | ||||||
| import org.schabi.newpipe.settings.HistorySettingsFragment; |  | ||||||
| import org.schabi.newpipe.util.NavigationHelper; | import org.schabi.newpipe.util.NavigationHelper; | ||||||
| import org.schabi.newpipe.util.OnClickGesture; | import org.schabi.newpipe.util.OnClickGesture; | ||||||
| import org.schabi.newpipe.util.PlayButtonHelper; | import org.schabi.newpipe.util.PlayButtonHelper; | ||||||
| @@ -161,14 +162,72 @@ public class StatisticsPlaylistFragment | |||||||
|     @Override |     @Override | ||||||
|     public boolean onOptionsItemSelected(final MenuItem item) { |     public boolean onOptionsItemSelected(final MenuItem item) { | ||||||
|         if (item.getItemId() == R.id.action_history_clear) { |         if (item.getItemId() == R.id.action_history_clear) { | ||||||
|             HistorySettingsFragment |             openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); | ||||||
|                     .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables); |  | ||||||
|         } else { |         } else { | ||||||
|             return super.onOptionsItemSelected(item); |             return super.onOptionsItemSelected(item); | ||||||
|         } |         } | ||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private static void openDeleteWatchHistoryDialog( | ||||||
|  |             @NonNull final Context context, | ||||||
|  |             final HistoryRecordManager recordManager, | ||||||
|  |             final CompositeDisposable disposables | ||||||
|  |     ) { | ||||||
|  |         new AlertDialog.Builder(context) | ||||||
|  |                 .setTitle(R.string.delete_view_history_alert) | ||||||
|  |                 .setNegativeButton(R.string.cancel, ((dialog, which) -> dialog.dismiss())) | ||||||
|  |                 .setPositiveButton(R.string.delete, ((dialog, which) -> { | ||||||
|  |                     disposables.add(getDeletePlaybackStatesDisposable(context, recordManager)); | ||||||
|  |                     disposables.add(getWholeStreamHistoryDisposable(context, recordManager)); | ||||||
|  |                     disposables.add(getRemoveOrphanedRecordsDisposable(context, recordManager)); | ||||||
|  |                 })) | ||||||
|  |                 .show(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static Disposable getDeletePlaybackStatesDisposable( | ||||||
|  |             @NonNull final Context context, | ||||||
|  |             final HistoryRecordManager recordManager | ||||||
|  |     ) { | ||||||
|  |         return recordManager.deleteCompleteStreamStateHistory() | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                         howManyDeleted -> Toast.makeText(context, | ||||||
|  |                                 R.string.watch_history_states_deleted, Toast.LENGTH_SHORT).show(), | ||||||
|  |                         throwable -> ErrorUtil.openActivity(context, | ||||||
|  |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                                         "Delete playback states")) | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static Disposable getWholeStreamHistoryDisposable( | ||||||
|  |             @NonNull final Context context, | ||||||
|  |             final HistoryRecordManager recordManager | ||||||
|  |     ) { | ||||||
|  |         return recordManager.deleteWholeStreamHistory() | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                         howManyDeleted -> Toast.makeText(context, | ||||||
|  |                                 R.string.watch_history_deleted, Toast.LENGTH_SHORT).show(), | ||||||
|  |                         throwable -> ErrorUtil.openActivity(context, | ||||||
|  |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                                         "Delete from history")) | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private static Disposable getRemoveOrphanedRecordsDisposable( | ||||||
|  |             @NonNull final Context context, final HistoryRecordManager recordManager) { | ||||||
|  |         return recordManager.removeOrphanedRecords() | ||||||
|  |                 .observeOn(AndroidSchedulers.mainThread()) | ||||||
|  |                 .subscribe( | ||||||
|  |                         howManyDeleted -> { | ||||||
|  |                         }, | ||||||
|  |                         throwable -> ErrorUtil.openActivity(context, | ||||||
|  |                                 new ErrorInfo(throwable, UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                                         "Clear orphaned records")) | ||||||
|  |                 ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     /////////////////////////////////////////////////////////////////////////// |     /////////////////////////////////////////////////////////////////////////// | ||||||
|     // Fragment LifeCycle - Loading |     // Fragment LifeCycle - Loading | ||||||
|     /////////////////////////////////////////////////////////////////////////// |     /////////////////////////////////////////////////////////////////////////// | ||||||
|   | |||||||
| @@ -43,6 +43,9 @@ import org.schabi.newpipe.views.FocusOverlayView; | |||||||
|  |  | ||||||
| import java.util.concurrent.TimeUnit; | import java.util.concurrent.TimeUnit; | ||||||
|  |  | ||||||
|  | import dagger.hilt.android.AndroidEntryPoint; | ||||||
|  |  | ||||||
|  |  | ||||||
| /* | /* | ||||||
|  * Created by Christian Schabesberger on 31.08.15. |  * Created by Christian Schabesberger on 31.08.15. | ||||||
|  * |  * | ||||||
| @@ -63,6 +66,7 @@ import java.util.concurrent.TimeUnit; | |||||||
|  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. |  * along with NewPipe.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  */ |  */ | ||||||
|  |  | ||||||
|  | @AndroidEntryPoint | ||||||
| public class SettingsActivity extends AppCompatActivity implements | public class SettingsActivity extends AppCompatActivity implements | ||||||
|         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, |         PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, | ||||||
|         PreferenceSearchResultListener { |         PreferenceSearchResultListener { | ||||||
|   | |||||||
| @@ -0,0 +1,85 @@ | |||||||
|  | package org.schabi.newpipe.settings.components.irreversible_preference | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.clickable | ||||||
|  | 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.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.derivedStateOf | ||||||
|  | import androidx.compose.runtime.getValue | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.draw.alpha | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  | import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall | ||||||
|  | import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun IrreversiblePreferenceComponent( | ||||||
|  |     title: String, | ||||||
|  |     summary: String, | ||||||
|  |     onClick: () -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     enabled: Boolean = true, | ||||||
|  | ) { | ||||||
|  |     val clickModifier = if (enabled) { | ||||||
|  |         Modifier.clickable { onClick() } | ||||||
|  |     } else { | ||||||
|  |         Modifier | ||||||
|  |     } | ||||||
|  |     Row( | ||||||
|  |         modifier = clickModifier.then(modifier), | ||||||
|  |         verticalAlignment = Alignment.CenterVertically, | ||||||
|  |     ) { | ||||||
|  |         val alpha by remember { | ||||||
|  |             derivedStateOf { | ||||||
|  |                 if (enabled) 1f else 0.38f | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         Column( | ||||||
|  |             modifier = Modifier.padding(SpacingMedium) | ||||||
|  |         ) { | ||||||
|  |             Text( | ||||||
|  |                 text = title, | ||||||
|  |                 modifier = Modifier.alpha(alpha), | ||||||
|  |             ) | ||||||
|  |             Spacer(modifier = Modifier.padding(SpacingExtraSmall)) | ||||||
|  |             Text( | ||||||
|  |                 text = summary, | ||||||
|  |                 style = MaterialTheme.typography.labelSmall, | ||||||
|  |                 modifier = Modifier.alpha(alpha * 0.6f), | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) | ||||||
|  | @Composable | ||||||
|  | private fun IrreversiblePreferenceComponentPreview() { | ||||||
|  |     val title = "Wipe cached metadata" | ||||||
|  |     val summary = "Remove all cached webpage data" | ||||||
|  |     AppTheme { | ||||||
|  |         Column { | ||||||
|  |  | ||||||
|  |             IrreversiblePreferenceComponent( | ||||||
|  |                 title = title, | ||||||
|  |                 summary = summary, | ||||||
|  |                 onClick = {}, | ||||||
|  |                 modifier = Modifier.fillMaxWidth() | ||||||
|  |             ) | ||||||
|  |             IrreversiblePreferenceComponent( | ||||||
|  |                 title = title, | ||||||
|  |                 summary = summary, | ||||||
|  |                 onClick = {}, | ||||||
|  |                 modifier = Modifier.fillMaxWidth(), | ||||||
|  |                 enabled = false | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,73 @@ | |||||||
|  | package org.schabi.newpipe.settings.components.switch_preference | ||||||
|  |  | ||||||
|  | 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.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.draw.alpha | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  | import org.schabi.newpipe.ui.theme.SizeTokens.SpacingExtraSmall | ||||||
|  | import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun SwitchPreferenceComponent( | ||||||
|  |     title: String, | ||||||
|  |     summary: String, | ||||||
|  |     isChecked: Boolean, | ||||||
|  |     onCheckedChange: (Boolean) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     Row( | ||||||
|  |         modifier = modifier, | ||||||
|  |         horizontalArrangement = Arrangement.SpaceBetween, | ||||||
|  |         verticalAlignment = Alignment.CenterVertically | ||||||
|  |     ) { | ||||||
|  |         Column( | ||||||
|  |             horizontalAlignment = Alignment.Start, | ||||||
|  |             verticalArrangement = Arrangement.Center, | ||||||
|  |             modifier = Modifier.padding(SpacingMedium) | ||||||
|  |         ) { | ||||||
|  |             Text(text = title) | ||||||
|  |             Spacer(modifier = Modifier.padding(SpacingExtraSmall)) | ||||||
|  |             Text( | ||||||
|  |                 text = summary, | ||||||
|  |                 style = MaterialTheme.typography.labelSmall, | ||||||
|  |                 modifier = Modifier.alpha(0.6f) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Switch( | ||||||
|  |             checked = isChecked, | ||||||
|  |             onCheckedChange = onCheckedChange, | ||||||
|  |             modifier = Modifier.padding(SpacingMedium) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) | ||||||
|  | @Composable | ||||||
|  | private fun SwitchPreferenceComponentPreview() { | ||||||
|  |     val title = "Watch history" | ||||||
|  |     val subtitle = "Keep track of watched videos" | ||||||
|  |     var isChecked = false | ||||||
|  |     AppTheme { | ||||||
|  |         SwitchPreferenceComponent( | ||||||
|  |             title = title, | ||||||
|  |             summary = subtitle, | ||||||
|  |             isChecked = isChecked, | ||||||
|  |             onCheckedChange = { | ||||||
|  |                 isChecked = it | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,138 @@ | |||||||
|  | package org.schabi.newpipe.settings.dependency_injection | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import dagger.Module | ||||||
|  | import dagger.Provides | ||||||
|  | import dagger.hilt.InstallIn | ||||||
|  | import dagger.hilt.android.qualifiers.ApplicationContext | ||||||
|  | import dagger.hilt.components.SingletonComponent | ||||||
|  | import org.schabi.newpipe.database.history.dao.SearchHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.history.dao.StreamHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamStateDAO | ||||||
|  | import org.schabi.newpipe.error.usecases.OpenErrorActivity | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepository | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepositoryImpl | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteCompleteSearchHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteCompleteStreamStateHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeletePlaybackStates | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteWatchHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.RemoveOrphanedRecords | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.get_preference.GetPreference | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.get_preference.GetPreferenceImpl | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.update_preference.UpdatePreference | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.update_preference.UpdatePreferenceImpl | ||||||
|  | import javax.inject.Singleton | ||||||
|  |  | ||||||
|  | @Module | ||||||
|  | @InstallIn(SingletonComponent::class) | ||||||
|  | object SettingsModule { | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideGetBooleanPreference( | ||||||
|  |         sharedPreferences: SharedPreferences, | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): GetPreference<Boolean> = GetPreferenceImpl(sharedPreferences, context) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideGetStringPreference( | ||||||
|  |         sharedPreferences: SharedPreferences, | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): GetPreference<String> = GetPreferenceImpl(sharedPreferences, context) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideUpdateBooleanPreference( | ||||||
|  |         sharedPreferences: SharedPreferences, | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): UpdatePreference<Boolean> = UpdatePreferenceImpl(context, sharedPreferences) { key, value -> | ||||||
|  |         putBoolean( | ||||||
|  |             key, | ||||||
|  |             value | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideUpdateStringPreference( | ||||||
|  |         sharedPreferences: SharedPreferences, | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): UpdatePreference<String> = UpdatePreferenceImpl(context, sharedPreferences) { key, value -> | ||||||
|  |         putString( | ||||||
|  |             key, | ||||||
|  |             value | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideUpdateIntPreference( | ||||||
|  |         sharedPreferences: SharedPreferences, | ||||||
|  |         @ApplicationContext context: Context, | ||||||
|  |     ): UpdatePreference<Int> = UpdatePreferenceImpl(context, sharedPreferences) { key, value -> | ||||||
|  |         putInt(key, value) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideHistoryRecordRepository( | ||||||
|  |         streamStateDao: StreamStateDAO, | ||||||
|  |         streamHistoryDAO: StreamHistoryDAO, | ||||||
|  |         streamDAO: StreamDAO, | ||||||
|  |         searchHistoryDAO: SearchHistoryDAO, | ||||||
|  |     ): HistoryRecordRepository = HistoryRecordRepositoryImpl( | ||||||
|  |         streamStateDao = streamStateDao, | ||||||
|  |         streamHistoryDAO = streamHistoryDAO, | ||||||
|  |         streamDAO = streamDAO, | ||||||
|  |         searchHistoryDAO = searchHistoryDAO, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideDeletePlaybackStatesUseCase( | ||||||
|  |         historyRecordRepository: HistoryRecordRepository, | ||||||
|  |     ): DeletePlaybackStates = DeletePlaybackStates( | ||||||
|  |         historyRecordRepository = historyRecordRepository, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideDeleteWholeStreamHistoryUseCase( | ||||||
|  |         historyRecordRepository: HistoryRecordRepository, | ||||||
|  |     ): DeleteCompleteStreamStateHistory = DeleteCompleteStreamStateHistory( | ||||||
|  |         historyRecordRepository = historyRecordRepository, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideRemoveOrphanedRecordsUseCase( | ||||||
|  |         historyRecordRepository: HistoryRecordRepository, | ||||||
|  |     ): RemoveOrphanedRecords = RemoveOrphanedRecords( | ||||||
|  |         historyRecordRepository = historyRecordRepository, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideDeleteCompleteSearchHistoryUseCase( | ||||||
|  |         historyRecordRepository: HistoryRecordRepository, | ||||||
|  |     ): DeleteCompleteSearchHistory = DeleteCompleteSearchHistory( | ||||||
|  |         historyRecordRepository = historyRecordRepository, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     @Provides | ||||||
|  |     @Singleton | ||||||
|  |     fun provideDeleteWatchHistoryUseCase( | ||||||
|  |         deletePlaybackStates: DeletePlaybackStates, | ||||||
|  |         deleteCompleteStreamStateHistory: DeleteCompleteStreamStateHistory, | ||||||
|  |         removeOrphanedRecords: RemoveOrphanedRecords, | ||||||
|  |         openErrorActivity: OpenErrorActivity, | ||||||
|  |     ): DeleteWatchHistory = DeleteWatchHistory( | ||||||
|  |         deletePlaybackStates = deletePlaybackStates, | ||||||
|  |         deleteCompleteStreamStateHistory = deleteCompleteStreamStateHistory, | ||||||
|  |         removeOrphanedRecords = removeOrphanedRecords, | ||||||
|  |         openErrorActivity = openErrorActivity | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.repositories | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  |  | ||||||
|  | interface HistoryRecordRepository { | ||||||
|  |     fun deleteCompleteStreamState(dispatcher: CoroutineDispatcher): Flow<Int> | ||||||
|  |     fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow<Int> | ||||||
|  |     fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow<Int> | ||||||
|  |     fun deleteCompleteSearchHistory(dispatcher: CoroutineDispatcher): Flow<Int> | ||||||
|  | } | ||||||
| @@ -0,0 +1,64 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.repositories | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.flow | ||||||
|  | import kotlinx.coroutines.flow.flowOn | ||||||
|  | import kotlinx.coroutines.flow.update | ||||||
|  | import org.schabi.newpipe.database.history.model.SearchHistoryEntry | ||||||
|  | import org.schabi.newpipe.database.history.model.StreamHistoryEntity | ||||||
|  | import org.schabi.newpipe.database.stream.model.StreamEntity | ||||||
|  | import org.schabi.newpipe.database.stream.model.StreamStateEntity | ||||||
|  |  | ||||||
|  | class HistoryRecordRepositoryFake : HistoryRecordRepository { | ||||||
|  |     private val _searchHistory: MutableStateFlow<List<SearchHistoryEntry>> = MutableStateFlow( | ||||||
|  |         emptyList() | ||||||
|  |     ) | ||||||
|  |     val searchHistory = _searchHistory.asStateFlow() | ||||||
|  |     private val _streamHistory = MutableStateFlow<List<StreamHistoryEntity>>(emptyList()) | ||||||
|  |     val streamHistory = _streamHistory.asStateFlow() | ||||||
|  |     private val _streams = MutableStateFlow<List<StreamEntity>>(emptyList()) | ||||||
|  |     val streams = _streams.asStateFlow() | ||||||
|  |     private val _streamStates = MutableStateFlow<List<StreamStateEntity>>(emptyList()) | ||||||
|  |     val streamStates = _streamStates.asStateFlow() | ||||||
|  |  | ||||||
|  |     override fun deleteCompleteStreamState(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val count = streamStates.value.size | ||||||
|  |         _streamStates.update { | ||||||
|  |             emptyList() | ||||||
|  |         } | ||||||
|  |         emit(count) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val count = streamHistory.value.size | ||||||
|  |         _streamHistory.update { | ||||||
|  |             emptyList() | ||||||
|  |         } | ||||||
|  |         emit(count) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val orphanedStreams = streams.value.filter { stream -> | ||||||
|  |             !streamHistory.value.any { it.streamUid == stream.uid } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         val deletedCount = orphanedStreams.size | ||||||
|  |  | ||||||
|  |         _streams.update { oldStreams -> | ||||||
|  |             oldStreams.filter { it !in orphanedStreams } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         emit(deletedCount) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun deleteCompleteSearchHistory(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val count = searchHistory.value.size | ||||||
|  |         _searchHistory.update { | ||||||
|  |             emptyList() | ||||||
|  |         } | ||||||
|  |         emit(count) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.repositories | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.flow | ||||||
|  | import kotlinx.coroutines.flow.flowOn | ||||||
|  | import org.schabi.newpipe.database.history.dao.SearchHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.history.dao.StreamHistoryDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamDAO | ||||||
|  | import org.schabi.newpipe.database.stream.dao.StreamStateDAO | ||||||
|  |  | ||||||
|  | class HistoryRecordRepositoryImpl( | ||||||
|  |     private val streamStateDao: StreamStateDAO, | ||||||
|  |     private val streamHistoryDAO: StreamHistoryDAO, | ||||||
|  |     private val streamDAO: StreamDAO, | ||||||
|  |     private val searchHistoryDAO: SearchHistoryDAO, | ||||||
|  | ) : HistoryRecordRepository { | ||||||
|  |     override fun deleteCompleteStreamState(dispatcher: CoroutineDispatcher): Flow<Int> = | ||||||
|  |         flow { | ||||||
|  |             val deletedCount = streamStateDao.deleteAll() | ||||||
|  |             emit(deletedCount) | ||||||
|  |         }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow<Int> = | ||||||
|  |         flow { | ||||||
|  |             val deletedCount = streamHistoryDAO.deleteAll() | ||||||
|  |             emit(deletedCount) | ||||||
|  |         }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val deletedCount = streamDAO.deleteOrphans() | ||||||
|  |         emit(deletedCount) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  |  | ||||||
|  |     override fun deleteCompleteSearchHistory(dispatcher: CoroutineDispatcher): Flow<Int> = flow { | ||||||
|  |         val deletedCount = searchHistoryDAO.deleteAll() | ||||||
|  |         emit(deletedCount) | ||||||
|  |     }.flowOn(dispatcher) | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.catch | ||||||
|  | import kotlinx.coroutines.flow.take | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepository | ||||||
|  |  | ||||||
|  | class DeleteCompleteSearchHistory( | ||||||
|  |     private val historyRecordRepository: HistoryRecordRepository, | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke( | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |         onError: (Throwable) -> Unit, | ||||||
|  |         onSuccess: () -> Unit, | ||||||
|  |     ) = historyRecordRepository.deleteCompleteSearchHistory(dispatcher).catch { error -> | ||||||
|  |         onError(error) | ||||||
|  |     }.take(1).collect { | ||||||
|  |         onSuccess() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.catch | ||||||
|  | import kotlinx.coroutines.flow.take | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepository | ||||||
|  |  | ||||||
|  | class DeleteCompleteStreamStateHistory( | ||||||
|  |     private val historyRecordRepository: HistoryRecordRepository, | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke( | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |         onError: (Throwable) -> Unit, | ||||||
|  |         onSuccess: () -> Unit, | ||||||
|  |     ) = historyRecordRepository.deleteWholeStreamHistory(dispatcher).catch { | ||||||
|  |         onError(it) | ||||||
|  |     }.take(1).collect { | ||||||
|  |         onSuccess() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.catch | ||||||
|  | import kotlinx.coroutines.flow.take | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepository | ||||||
|  |  | ||||||
|  | class DeletePlaybackStates( | ||||||
|  |     private val historyRecordRepository: HistoryRecordRepository, | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke( | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |         onError: (Throwable) -> Unit, | ||||||
|  |         onSuccess: () -> Unit, | ||||||
|  |     ) = historyRecordRepository.deleteCompleteStreamState(dispatcher).catch { | ||||||
|  |         onError(it) | ||||||
|  |     }.take(1).collect { | ||||||
|  |         onSuccess() | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,69 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.coroutineScope | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo | ||||||
|  | import org.schabi.newpipe.error.UserAction | ||||||
|  | import org.schabi.newpipe.error.usecases.OpenErrorActivity | ||||||
|  |  | ||||||
|  | class DeleteWatchHistory( | ||||||
|  |     private val deletePlaybackStates: DeletePlaybackStates, | ||||||
|  |     private val deleteCompleteStreamStateHistory: DeleteCompleteStreamStateHistory, | ||||||
|  |     private val removeOrphanedRecords: RemoveOrphanedRecords, | ||||||
|  |     private val openErrorActivity: OpenErrorActivity, | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke( | ||||||
|  |         onDeletePlaybackStates: () -> Unit, | ||||||
|  |         onDeleteWholeStreamHistory: () -> Unit, | ||||||
|  |         onRemoveOrphanedRecords: () -> Unit, | ||||||
|  |         dispatcher: CoroutineDispatcher = Dispatchers.IO, | ||||||
|  |     ) = coroutineScope { | ||||||
|  |         launch { | ||||||
|  |             deletePlaybackStates( | ||||||
|  |                 dispatcher, | ||||||
|  |                 onError = { error -> | ||||||
|  |                     openErrorActivity( | ||||||
|  |                         ErrorInfo( | ||||||
|  |                             error, | ||||||
|  |                             UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                             "Delete playback states" | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 }, | ||||||
|  |                 onSuccess = onDeletePlaybackStates | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         launch { | ||||||
|  |             deleteCompleteStreamStateHistory( | ||||||
|  |                 dispatcher, | ||||||
|  |                 onError = { error -> | ||||||
|  |                     openErrorActivity( | ||||||
|  |                         ErrorInfo( | ||||||
|  |                             error, | ||||||
|  |                             UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                             "Delete from history" | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 }, | ||||||
|  |                 onSuccess = onDeleteWholeStreamHistory | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         launch { | ||||||
|  |             removeOrphanedRecords( | ||||||
|  |                 dispatcher, | ||||||
|  |                 onError = { error -> | ||||||
|  |                     openErrorActivity( | ||||||
|  |                         ErrorInfo( | ||||||
|  |                             error, | ||||||
|  |                             UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                             "Clear orphaned records" | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                 }, | ||||||
|  |                 onSuccess = onRemoveOrphanedRecords | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.CoroutineDispatcher | ||||||
|  | import kotlinx.coroutines.flow.catch | ||||||
|  | import kotlinx.coroutines.flow.take | ||||||
|  | import org.schabi.newpipe.settings.domain.repositories.HistoryRecordRepository | ||||||
|  |  | ||||||
|  | class RemoveOrphanedRecords( | ||||||
|  |     private val historyRecordRepository: HistoryRecordRepository, | ||||||
|  | ) { | ||||||
|  |     suspend operator fun invoke( | ||||||
|  |         dispatcher: CoroutineDispatcher, | ||||||
|  |         onError: (Throwable) -> Unit, | ||||||
|  |         onSuccess: () -> Unit, | ||||||
|  |     ) = | ||||||
|  |         historyRecordRepository.removeOrphanedRecords(dispatcher).catch { | ||||||
|  |             onError(it) | ||||||
|  |         }.take(1).collect { | ||||||
|  |             onSuccess() | ||||||
|  |         } | ||||||
|  | } | ||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.get_preference | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  |  | ||||||
|  | fun interface GetPreference<T> { | ||||||
|  |     operator fun invoke(key: Int, defaultValue: T): Flow<T> | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.get_preference | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.map | ||||||
|  |  | ||||||
|  | class GetPreferenceFake<T>( | ||||||
|  |     private val preferences: MutableStateFlow<MutableMap<Int, T>>, | ||||||
|  | ) : GetPreference<T> { | ||||||
|  |     override fun invoke(key: Int, defaultValue: T): Flow<T> { | ||||||
|  |         return preferences.asStateFlow().map { | ||||||
|  |             it[key] ?: defaultValue | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,49 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.get_preference | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import kotlinx.coroutines.cancel | ||||||
|  | import kotlinx.coroutines.channels.awaitClose | ||||||
|  | import kotlinx.coroutines.flow.Flow | ||||||
|  | import kotlinx.coroutines.flow.callbackFlow | ||||||
|  |  | ||||||
|  | class GetPreferenceImpl<T>( | ||||||
|  |     private val sharedPreferences: SharedPreferences, | ||||||
|  |     private val context: Context, | ||||||
|  | ) : GetPreference<T> { | ||||||
|  |     override fun invoke(key: Int, defaultValue: T): Flow<T> { | ||||||
|  |         val keyString = context.getString(key) | ||||||
|  |         return sharedPreferences.getFlowForKey(keyString, defaultValue) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun <T> SharedPreferences.getFlowForKey(key: String, defaultValue: T) = callbackFlow { | ||||||
|  |         val listener = | ||||||
|  |             SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey -> | ||||||
|  |                 if (key == changedKey) { | ||||||
|  |                     val updated = getPreferenceValue(key, defaultValue) | ||||||
|  |                     trySend(updated) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         registerOnSharedPreferenceChangeListener(listener) | ||||||
|  |         println("Current value for $key: ${getPreferenceValue(key, defaultValue)}") | ||||||
|  |         if (contains(key)) { | ||||||
|  |             send(getPreferenceValue(key, defaultValue)) | ||||||
|  |         } | ||||||
|  |         awaitClose { | ||||||
|  |             unregisterOnSharedPreferenceChangeListener(listener) | ||||||
|  |             cancel() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @Suppress("UNCHECKED_CAST") | ||||||
|  |     private fun <T> SharedPreferences.getPreferenceValue(key: String, defaultValue: T): T { | ||||||
|  |         return when (defaultValue) { | ||||||
|  |             is Boolean -> getBoolean(key, defaultValue) as T | ||||||
|  |             is Int -> getInt(key, defaultValue) as T | ||||||
|  |             is Long -> getLong(key, defaultValue) as T | ||||||
|  |             is Float -> getFloat(key, defaultValue) as T | ||||||
|  |             is String -> getString(key, defaultValue) as T | ||||||
|  |             else -> throw IllegalArgumentException("Unsupported type") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,5 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.update_preference | ||||||
|  |  | ||||||
|  | fun interface UpdatePreference<T> { | ||||||
|  |     suspend operator fun invoke(key: Int, value: T) | ||||||
|  | } | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.update_preference | ||||||
|  |  | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.update | ||||||
|  |  | ||||||
|  | class UpdatePreferenceFake<T>( | ||||||
|  |     private val preferences: MutableStateFlow<MutableMap<Int, T>>, | ||||||
|  | ) : UpdatePreference<T> { | ||||||
|  |     override suspend fun invoke(key: Int, value: T) { | ||||||
|  |         preferences.update { | ||||||
|  |             it.apply { | ||||||
|  |                 put(key, value) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | package org.schabi.newpipe.settings.domain.usecases.update_preference | ||||||
|  |  | ||||||
|  | import android.content.Context | ||||||
|  | import android.content.SharedPreferences | ||||||
|  | import androidx.core.content.edit | ||||||
|  |  | ||||||
|  | class UpdatePreferenceImpl<T>( | ||||||
|  |     private val context: Context, | ||||||
|  |     private val sharedPreferences: SharedPreferences, | ||||||
|  |     private val setter: SharedPreferences.Editor.(String, T) -> SharedPreferences.Editor, | ||||||
|  | ) : UpdatePreference<T> { | ||||||
|  |     override suspend operator fun invoke(key: Int, value: T) { | ||||||
|  |         val stringKey = context.getString(key) | ||||||
|  |         sharedPreferences.edit { | ||||||
|  |             setter(stringKey, value) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,39 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.compose.foundation.layout.fillMaxSize | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.platform.ComposeView | ||||||
|  | import androidx.compose.ui.platform.ViewCompositionStrategy | ||||||
|  | import androidx.core.os.bundleOf | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import org.schabi.newpipe.fragments.list.comments.CommentsFragment | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  | import org.schabi.newpipe.util.KEY_SERVICE_ID | ||||||
|  | import org.schabi.newpipe.util.KEY_URL | ||||||
|  |  | ||||||
|  | class HistoryCacheFragment : Fragment() { | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle?, | ||||||
|  |     ) = ComposeView(requireContext()).apply { | ||||||
|  |         setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||||||
|  |         setContent { | ||||||
|  |             AppTheme { | ||||||
|  |                 HistoryCacheSettingsScreen( | ||||||
|  |                     modifier = Modifier.fillMaxSize() | ||||||
|  |                 ) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         @JvmStatic | ||||||
|  |         fun getInstance(serviceId: Int, url: String?) = CommentsFragment().apply { | ||||||
|  |             arguments = bundleOf(KEY_SERVICE_ID to serviceId, KEY_URL to url) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,137 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.layout.Arrangement | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | 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 | ||||||
|  | import androidx.compose.material3.HorizontalDivider | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.SnackbarHost | ||||||
|  | import androidx.compose.material3.SnackbarHostState | ||||||
|  | import androidx.compose.material3.Surface | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.runtime.collectAsState | ||||||
|  | import androidx.compose.runtime.getValue | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.hilt.navigation.compose.hiltViewModel | ||||||
|  | import org.schabi.newpipe.R | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.components.CachePreferencesComponent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.components.HistoryPreferencesComponent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowClearWatchHistorySnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowDeletePlaybackSnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowDeleteSearchHistorySnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowReCaptchaCookiesSnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowWipeCachedMetadataSnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.state.SwitchPreferencesUiState | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun HistoryCacheSettingsScreen( | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  |     viewModel: HistoryCacheSettingsViewModel = hiltViewModel(), | ||||||
|  | ) { | ||||||
|  |     val snackBarHostState = remember { SnackbarHostState() } | ||||||
|  |     val playBackPositionsDeleted = stringResource(R.string.watch_history_states_deleted) | ||||||
|  |     val watchHistoryDeleted = stringResource(R.string.watch_history_deleted) | ||||||
|  |     val wipeCachedMetadataSnackbar = stringResource(R.string.metadata_cache_wipe_complete_notice) | ||||||
|  |     val deleteSearchHistory = stringResource(R.string.search_history_deleted) | ||||||
|  |     val clearReCaptchaCookiesSnackbar = stringResource(R.string.recaptcha_cookies_cleared) | ||||||
|  |  | ||||||
|  |     LaunchedEffect(key1 = true) { | ||||||
|  |         viewModel.onInit() | ||||||
|  |         viewModel.eventFlow.collect { event -> | ||||||
|  |             val message = when (event) { | ||||||
|  |                 is ShowDeletePlaybackSnackbar -> playBackPositionsDeleted | ||||||
|  |                 is ShowClearWatchHistorySnackbar -> watchHistoryDeleted | ||||||
|  |                 is ShowWipeCachedMetadataSnackbar -> wipeCachedMetadataSnackbar | ||||||
|  |                 is ShowDeleteSearchHistorySnackbar -> deleteSearchHistory | ||||||
|  |                 is ShowReCaptchaCookiesSnackbar -> clearReCaptchaCookiesSnackbar | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             snackBarHostState.showSnackbar(message) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val switchPreferencesUiState by viewModel.switchState.collectAsState() | ||||||
|  |     val recaptchaCookiesEnabled by viewModel.captchaCookies.collectAsState() | ||||||
|  |     HistoryCacheComponent( | ||||||
|  |         switchPreferences = switchPreferencesUiState, | ||||||
|  |         recaptchaCookiesEnabled = recaptchaCookiesEnabled, | ||||||
|  |         onEvent = { viewModel.onEvent(it) }, | ||||||
|  |         snackBarHostState = snackBarHostState, | ||||||
|  |         modifier = modifier | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun HistoryCacheComponent( | ||||||
|  |     switchPreferences: SwitchPreferencesUiState, | ||||||
|  |     recaptchaCookiesEnabled: Boolean, | ||||||
|  |     onEvent: (HistoryCacheEvent) -> Unit, | ||||||
|  |     snackBarHostState: SnackbarHostState, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     Scaffold( | ||||||
|  |         modifier = modifier, | ||||||
|  |         snackbarHost = { | ||||||
|  |             SnackbarHost(snackBarHostState) | ||||||
|  |         } | ||||||
|  |     ) { padding -> | ||||||
|  |         val scrollState = rememberScrollState() | ||||||
|  |         Column( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .padding(padding) | ||||||
|  |                 .verticalScroll(scrollState), | ||||||
|  |             horizontalAlignment = Alignment.CenterHorizontally, | ||||||
|  |             verticalArrangement = Arrangement.Center, | ||||||
|  |         ) { | ||||||
|  |             HistoryPreferencesComponent( | ||||||
|  |                 state = switchPreferences, | ||||||
|  |                 onEvent = { key, value -> | ||||||
|  |                     onEvent(HistoryCacheEvent.OnUpdateBooleanPreference(key, value)) | ||||||
|  |                 }, | ||||||
|  |                 modifier = Modifier.fillMaxWidth(), | ||||||
|  |             ) | ||||||
|  |             HorizontalDivider(Modifier.fillMaxWidth()) | ||||||
|  |             CachePreferencesComponent( | ||||||
|  |                 recaptchaCookiesEnabled = recaptchaCookiesEnabled, | ||||||
|  |                 onEvent = { onEvent(it) }, | ||||||
|  |                 modifier = Modifier.fillMaxWidth() | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(showBackground = true) | ||||||
|  | @Composable | ||||||
|  | private fun HistoryCacheComponentPreview() { | ||||||
|  |     val state by remember { | ||||||
|  |         mutableStateOf( | ||||||
|  |             SwitchPreferencesUiState() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     AppTheme( | ||||||
|  |         useDarkTheme = false | ||||||
|  |     ) { | ||||||
|  |         Surface { | ||||||
|  |             HistoryCacheComponent( | ||||||
|  |                 switchPreferences = state, | ||||||
|  |                 recaptchaCookiesEnabled = false, | ||||||
|  |                 onEvent = { | ||||||
|  |                 }, | ||||||
|  |                 snackBarHostState = SnackbarHostState(), | ||||||
|  |                 modifier = Modifier.fillMaxSize() | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,204 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache | ||||||
|  |  | ||||||
|  | import androidx.lifecycle.ViewModel | ||||||
|  | import androidx.lifecycle.viewModelScope | ||||||
|  | import dagger.hilt.android.lifecycle.HiltViewModel | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.flow.MutableSharedFlow | ||||||
|  | import kotlinx.coroutines.flow.MutableStateFlow | ||||||
|  | import kotlinx.coroutines.flow.StateFlow | ||||||
|  | import kotlinx.coroutines.flow.asSharedFlow | ||||||
|  | import kotlinx.coroutines.flow.asStateFlow | ||||||
|  | import kotlinx.coroutines.flow.update | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import org.schabi.newpipe.DownloaderImpl | ||||||
|  | import org.schabi.newpipe.R | ||||||
|  | import org.schabi.newpipe.error.ErrorInfo | ||||||
|  | import org.schabi.newpipe.error.ReCaptchaActivity | ||||||
|  | import org.schabi.newpipe.error.UserAction | ||||||
|  | import org.schabi.newpipe.error.usecases.OpenErrorActivity | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteCompleteSearchHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteCompleteStreamStateHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.DeleteWatchHistory | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.get_preference.GetPreference | ||||||
|  | import org.schabi.newpipe.settings.domain.usecases.update_preference.UpdatePreference | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnClickClearSearchHistory | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnClickClearWatchHistory | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnClickDeletePlaybackPositions | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnClickReCaptchaCookies | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnClickWipeCachedMetadata | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent.OnUpdateBooleanPreference | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowClearWatchHistorySnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowDeletePlaybackSnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowDeleteSearchHistorySnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheUiEvent.ShowWipeCachedMetadataSnackbar | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.state.SwitchPreferencesUiState | ||||||
|  | import org.schabi.newpipe.util.InfoCache | ||||||
|  | import javax.inject.Inject | ||||||
|  |  | ||||||
|  | @HiltViewModel | ||||||
|  | class HistoryCacheSettingsViewModel @Inject constructor( | ||||||
|  |     private val updateStringPreference: UpdatePreference<String>, | ||||||
|  |     private val updateBooleanPreference: UpdatePreference<Boolean>, | ||||||
|  |     private val getStringPreference: GetPreference<String>, | ||||||
|  |     private val getBooleanPreference: GetPreference<Boolean>, | ||||||
|  |     private val deleteWatchHistory: DeleteWatchHistory, | ||||||
|  |     private val deleteCompleteStreamStateHistory: DeleteCompleteStreamStateHistory, | ||||||
|  |     private val deleteCompleteSearchHistory: DeleteCompleteSearchHistory, | ||||||
|  |     private val openErrorActivity: OpenErrorActivity, | ||||||
|  | ) : ViewModel() { | ||||||
|  |     private val _switchState = MutableStateFlow(SwitchPreferencesUiState()) | ||||||
|  |     val switchState: StateFlow<SwitchPreferencesUiState> = _switchState.asStateFlow() | ||||||
|  |  | ||||||
|  |     private val _captchaCookies = MutableStateFlow(false) | ||||||
|  |     val captchaCookies: StateFlow<Boolean> = _captchaCookies.asStateFlow() | ||||||
|  |  | ||||||
|  |     private val _eventFlow = MutableSharedFlow<HistoryCacheUiEvent>() | ||||||
|  |     val eventFlow = _eventFlow.asSharedFlow() | ||||||
|  |  | ||||||
|  |     fun onInit() { | ||||||
|  |  | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             val flow = getStringPreference(R.string.recaptcha_cookies_key, "") | ||||||
|  |             flow.collect { preference -> | ||||||
|  |                 _captchaCookies.update { | ||||||
|  |                     preference.isNotEmpty() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             getBooleanPreference(R.string.enable_watch_history_key, true).collect { preference -> | ||||||
|  |                 _switchState.update { oldState -> | ||||||
|  |                     oldState.copy( | ||||||
|  |                         watchHistoryEnabled = preference | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             getBooleanPreference(R.string.enable_playback_resume_key, true).collect { preference -> | ||||||
|  |                 _switchState.update { oldState -> | ||||||
|  |                     oldState.copy( | ||||||
|  |                         resumePlaybackEnabled = preference | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             getBooleanPreference( | ||||||
|  |                 R.string.enable_playback_state_lists_key, | ||||||
|  |                 true | ||||||
|  |             ).collect { preference -> | ||||||
|  |                 _switchState.update { oldState -> | ||||||
|  |                     oldState.copy( | ||||||
|  |                         positionsInListsEnabled = preference | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         viewModelScope.launch { | ||||||
|  |             getBooleanPreference(R.string.enable_search_history_key, true).collect { preference -> | ||||||
|  |                 _switchState.update { oldState -> | ||||||
|  |                     oldState.copy( | ||||||
|  |                         searchHistoryEnabled = preference | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun onEvent(event: HistoryCacheEvent) { | ||||||
|  |         when (event) { | ||||||
|  |             is OnUpdateBooleanPreference -> { | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     updateBooleanPreference(event.key, event.isEnabled) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             is OnClickWipeCachedMetadata -> { | ||||||
|  |                 InfoCache.getInstance().clearCache() | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     _eventFlow.emit(ShowWipeCachedMetadataSnackbar) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             is OnClickClearWatchHistory -> { | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     deleteWatchHistory( | ||||||
|  |                         onDeletePlaybackStates = { | ||||||
|  |                             viewModelScope.launch { | ||||||
|  |                                 _eventFlow.emit(ShowDeletePlaybackSnackbar) | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         onDeleteWholeStreamHistory = { | ||||||
|  |                             viewModelScope.launch { | ||||||
|  |                                 _eventFlow.emit(ShowClearWatchHistorySnackbar) | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         onRemoveOrphanedRecords = { | ||||||
|  |                             // TODO: ask why original in android fragments did nothing | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             is OnClickDeletePlaybackPositions -> { | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     deleteCompleteStreamStateHistory( | ||||||
|  |                         Dispatchers.IO, | ||||||
|  |                         onError = { error -> | ||||||
|  |                             openErrorActivity( | ||||||
|  |                                 ErrorInfo( | ||||||
|  |                                     error, | ||||||
|  |                                     UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                                     "Delete playback states" | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                         }, | ||||||
|  |                         onSuccess = { | ||||||
|  |                             viewModelScope.launch { | ||||||
|  |                                 _eventFlow.emit(ShowDeletePlaybackSnackbar) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             is OnClickClearSearchHistory -> { | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     deleteCompleteSearchHistory( | ||||||
|  |                         dispatcher = Dispatchers.IO, | ||||||
|  |                         onError = { error -> | ||||||
|  |                             openErrorActivity( | ||||||
|  |                                 ErrorInfo( | ||||||
|  |                                     error, | ||||||
|  |                                     UserAction.DELETE_FROM_HISTORY, | ||||||
|  |                                     "Delete search history" | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                         }, | ||||||
|  |                         onSuccess = { | ||||||
|  |                             viewModelScope.launch { | ||||||
|  |                                 _eventFlow.emit(ShowDeleteSearchHistorySnackbar) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             is OnClickReCaptchaCookies -> { | ||||||
|  |                 viewModelScope.launch { | ||||||
|  |                     updateStringPreference(event.key, "") | ||||||
|  |                     DownloaderImpl.getInstance() | ||||||
|  |                         .setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, "") | ||||||
|  |                     _eventFlow.emit(ShowWipeCachedMetadataSnackbar) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,174 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache.components | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.material3.AlertDialog | ||||||
|  | import androidx.compose.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.material3.TextButton | ||||||
|  | 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.Modifier | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import org.schabi.newpipe.R | ||||||
|  | import org.schabi.newpipe.settings.components.irreversible_preference.IrreversiblePreferenceComponent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.events.HistoryCacheEvent | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  | import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun CachePreferencesComponent( | ||||||
|  |     recaptchaCookiesEnabled: Boolean, | ||||||
|  |     onEvent: (HistoryCacheEvent) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     var dialogTitle by remember { mutableStateOf("") } | ||||||
|  |     var dialogOnClick by remember { mutableStateOf({}) } | ||||||
|  |     var isDialogVisible by remember { mutableStateOf(false) } | ||||||
|  |  | ||||||
|  |     val deleteViewHistory = stringResource(id = R.string.delete_view_history_alert) | ||||||
|  |     val deletePlayBacks = stringResource(id = R.string.delete_playback_states_alert) | ||||||
|  |     val deleteSearchHistory = stringResource(id = R.string.delete_search_history_alert) | ||||||
|  |  | ||||||
|  |     val onOpenDialog: (String, HistoryCacheEvent) -> Unit = { title, eventType -> | ||||||
|  |         dialogTitle = title | ||||||
|  |         isDialogVisible = true | ||||||
|  |         dialogOnClick = { | ||||||
|  |             onEvent(eventType) | ||||||
|  |             isDialogVisible = false | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier, | ||||||
|  |     ) { | ||||||
|  |         Text( | ||||||
|  |             stringResource(id = R.string.settings_category_clear_data_title), | ||||||
|  |             style = MaterialTheme.typography.titleMedium, | ||||||
|  |             modifier = Modifier.padding(SpacingMedium) | ||||||
|  |         ) | ||||||
|  |         IrreversiblePreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.metadata_cache_wipe_title), | ||||||
|  |             summary = stringResource(id = R.string.metadata_cache_wipe_summary), | ||||||
|  |             onClick = { | ||||||
|  |                 onEvent(HistoryCacheEvent.OnClickWipeCachedMetadata(R.string.metadata_cache_wipe_key)) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         IrreversiblePreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.clear_views_history_title), | ||||||
|  |             summary = stringResource(id = R.string.clear_views_history_summary), | ||||||
|  |             onClick = { | ||||||
|  |                 onOpenDialog( | ||||||
|  |                     deleteViewHistory, | ||||||
|  |                     HistoryCacheEvent.OnClickClearWatchHistory(R.string.clear_views_history_key) | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         IrreversiblePreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.clear_playback_states_title), | ||||||
|  |             summary = stringResource(id = R.string.clear_playback_states_summary), | ||||||
|  |             onClick = { | ||||||
|  |                 onOpenDialog( | ||||||
|  |                     deletePlayBacks, | ||||||
|  |                     HistoryCacheEvent.OnClickDeletePlaybackPositions(R.string.clear_playback_states_key) | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         IrreversiblePreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.clear_search_history_title), | ||||||
|  |             summary = stringResource(id = R.string.clear_search_history_summary), | ||||||
|  |             onClick = { | ||||||
|  |                 onOpenDialog( | ||||||
|  |                     deleteSearchHistory, | ||||||
|  |                     HistoryCacheEvent.OnClickClearSearchHistory(R.string.clear_search_history_key) | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         IrreversiblePreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.clear_cookie_title), | ||||||
|  |             summary = stringResource(id = R.string.clear_cookie_summary), | ||||||
|  |             onClick = { | ||||||
|  |                 onEvent(HistoryCacheEvent.OnClickReCaptchaCookies(R.string.recaptcha_cookies_key)) | ||||||
|  |             }, | ||||||
|  |             enabled = recaptchaCookiesEnabled, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         if (isDialogVisible) { | ||||||
|  |             CacheAlertDialog( | ||||||
|  |                 dialogTitle = dialogTitle, | ||||||
|  |                 onClickCancel = { isDialogVisible = false }, | ||||||
|  |                 onClick = dialogOnClick | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) | ||||||
|  | @Composable | ||||||
|  | private fun CachePreferencesComponentPreview() { | ||||||
|  |     AppTheme { | ||||||
|  |         Scaffold { padding -> | ||||||
|  |             CachePreferencesComponent( | ||||||
|  |                 recaptchaCookiesEnabled = false, | ||||||
|  |                 onEvent = {}, | ||||||
|  |                 modifier = Modifier | ||||||
|  |                     .fillMaxWidth() | ||||||
|  |                     .padding(padding) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | private fun CacheAlertDialog( | ||||||
|  |     dialogTitle: String, | ||||||
|  |     onClickCancel: () -> Unit, | ||||||
|  |     onClick: () -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     AlertDialog( | ||||||
|  |         onDismissRequest = onClickCancel, | ||||||
|  |         confirmButton = { | ||||||
|  |             TextButton(onClick = onClick) { | ||||||
|  |                 Text(text = "Delete") | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         dismissButton = { | ||||||
|  |             TextButton(onClick = onClickCancel) { | ||||||
|  |                 Text(text = "Cancel") | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         title = { | ||||||
|  |             Text(text = dialogTitle) | ||||||
|  |         }, | ||||||
|  |         text = { | ||||||
|  |             Text(text = "This is an irreversible action") | ||||||
|  |         }, | ||||||
|  |         modifier = modifier | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(backgroundColor = 0xFFFFFFFF) | ||||||
|  | @Composable | ||||||
|  | private fun CacheAlertDialogPreview() { | ||||||
|  |     AppTheme { | ||||||
|  |         Scaffold { padding -> | ||||||
|  |             CacheAlertDialog( | ||||||
|  |                 dialogTitle = "Delete view history", | ||||||
|  |                 onClickCancel = {}, | ||||||
|  |                 onClick = {}, | ||||||
|  |                 modifier = Modifier.padding(padding) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,94 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache.components | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | import androidx.compose.foundation.layout.fillMaxWidth | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | 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.Modifier | ||||||
|  | import androidx.compose.ui.res.stringResource | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import org.schabi.newpipe.R | ||||||
|  | import org.schabi.newpipe.settings.components.switch_preference.SwitchPreferenceComponent | ||||||
|  | import org.schabi.newpipe.settings.presentation.history_cache.state.SwitchPreferencesUiState | ||||||
|  | import org.schabi.newpipe.ui.theme.AppTheme | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun HistoryPreferencesComponent( | ||||||
|  |     state: SwitchPreferencesUiState, | ||||||
|  |     onEvent: (Int, Boolean) -> Unit, | ||||||
|  |     modifier: Modifier = Modifier, | ||||||
|  | ) { | ||||||
|  |     Column( | ||||||
|  |         modifier = modifier, | ||||||
|  |     ) { | ||||||
|  |         SwitchPreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.enable_watch_history_title), | ||||||
|  |             summary = stringResource(id = R.string.enable_watch_history_summary), | ||||||
|  |             isChecked = state.watchHistoryEnabled, | ||||||
|  |             onCheckedChange = { | ||||||
|  |                 onEvent(R.string.enable_watch_history_key, it) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         SwitchPreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.enable_playback_resume_title), | ||||||
|  |             summary = stringResource(id = R.string.enable_playback_resume_summary), | ||||||
|  |             isChecked = state.resumePlaybackEnabled, | ||||||
|  |             onCheckedChange = { | ||||||
|  |                 onEvent(R.string.enable_playback_resume_key, it) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         SwitchPreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.enable_playback_state_lists_title), | ||||||
|  |             summary = stringResource(id = R.string.enable_playback_state_lists_summary), | ||||||
|  |             isChecked = state.positionsInListsEnabled, | ||||||
|  |             onCheckedChange = { | ||||||
|  |                 onEvent(R.string.enable_playback_state_lists_key, it) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |         SwitchPreferenceComponent( | ||||||
|  |             title = stringResource(id = R.string.enable_search_history_title), | ||||||
|  |             summary = stringResource(id = R.string.enable_search_history_summary), | ||||||
|  |             isChecked = state.searchHistoryEnabled, | ||||||
|  |             onCheckedChange = { | ||||||
|  |                 onEvent(R.string.enable_search_history_key, it) | ||||||
|  |             }, | ||||||
|  |             modifier = Modifier.fillMaxWidth() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Preview(showBackground = true) | ||||||
|  | @Composable | ||||||
|  | private fun SwitchPreferencesComponentPreview() { | ||||||
|  |     var state by remember { | ||||||
|  |         mutableStateOf( | ||||||
|  |             SwitchPreferencesUiState() | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |     AppTheme( | ||||||
|  |         useDarkTheme = false | ||||||
|  |     ) { | ||||||
|  |         Scaffold { padding -> | ||||||
|  |             HistoryPreferencesComponent( | ||||||
|  |                 state = state, | ||||||
|  |                 onEvent = { _, _ -> | ||||||
|  |                     // Mock behaviour to preview | ||||||
|  |                     state = state.copy( | ||||||
|  |                         watchHistoryEnabled = !state.watchHistoryEnabled | ||||||
|  |                     ) | ||||||
|  |                 }, | ||||||
|  |                 modifier = Modifier | ||||||
|  |                     .fillMaxWidth() | ||||||
|  |                     .padding(padding), | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache.events | ||||||
|  |  | ||||||
|  | sealed class HistoryCacheEvent { | ||||||
|  |     data class OnUpdateBooleanPreference(val key: Int, val isEnabled: Boolean) : HistoryCacheEvent() | ||||||
|  |     data class OnClickWipeCachedMetadata(val key: Int) : HistoryCacheEvent() | ||||||
|  |     data class OnClickClearWatchHistory(val key: Int) : HistoryCacheEvent() | ||||||
|  |     data class OnClickDeletePlaybackPositions(val key: Int) : HistoryCacheEvent() | ||||||
|  |     data class OnClickClearSearchHistory(val key: Int) : HistoryCacheEvent() | ||||||
|  |     data class OnClickReCaptchaCookies(val key: Int) : HistoryCacheEvent() | ||||||
|  | } | ||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache.events | ||||||
|  |  | ||||||
|  | sealed class HistoryCacheUiEvent { | ||||||
|  |     data object ShowDeletePlaybackSnackbar : HistoryCacheUiEvent() | ||||||
|  |     data object ShowDeleteSearchHistorySnackbar : HistoryCacheUiEvent() | ||||||
|  |     data object ShowClearWatchHistorySnackbar : HistoryCacheUiEvent() | ||||||
|  |     data object ShowReCaptchaCookiesSnackbar : HistoryCacheUiEvent() | ||||||
|  |     data object ShowWipeCachedMetadataSnackbar : HistoryCacheUiEvent() | ||||||
|  | } | ||||||
| @@ -0,0 +1,10 @@ | |||||||
|  | package org.schabi.newpipe.settings.presentation.history_cache.state | ||||||
|  |  | ||||||
|  | import androidx.compose.runtime.Stable | ||||||
|  | @Stable | ||||||
|  | data class SwitchPreferencesUiState( | ||||||
|  |     val watchHistoryEnabled: Boolean = false, | ||||||
|  |     val resumePlaybackEnabled: Boolean = false, | ||||||
|  |     val positionsInListsEnabled: Boolean = false, | ||||||
|  |     val searchHistoryEnabled: Boolean = false, | ||||||
|  | ) | ||||||
| @@ -23,7 +23,7 @@ | |||||||
|         app:iconSpaceReserved="false" /> |         app:iconSpaceReserved="false" /> | ||||||
|  |  | ||||||
|     <PreferenceScreen |     <PreferenceScreen | ||||||
|         android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment" |         android:fragment="org.schabi.newpipe.settings.presentation.history_cache.HistoryCacheFragment" | ||||||
|         android:icon="@drawable/ic_history" |         android:icon="@drawable/ic_history" | ||||||
|         android:title="@string/settings_category_history_title" |         android:title="@string/settings_category_history_title" | ||||||
|         app:iconSpaceReserved="false" /> |         app:iconSpaceReserved="false" /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 brais
					brais