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