diff --git a/app/build.gradle b/app/build.gradle
index 24e3d32cf..b9e621aca 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -310,9 +310,11 @@ dependencies {
// Coroutines interop
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-rx3:1.8.1'
- // Hilt
+ // Hilt & Dagger
implementation("com.google.dagger:hilt-android: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
implementation 'com.github.nanihadesuka:LazyColumnScrollbar:2.2.0'
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 41abd10c1..5d956ff41 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -94,6 +94,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import dagger.hilt.android.AndroidEntryPoint;
+
+@AndroidEntryPoint
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@SuppressWarnings("ConstantConditions")
diff --git a/app/src/main/java/org/schabi/newpipe/dependency_injection/AppModule.kt b/app/src/main/java/org/schabi/newpipe/dependency_injection/AppModule.kt
new file mode 100644
index 000000000..1365f80f8
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/dependency_injection/AppModule.kt
@@ -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)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/dependency_injection/DatabaseModule.kt b/app/src/main/java/org/schabi/newpipe/dependency_injection/DatabaseModule.kt
new file mode 100644
index 000000000..4e20fe30c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/dependency_injection/DatabaseModule.kt
@@ -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()
+}
diff --git a/app/src/main/java/org/schabi/newpipe/error/usecases/OpenErrorActivity.kt b/app/src/main/java/org/schabi/newpipe/error/usecases/OpenErrorActivity.kt
new file mode 100644
index 000000000..5168c0d3c
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/error/usecases/OpenErrorActivity.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
index fac358075..830fa9b8f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/StatisticsPlaylistFragment.java
@@ -13,6 +13,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.viewbinding.ViewBinding;
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.StatisticPlaylistControlBinding;
import org.schabi.newpipe.error.ErrorInfo;
+import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
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.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
-import org.schabi.newpipe.settings.HistorySettingsFragment;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.OnClickGesture;
import org.schabi.newpipe.util.PlayButtonHelper;
@@ -161,14 +162,72 @@ public class StatisticsPlaylistFragment
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
if (item.getItemId() == R.id.action_history_clear) {
- HistorySettingsFragment
- .openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
+ openDeleteWatchHistoryDialog(requireContext(), recordManager, disposables);
} else {
return super.onOptionsItemSelected(item);
}
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
///////////////////////////////////////////////////////////////////////////
@@ -307,7 +366,7 @@ public class StatisticsPlaylistFragment
sortMode = StatisticSortMode.LAST_PLAYED;
setTitle(getString(R.string.title_last_played));
headerBinding.sortButtonIcon.setImageResource(
- R.drawable.ic_filter_list);
+ R.drawable.ic_filter_list);
headerBinding.sortButtonText.setText(R.string.title_most_played);
}
startLoading(true);
diff --git a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
index 0d57ce174..0aab12114 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/SettingsActivity.java
@@ -43,6 +43,9 @@ import org.schabi.newpipe.views.FocusOverlayView;
import java.util.concurrent.TimeUnit;
+import dagger.hilt.android.AndroidEntryPoint;
+
+
/*
* Created by Christian Schabesberger on 31.08.15.
*
@@ -63,6 +66,7 @@ import java.util.concurrent.TimeUnit;
* along with NewPipe. If not, see .
*/
+@AndroidEntryPoint
public class SettingsActivity extends AppCompatActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
PreferenceSearchResultListener {
diff --git a/app/src/main/java/org/schabi/newpipe/settings/components/irreversible_preference/IrreversiblePreference.kt b/app/src/main/java/org/schabi/newpipe/settings/components/irreversible_preference/IrreversiblePreference.kt
new file mode 100644
index 000000000..f9cc79099
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/components/irreversible_preference/IrreversiblePreference.kt
@@ -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
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/components/switch_preference/SwitchPreference.kt b/app/src/main/java/org/schabi/newpipe/settings/components/switch_preference/SwitchPreference.kt
new file mode 100644
index 000000000..61a64e863
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/components/switch_preference/SwitchPreference.kt
@@ -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()
+ )
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/dependency_injection/SettingsModule.kt b/app/src/main/java/org/schabi/newpipe/settings/dependency_injection/SettingsModule.kt
new file mode 100644
index 000000000..d0f271025
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/dependency_injection/SettingsModule.kt
@@ -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 = GetPreferenceImpl(sharedPreferences, context)
+
+ @Provides
+ @Singleton
+ fun provideGetStringPreference(
+ sharedPreferences: SharedPreferences,
+ @ApplicationContext context: Context,
+ ): GetPreference = GetPreferenceImpl(sharedPreferences, context)
+
+ @Provides
+ @Singleton
+ fun provideUpdateBooleanPreference(
+ sharedPreferences: SharedPreferences,
+ @ApplicationContext context: Context,
+ ): UpdatePreference = UpdatePreferenceImpl(context, sharedPreferences) { key, value ->
+ putBoolean(
+ key,
+ value
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideUpdateStringPreference(
+ sharedPreferences: SharedPreferences,
+ @ApplicationContext context: Context,
+ ): UpdatePreference = UpdatePreferenceImpl(context, sharedPreferences) { key, value ->
+ putString(
+ key,
+ value
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideUpdateIntPreference(
+ sharedPreferences: SharedPreferences,
+ @ApplicationContext context: Context,
+ ): UpdatePreference = 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
+ )
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepository.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepository.kt
new file mode 100644
index 000000000..3b9edaa48
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepository.kt
@@ -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
+ fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow
+ fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow
+ fun deleteCompleteSearchHistory(dispatcher: CoroutineDispatcher): Flow
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryFake.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryFake.kt
new file mode 100644
index 000000000..6e03fec47
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryFake.kt
@@ -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> = MutableStateFlow(
+ emptyList()
+ )
+ val searchHistory = _searchHistory.asStateFlow()
+ private val _streamHistory = MutableStateFlow>(emptyList())
+ val streamHistory = _streamHistory.asStateFlow()
+ private val _streams = MutableStateFlow>(emptyList())
+ val streams = _streams.asStateFlow()
+ private val _streamStates = MutableStateFlow>(emptyList())
+ val streamStates = _streamStates.asStateFlow()
+
+ override fun deleteCompleteStreamState(dispatcher: CoroutineDispatcher): Flow = flow {
+ val count = streamStates.value.size
+ _streamStates.update {
+ emptyList()
+ }
+ emit(count)
+ }.flowOn(dispatcher)
+
+ override fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow = flow {
+ val count = streamHistory.value.size
+ _streamHistory.update {
+ emptyList()
+ }
+ emit(count)
+ }.flowOn(dispatcher)
+
+ override fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow = 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 = flow {
+ val count = searchHistory.value.size
+ _searchHistory.update {
+ emptyList()
+ }
+ emit(count)
+ }.flowOn(dispatcher)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryImpl.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryImpl.kt
new file mode 100644
index 000000000..b1d4abeeb
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/repositories/HistoryRecordRepositoryImpl.kt
@@ -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 =
+ flow {
+ val deletedCount = streamStateDao.deleteAll()
+ emit(deletedCount)
+ }.flowOn(dispatcher)
+
+ override fun deleteWholeStreamHistory(dispatcher: CoroutineDispatcher): Flow =
+ flow {
+ val deletedCount = streamHistoryDAO.deleteAll()
+ emit(deletedCount)
+ }.flowOn(dispatcher)
+
+ override fun removeOrphanedRecords(dispatcher: CoroutineDispatcher): Flow = flow {
+ val deletedCount = streamDAO.deleteOrphans()
+ emit(deletedCount)
+ }.flowOn(dispatcher)
+
+ override fun deleteCompleteSearchHistory(dispatcher: CoroutineDispatcher): Flow = flow {
+ val deletedCount = searchHistoryDAO.deleteAll()
+ emit(deletedCount)
+ }.flowOn(dispatcher)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteSearchHistory.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteSearchHistory.kt
new file mode 100644
index 000000000..7b2c2d99a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteSearchHistory.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteStreamStateHistory.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteStreamStateHistory.kt
new file mode 100644
index 000000000..0b516966d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteCompleteStreamStateHistory.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeletePlaybackStates.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeletePlaybackStates.kt
new file mode 100644
index 000000000..e8416a492
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeletePlaybackStates.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteWatchHistory.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteWatchHistory.kt
new file mode 100644
index 000000000..403f6f1c0
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/DeleteWatchHistory.kt
@@ -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
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/RemoveOrphanedRecords.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/RemoveOrphanedRecords.kt
new file mode 100644
index 000000000..42e9cd00d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/RemoveOrphanedRecords.kt
@@ -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()
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreference.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreference.kt
new file mode 100644
index 000000000..19ed9d226
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreference.kt
@@ -0,0 +1,7 @@
+package org.schabi.newpipe.settings.domain.usecases.get_preference
+
+import kotlinx.coroutines.flow.Flow
+
+fun interface GetPreference {
+ operator fun invoke(key: Int, defaultValue: T): Flow
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceFake.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceFake.kt
new file mode 100644
index 000000000..805a30083
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceFake.kt
@@ -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(
+ private val preferences: MutableStateFlow>,
+) : GetPreference {
+ override fun invoke(key: Int, defaultValue: T): Flow {
+ return preferences.asStateFlow().map {
+ it[key] ?: defaultValue
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceImpl.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceImpl.kt
new file mode 100644
index 000000000..bb87025b7
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/get_preference/GetPreferenceImpl.kt
@@ -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(
+ private val sharedPreferences: SharedPreferences,
+ private val context: Context,
+) : GetPreference {
+ override fun invoke(key: Int, defaultValue: T): Flow {
+ val keyString = context.getString(key)
+ return sharedPreferences.getFlowForKey(keyString, defaultValue)
+ }
+
+ private fun 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 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")
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreference.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreference.kt
new file mode 100644
index 000000000..0a1b0e966
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreference.kt
@@ -0,0 +1,5 @@
+package org.schabi.newpipe.settings.domain.usecases.update_preference
+
+fun interface UpdatePreference {
+ suspend operator fun invoke(key: Int, value: T)
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceFake.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceFake.kt
new file mode 100644
index 000000000..a67677584
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceFake.kt
@@ -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(
+ private val preferences: MutableStateFlow>,
+) : UpdatePreference {
+ override suspend fun invoke(key: Int, value: T) {
+ preferences.update {
+ it.apply {
+ put(key, value)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceImpl.kt b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceImpl.kt
new file mode 100644
index 000000000..e51ec731b
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/domain/usecases/update_preference/UpdatePreferenceImpl.kt
@@ -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(
+ private val context: Context,
+ private val sharedPreferences: SharedPreferences,
+ private val setter: SharedPreferences.Editor.(String, T) -> SharedPreferences.Editor,
+) : UpdatePreference {
+ override suspend operator fun invoke(key: Int, value: T) {
+ val stringKey = context.getString(key)
+ sharedPreferences.edit {
+ setter(stringKey, value)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheFragment.kt
new file mode 100644
index 000000000..d9a2f49c5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheFragment.kt
@@ -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)
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsScreen.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsScreen.kt
new file mode 100644
index 000000000..9a246257a
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsScreen.kt
@@ -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()
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsViewModel.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsViewModel.kt
new file mode 100644
index 000000000..49a8d3f1f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/HistoryCacheSettingsViewModel.kt
@@ -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,
+ private val updateBooleanPreference: UpdatePreference,
+ private val getStringPreference: GetPreference,
+ private val getBooleanPreference: GetPreference,
+ 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 = _switchState.asStateFlow()
+
+ private val _captchaCookies = MutableStateFlow(false)
+ val captchaCookies: StateFlow = _captchaCookies.asStateFlow()
+
+ private val _eventFlow = MutableSharedFlow()
+ 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)
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/CachePreferences.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/CachePreferences.kt
new file mode 100644
index 000000000..c52593814
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/CachePreferences.kt
@@ -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)
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/HistoryPreferences.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/HistoryPreferences.kt
new file mode 100644
index 000000000..c9ae14e41
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/components/HistoryPreferences.kt
@@ -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),
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheEvent.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheEvent.kt
new file mode 100644
index 000000000..0bd5d49c5
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheEvent.kt
@@ -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()
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheUiEvent.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheUiEvent.kt
new file mode 100644
index 000000000..c40c1b45f
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/events/HistoryCacheUiEvent.kt
@@ -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()
+}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/state/SwitchPreferencesUiState.kt b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/state/SwitchPreferencesUiState.kt
new file mode 100644
index 000000000..887aaf5ac
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/settings/presentation/history_cache/state/SwitchPreferencesUiState.kt
@@ -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,
+)
diff --git a/app/src/main/res/xml/main_settings.xml b/app/src/main/res/xml/main_settings.xml
index 5f96989f9..7920b7a06 100644
--- a/app/src/main/res/xml/main_settings.xml
+++ b/app/src/main/res/xml/main_settings.xml
@@ -23,7 +23,7 @@
app:iconSpaceReserved="false" />