Added the new compose screen with its components and events

This commit is contained in:
brais 2024-08-05 00:42:01 +02:00 committed by Profpatsch
parent 2eb85bd804
commit c3aa4e78ec
33 changed files with 1523 additions and 6 deletions

View File

@ -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'

View File

@ -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")

View File

@ -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)
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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
///////////////////////////////////////////////////////////////////////////

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
@AndroidEntryPoint
public class SettingsActivity extends AppCompatActivity implements
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback,
PreferenceSearchResultListener {

View File

@ -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
)
}
}
}

View File

@ -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()
)
}
}

View File

@ -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
)
}

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
)
}
}
}

View File

@ -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()
}
}

View File

@ -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>
}

View File

@ -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
}
}
}

View File

@ -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")
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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()
)
}
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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),
)
}
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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,
)

View File

@ -23,7 +23,7 @@
app:iconSpaceReserved="false" />
<PreferenceScreen
android:fragment="org.schabi.newpipe.settings.HistorySettingsFragment"
android:fragment="org.schabi.newpipe.settings.presentation.history_cache.HistoryCacheFragment"
android:icon="@drawable/ic_history"
android:title="@string/settings_category_history_title"
app:iconSpaceReserved="false" />